Blazor: Scoping services to a component

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.

  1. It only allows you to specify a single dependency to own.
  2. 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

  1. First, copy and paste the code below into your application. 
  2. Then descend your component from IsolatedComponentBase.
  3. 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

Leave a Reply

Your email address will not be published. Required fields are marked *