How to bind all interfaces of a service using Microsoft's DependencyInjection framework
While working on our new Xamarin mobile app we migrated parts of our code base from .NET Framework to .NET Standard. This meant we needed a new dependency injection container for .NET Standard and Microsoft's DependencyInjection framework (as used in ASP.NET Core) ticked all the boxes... except for one.
Our requirements were straightforward:
☑ Must run on all platforms including web, desktop and mobile.
☑ Must support constructor injection.
☑ Modules that register services with the container should depend on a stable abstraction.
☒ It should be simple to register services that implement multiple interfaces.
Unfortunately, Microsoft's framework required us to manually bind all interfaces for a particular service. An unnecessary burden and one feature we took for granted with our previous abstraction (NServiceBus' Configurer).
The NuGet package Microsoft.Extensions.DependencyInjection.Abstractions
provides two main interfaces
IServiceCollection
- used to register interfaces to implementations for a specific lifetime with the container
void RegisterServices(IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency>();
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation());
}
IServiceProvider
- used to build instances of interfaces/services
void BuildServices(IServiceProvider provider)
{
var dependency1 = provider.GetRequiredService<IMyDependency>();
var operation1 = provider.GetService<IOperationTransient>();
}
In order to meet our last requirement and simplify registering types, we created extension methods on IServiceCollection
based off the work done by Particular/NServiceBus on their own DI adapters.
/// <summary>
/// Registers the type <see paramref="TType"/> and all its alias (interfaces + base type) with the container with the specified <see paramref="lifetime"/>
/// </summary>
/// <typeparam name="TType"></typeparam>
/// <param name="services"></param>
/// <param name="lifetime"></param>
/// <returns></returns>
public static IServiceCollection RegisterComponent<TType>(this IServiceCollection services, ServiceLifetime lifetime)
{
var type = typeof(TType);
services.Add(new ServiceDescriptor(type, type, lifetime));
BindAliasesOfComponentToComponent(services, type, lifetime);
return services;
}
In the above code we first add a service descriptor that maps the type to itself, then we bind all of its aliases (interfaces) to itself which looks like:
private static void BindAliasesOfComponentToComponent(IServiceCollection serviceCollection, Type component, ServiceLifetime lifetime)
{
var services = GetAllServices(component).Where(t=> t != component);
foreach (var service in services)
{
// Bind all interfaces to a factory method which builds the implementation type
serviceCollection.Add(new ServiceDescriptor(service, ctx => ctx.GetService(component), lifetime));
}
var baseType = component.BaseType;
if (baseType == null || !baseType.IsAbstract)
{
return;
}
// Don't forget to bind an abstract base type to the implementation
serviceCollection.Add(new ServiceDescriptor(baseType, ctx => ctx.GetService(component), lifetime));
}
We use reflection to recursively find all of a type's interfaces:
static IEnumerable<Type> GetAllServices(Type type)
{
if (type == null)
{
return new List<Type>();
}
var result = new List<Type>(type.GetInterfaces())
{
type
};
foreach (var interfaceType in type.GetInterfaces())
{
result.AddRange(GetAllServices(interfaceType));
}
return result.Distinct();
}
Finally in our module we can use this new extension method to automatically bind a type to all of its interfaces:
using App.Extensions;
using Microsoft.Extensions.DependencyInjection;
public class ServiceAgent : IServiceAgent, IWantNotifications
{
}
public class Module
{
public void Initialize(IServiceCollection services)
{
services.RegisterComponent<ServiceAgent>(ServiceLifetime.Singleton);
}
}
In this example the interfaces IServiceAgent
and IWantNotifications
are bound to the same singleton class ServiceAgent
. They can now be built using provider.GetService<IServiceAgent>()
or used in constructor injection for other services.