Dependency Injection, the missing methods

In dotnet, we configure the dependency injection by adding services into a service collection, that simply implements IServiceCollection, a specialization of IList<ServiceDescriptor>. The IServiceCollection does not define any method or property. All the work is done by extensions methods of two kind:

  • the operationals, quite shallow, that manipulate the service collection by helping to create the ServiceDescriptor to add, or by removing or replacing some services.
  • the configurationals, with more depth, to add a set of services in order to fulfill a specific goal such as adding logging or configuring the options.

The extensions I am going to introduce are operationals.

Add Alias

ISP encourages us to design fine grained interfaces but a class may implement more than one interface. Registering the same implementation for several services, will only result in instanciating the same class more than one, which can lead to subtle bugs, especially if the implementation is supposed to be a singleton.

Andrew Locked discussed this in great length in his post How to register a service with multiple interfaces in ASP.NET Core DI

The following extension method handle both of his solutions:

/// <summary>Declares another service associated to an already registered implementation.</summary>
/// <typeparam name="TService">The type of the service to add.</typeparam>
/// <typeparam name="TImplementation">The type of the implementation to use.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
/// <remarks><see cref="IDisposable.Dispose"/> may be called several time so it is important for its implementation to be idempotent.</remarks>
public static IServiceCollection AddAlias<TService, TImplementation>(this IServiceCollection services)
    where TService : class
    where TImplementation : class, TService
{
    var descriptor = services.Single(d => (d.ImplementationType ?? d.ServiceType) == typeof(TImplementation));
    if (descriptor.ImplementationInstance is not null)
    {
        // singleton instance, register the alias as such to prevent dispose
        Debug.Assert(descriptor.Lifetime == ServiceLifetime.Singleton);
        services.Add(new ServiceDescriptor(typeof(TService), descriptor.ImplementationInstance));
    }
    else if (descriptor.Lifetime == ServiceLifetime.Transient)
    {
        throw new InvalidOperationException("Alias on transient services are meaningless as a new instance is always created for each resolution.");
    }
    else
    {
        // the same type of service can be registered multiple time, so get the service of the expected implementation.
        // propagate the lifetime but, unfortunatelly, dispose will be called more than once.
        Func<IServiceProvider, TImplementation> resolver = sp => sp.GetServices(descriptor.ServiceType).OfType<TImplementation>().Single();
        services.Add(new ServiceDescriptor(typeof(TService), resolver, descriptor.Lifetime));
    }
    return services;
}

The post Add alias to depency injection covers this with more details.

Add Parametrized Services

Sometime, you may have a service that have both services and arguments as parameters.

The easiest way is to register the services with a factory using ActivatorUtilities.CreateInstance.

/// <summary>
/// Adds a transient service of the type specified in serviceType with an instance activated by <see cref="ActivatorUtilities"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
/// <param name="serviceType">The type of the service to register.</param>
/// <param name="implementationType">The implementation type of the service.</param>
/// <param name="parameters">Constructor arguments not provided by the provider.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static IServiceCollection AddParametrizedTransient(this IServiceCollection services
    , Type serviceType
    , [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType
    , params object[] parameters)
{
    return services.AddTransient(serviceType, sp => ActivatorUtilities.CreateInstance(sp, implementationType, parameters));
}

/// <summary>
/// Adds a transient service of the type specified in serviceType with an instance activated by <see cref="ActivatorUtilities"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
/// <param name="implementationType">The implementation type of the service.</param>
/// <param name="parameters">Constructor arguments not provided by the provider.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static IServiceCollection AddParametrizedTransient(this IServiceCollection services
    , [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType
    , params object[] parameters)
{
    return services.AddTransient(sp => ActivatorUtilities.CreateInstance(sp, implementationType, parameters));
}

/// <summary>
/// Adds a transient service of the type specified in serviceType with an instance activated by <see cref="ActivatorUtilities"/>.
/// </summary>
/// <typeparam name="TService">The type of the service to add.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
/// <param name="parameters">Constructor arguments not provided by the provider.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static IServiceCollection AddParametrizedTransient<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection services
    , params object[] parameters)
    where TService : class
{
    return services.AddTransient(sp => ActivatorUtilities.CreateInstance<TService>(sp, parameters));
}

/// <summary>
/// Adds a transient service of the type specified in serviceType with an instance activated by <see cref="ActivatorUtilities"/>.
/// </summary>
/// <typeparam name="TService">The type of the service to add.</typeparam>
/// <typeparam name="TImplementation">The type of the implementation to use.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the service to.</param>
/// <param name="parameters">Constructor arguments not provided by the provider.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
public static IServiceCollection AddParametrizedTransient<TService, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection services
    , params object[] parameters)
    where TService : class
    where TImplementation : class, TService
{
    return services.AddTransient<TService>(sp => ActivatorUtilities.CreateInstance<TImplementation>(sp, parameters));
}

