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