Blazor: Scoping services to a component

If you want your Blazor component to have its own dependency injection scope, so you can inject new instances of services and have those services disposed of when the component is disposed, then do the following:

  1. First, copy and paste the code below into your application. 
  2. Then descend your component from ScopedComponentBase.
  3. Finally, decorate your dependency properties with [InjectScoped] rather than [Inject] or @inject.
For example
@inherits ScopedComponentBase

@code
{
  [InjectScoped]
  private WeatherService MyWeatherService { get; set; }
}


Here is the code you need
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace BlazorApp130.Shared
{
 public abstract class ScopedComponentBase : OwningComponentBase<IServiceProvider>
 {
  protected override void OnInitialized()
  {
   base.OnInitialized();
   DependencyInjector.InjectDependencies(this, Service);
  }
 }

 [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
 public class InjectScopedAttribute : Attribute
 {
 }

 internal static class DependencyInjector
 {
  private static readonly ConcurrentDictionary<Type, IEnumerable<PropertyInjector>>
   TypeToPropertyInjectors = new ConcurrentDictionary<Type, IEnumerable<PropertyInjector>>();

  public static void InjectDependencies(
   ScopedComponentBase instance,
   IServiceProvider serviceProvider)
  {
   IEnumerable<PropertyInjector> propertyInjectors =
    TypeToPropertyInjectors
    .GetOrAdd(instance.GetType(), _ => CreatePropertyInjectors(instance));

   var serviceTypes = propertyInjectors
    .Select(x => x.ServiceType)
    .Distinct()
    .ToDictionary(x => x, x => serviceProvider.GetService(x));

   foreach (var injector in propertyInjectors)
    injector.PropertySetter(instance, serviceTypes[injector.ServiceType]);
  }

  private static IEnumerable<PropertyInfo> GetAllProperties(Type t) =>
    t == typeof(object)
    ? Enumerable.Empty<PropertyInfo>()
    : GetStateProperties(t.BaseType)
      .Union(
        t.GetProperties(
          BindingFlags.Instance
          | BindingFlags.Public
          | BindingFlags.NonPublic
          | BindingFlags.DeclaredOnly));

  private static IEnumerable<PropertyInjector> CreatePropertyInjectors(
    ScopedComponentBase instance)
  {
   Type componentType = instance.GetType();

   var properties = componentType
    .GetAllProperties(componentType)
    .Select(p =>
     new
     { 
      Property = p,
      Attribute = p.GetCustomAttribute<InjectScopedAttribute>()
     })
    .Where(x => x.Attribute != null)
    .Select(x =>
     new
     {
      x.Property.PropertyType,
      x.Property.SetMethod
     })
    .Where(x => x.SetMethod != null);

   var injectors = new List<PropertyInjector>();
   foreach(var property in properties)
   {
    var setterDelegateType = 
     typeof(Action<,>)
     .MakeGenericType(componentType, property.PropertyType);

    var setterDelegate = 
     Delegate
     .CreateDelegate(setterDelegateType, property.SetMethod);

    Action<object, object> setProperty = (instance, service) =>
     setterDelegate.DynamicInvoke(instance, service);

    var injector = new PropertyInjector(property.PropertyType, setProperty);
    injectors.Add(injector);
   }
   return injectors;
  }

  private class PropertyInjector
  {
   public Type ServiceType { get; }
   public Action<object, object> PropertySetter { get; }

   public PropertyInjector(Type serviceType, Action<object, object> propertySetter)
   {
    ServiceType = serviceType;
    PropertySetter = propertySetter;
   }
  }
 }
}

Comments

Popular posts from this blog

Angular - How to create composite controls that work with formGroup/formGroupName and ReactiveForms

Convert absolute path to relative path

Blazor setTimeout