When you want your Blazor component to own an injected dependency and have it disposed along with the component, you will likely have used the OwningComponentBase<T> component.
There are two problems with using this class.
- It only allows you to specify a single dependency to own.
- If you inject a service registered as Transient that also implements IDisposable, then each newly created instance will hang around for the lifetime of your application.
To overcome these limitations, we can inject
- First, copy and paste the code below into your application.
- Then descend your component from IsolatedComponentBase.
- Finally, decorate your dependency properties with [InjectIsolated] rather than [Inject] or @inject.
For example
@inherits IsolatedComponentBase @code { [InjectIsolated] private IMyService MyService { get; set; } }
The implementation code (listed later) will create a new scope for your component and inject all dependencies.
Note that any services registered as Singleton will be shared with the root injection container, as usual; however, any registered as Scoped or Transient will be created inside an injection container that belongs specifically to your component.
Any components rendered within your component will *not* have the same instance injected. If this is something you need to do, then you should use a CascadingValue.
using Microsoft.AspNetCore.Components; using System.Collections.Concurrent; using System.Reflection; namespace MyApp; public abstract class IsolatedComponentBase : OwningComponentBase<IServiceScopeFactory>, IAsyncDisposable { private AsyncServiceScope ServiceScope; protected IServiceProvider ServiceProvider { get; private set; } = null!; protected override void OnInitialized() { base.OnInitialized(); ServiceScope = Service.CreateAsyncScope(); ServiceProvider = ServiceScope.ServiceProvider; DependencyInjector.InjectDependencies(this, ServiceProvider); } ValueTask IAsyncDisposable.DisposeAsync() { return DisposeAsyncCore(); } protected virtual ValueTask DisposeAsyncCore() { return ServiceScope.DisposeAsync(); } } [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public class InjectIsolatedAttribute : Attribute { } internal static class DependencyInjector { private static readonly ConcurrentDictionary<Type, IEnumerable<PropertyInjector>> TypeToPropertyInjectors = new ConcurrentDictionary<Type, IEnumerable<PropertyInjector>>(); public static void InjectDependencies( IsolatedComponentBase instance, IServiceProvider serviceProvider) { IEnumerable<PropertyInjector> propertyInjectors = TypeToPropertyInjectors .GetOrAdd(instance.GetType(), type => CreatePropertyInjectors(type)); Dictionary<Type, object> 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<PropertyInjector> CreatePropertyInjectors(Type componentType) { var properties = componentType .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Select(p => new { Property = p, Attribute = p.GetCustomAttribute<InjectIsolatedAttribute>() }) .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