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.