Add alias to depency injection

Sometimes you have a class that implement several interfaces and you would like to register each of those in the Microsoft.Extensions.DependencyInjection's IServiceCollection. Unfortunatelly, the service container will instanciate the class for once for each registered interface. The singleton is not really a singleton anymore.

[Fact]
public void Singleton_without_Alias_are_instanciated_for_each_interface()
{
    var services = new ServiceCollection();
    services.AddSingleton<IFirstInterface, Implementation>();
    services.AddSingleton<ISecondInterface, Implementation>();

    using (var serviceProvider = services.BuildServiceProvider())
    {
        var first = serviceProvider.GetService<IFirstInterface>();
        var second = serviceProvider.GetService<ISecondInterface>();

        Assert.False(ReferenceEquals(first, second));
    }
}

The following extension creates an alias that will resolve the previous declared implementation.

/// <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 != 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 an instance is always created for each resolution.");
    }
    else
    {
        Debug.Assert(descriptor.Lifetime != ServiceLifetime.Transient);

        // 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;
}

Now, the alias can be registered as such.

[Fact]
public void Singleton_alias_are_instanciated_once()
{
    var services = new ServiceCollection();
    services.AddSingleton<IFirstInterface, Implementation>();
    services.AddAlias<ISecondInterface, Implementation>();

    using (var serviceProvider = services.BuildServiceProvider())
    {
        var first = serviceProvider.GetService<IFirstInterface>();
        var second = serviceProvider.GetService<ISecondInterface>();

        Assert.True(ReferenceEquals(first, second));
    }
}

For IDisposable services, the only drawback of this technique is that the container consider it instantiated several instances and therefore, the Dispose will be called for each of them.

Nevertheless this should not be a problem as the Dispose method is recommended to be idempotent.

To help ensure that resources are always cleaned up appropriately, a Dispose method should be idempotent, such that it is callable multiple times without throwing an exception. Furthermore, subsequent invocations of Dispose should do nothing.

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.