Implementing AddParametrizedScoped and AddParametrizedSingleton for the other lifetime is pretty straightforward.

Add Decorator or Proxy

By design, the dependency injection mechanism hides which type is instantiated when you get a service. And when you do, you may not be able to derived it to add the behavior you need. In such case, you may need to implement a decorator or a proxy and then you have to instanciate that decorator or proxy and ensure this decorator or proxy is the instance returned by the dependency injection.

/// <summary>
/// Decorates a service already registered in the service collection and replaces it.
/// </summary>
/// <typeparam name="TService">The type of service to decorate.</typeparam>
/// <typeparam name="TImplementation">The type of the implementation to use.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="parameters">Constructor arguments not provided by the provider.</param>
/// <returns>A reference to this instance after the operation has completed.</returns>
/// <exception cref="InvalidOperationException">The underlying service had neither an instance, a factory or an implementation type.</exception>
/// <exception cref="InvalidCastException">The provided instance was not of the expected type.</exception>
public static IServiceCollection AddDecoratorOrProxy<TService, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection services
    , params object[] parameters)
    where TService : class
    where TImplementation : class, TService
{
    var descriptor = services.Single(d => d.ServiceType == typeof(TService));
    services.Remove(descriptor);

    var lifetime = descriptor.Lifetime;
    if (descriptor.ImplementationInstance is not null)
    {
        Debug.Assert(lifetime == ServiceLifetime.Singleton);
        services.Register<TService>(lifetime, sp => CreateInstance(sp, typeof(TImplementation), descriptor.ImplementationInstance, parameters));
    }
    else if (descriptor.ImplementationFactory is not null)
    {
        services.Register<Holder<TImplementation>>(lifetime, sp => new Holder<TImplementation>(descriptor.ImplementationFactory(sp)));
        services.Register<TService>(lifetime, sp => CreateInstance(sp, typeof(TImplementation), GetBack<TImplementation>(sp), parameters));
    }
    else if (typeof(IDisposable).IsAssignableFrom(descriptor.ImplementationType))
    {
        services.Register<Holder<TImplementation>>(lifetime, sp => new Holder<TImplementation>(ActivatorUtilities.CreateInstance(sp, descriptor.ImplementationType)));
        services.Register<TService>(lifetime, sp => CreateInstance(sp, typeof(TImplementation), GetBack<TImplementation>(sp), parameters));
    }
    else if (descriptor.ImplementationType is not null)
    {
        services.Register<TService>(lifetime, sp => CreateInstance(sp, typeof(TImplementation), ActivatorUtilities.CreateInstance(sp, descriptor.ImplementationType), parameters));
    }
    else
    {
        throw new InvalidOperationException("The underlying service had neither an instance, a factory or an implementation type.");
    }
    return services;
}

#region Utilities

private static void Register<TService>(this IServiceCollection services, ServiceLifetime lifetime, Func<IServiceProvider, object> factory)
    => services.Add(new ServiceDescriptor(typeof(TService), factory, lifetime));

private static object GetBack<T>(IServiceProvider sp) where T : class
    => sp.GetService<Holder<T>>()!.Instance;

private static object CreateInstance(IServiceProvider serviceProvider, Type instanceType, object underlyingInstance, object[] otherParameters)
{
    var parameters = new object[1 + otherParameters.Length];
    parameters[0] = underlyingInstance;
    Array.Copy(otherParameters, 0, parameters, 1, otherParameters.Length);
    return ActivatorUtilities.CreateInstance(serviceProvider, instanceType, parameters);
}

private class Holder<T> : IDisposable
    where T : class
{
    public readonly object Instance;

    public Holder(object instance)
    {
        Instance = instance;
    }

    void IDisposable.Dispose()
    {
        (Instance as IDisposable)?.Dispose();
    }
}

#endregion

The post Add decorator or proxy to dependency injection covers this with more details.

Concluding words

All the extension methods above rely on forwarding the request to the GetService. As Andrew Lock noted:

The "service-locator style" GetService() invocation is generally best avoided where possible.
However, I feel it's definitely the preferable course of action in this case.

Also, some of the extension methods also rely on ActivatorUtilities.CreateInstance, for which there may some performance penalty too.

Anyway, I ran a little benchmark and it was inconclusive.

References & Useful Links

Leave a comment

Please note that we won't show your email to others, or use it for sending unwanted emails. We will only use it to render your Gravatar image and to validate you as a real person.