Modular extension class solution
Very late answer, but this is the way I do it, which has some advantages over some of the other solutions to this question.
Advantages:
- only 1 line of code per service implementation registration, no extra logic necessary in the registration method
- the keyed services do not need to all be registered at the same time and/or place. the registrations can even be done in different projects if that is what is needed, as long as the keys are unique. this allows new implementations to be added completely modularly.
- service instantiation is lazy (+ thread safe), so no unnecessary activation of all implementations when only one or a few are used.
- no dependency on any external delegate or type in your code, the service is injected as a plain
Func<TKey, TService>
by default, but it is easy to register a custom delegate or type if you prefer
- easy to choose between Transient, Singleton, or Scoped registration for the factory
- use any key type you like (I do strongly suggest you just use simple types with build-in efficient equality comparison like an
int
, string
, enum
, or bool
because why make life more complicated than it needs to be)
Configuration examples:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// default instantiation:
services.AddKeyedService<IService, ImplementationA, string>("A", ServiceLifetime.Scoped);
// using an implementation factory to pass a connection string to the constructor:
services.AddKeyedService<IService, ImplementationB, string>("B", x => {
var connectionString = ConfigurationManager.ConnectionStrings["mongo"].ConnectionString;
return new ImplementationB(connectionString);
}, ServiceLifetime.Scoped);
// using a custom delegate instead of Func<TKey, TService>
services.AddKeyedService<IService, ImplementationC, string, StringKeyedService>(
"C", (_, x) => new StringKeyedService(x), ServiceLifetime.Singleton);
return services.BuildServiceProvider();
}
public delegate IService StringKeyedService(string key);
Usage examples:
public ExampleClass(Func<string, IService> keyedServiceFactory, StringKeyedService<IService> keyedServiceDelegate)
{
var serviceKey = Configuration.GetValue<string>("IService.Key");
var service = keyedServiceFactory(serviceKey);
var serviceC = keyedServiceDelegate("C");
}
Implementation:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.DependencyInjection;
public static class KeyedServiceExtensions
{
// Use this to register TImplementation as TService, injectable as Func<TKey, TService>.
// Uses default instance activator.
public static IServiceCollection AddKeyedService<TService, TImplementation, TKey>(this IServiceCollection services, TKey key, ServiceLifetime serviceLifetime)
where TService : class
where TImplementation : class, TService
{
services.AddTransient<TImplementation>();
var keyedServiceBuilder = services.CreateOrUpdateKeyedServiceBuilder<TKey, TService, Func<TKey, TService>>(
DefaultImplementationFactory<TKey, TService>, serviceLifetime);
keyedServiceBuilder.Add<TImplementation>(key);
return services;
}
// Use this to register TImplementation as TService, injectable as Func<TKey, TService>.
// Uses implementationFactory to create instances
public static IServiceCollection AddKeyedService<TService, TImplementation, TKey>(this IServiceCollection services, TKey key,
Func<IServiceProvider, TImplementation> implementationFactory, ServiceLifetime serviceLifetime)
where TService : class
where TImplementation : class, TService
{
services.AddTransient(implementationFactory);
var keyedServiceBuilder = services.CreateOrUpdateKeyedServiceBuilder<TKey, TService, Func<TKey, TService>>(
DefaultImplementationFactory<TKey, TService>, serviceLifetime);
keyedServiceBuilder.Add<TImplementation>(key);
return services;
}
// Use this to register TImplementation as TService, injectable as TInjection.
// Uses default instance activator.
public static IServiceCollection AddKeyedService<TService, TImplementation, TKey, TInjection>(this IServiceCollection services, TKey key,
Func<IServiceProvider, Func<TKey, TService>, TInjection> serviceFactory, ServiceLifetime serviceLifetime)
where TService : class
where TImplementation : class, TService
where TInjection : class
{
services.AddTransient<TImplementation>();
var keyedServiceBuilder = services.CreateOrUpdateKeyedServiceBuilder<TKey, TService, TInjection>(
x => serviceFactory(x, DefaultImplementationFactory<TKey, TService>(x)), serviceLifetime);
keyedServiceBuilder.Add<TImplementation>(key);
return services;
}
// Use this to register TImplementation as TService, injectable as TInjection.
// Uses implementationFactory to create instances
public static IServiceCollection AddKeyedService<TService, TImplementation, TKey, TInjection>(this IServiceCollection services, TKey key,
Func<IServiceProvider, TImplementation> implementationFactory, Func<IServiceProvider, Func<TKey, TService>, TInjection> serviceFactory, ServiceLifetime serviceLifetime)
where TService : class
where TImplementation : class, TService
where TInjection : class
{
services.AddTransient(implementationFactory);
var keyedServiceBuilder = services.CreateOrUpdateKeyedServiceBuilder<TKey, TService, TInjection>(
x => serviceFactory(x, DefaultImplementationFactory<TKey, TService>(x)), serviceLifetime);
keyedServiceBuilder.Add<TImplementation>(key);
return services;
}
private static KeyedServiceBuilder<TKey, TService> CreateOrUpdateKeyedServiceBuilder<TKey, TService, TInjection>(this IServiceCollection services,
Func<IServiceProvider, TInjection> serviceFactory, ServiceLifetime serviceLifetime)
where TService : class
where TInjection : class
{
var builderServiceDescription = services.SingleOrDefault(x => x.ServiceType == typeof(KeyedServiceBuilder<TKey, TService>));
KeyedServiceBuilder<TKey, TService> keyedServiceBuilder;
if (builderServiceDescription is null)
{
keyedServiceBuilder = new KeyedServiceBuilder<TKey, TService>();
services.AddSingleton(keyedServiceBuilder);
switch (serviceLifetime)
{
case ServiceLifetime.Singleton:
services.AddSingleton(serviceFactory);
break;
case ServiceLifetime.Scoped:
services.AddScoped(serviceFactory);
break;
case ServiceLifetime.Transient:
services.AddTransient(serviceFactory);
break;
default:
throw new ArgumentOutOfRangeException(nameof(serviceLifetime), serviceLifetime, "Invalid value for " + nameof(serviceLifetime));
}
}
else
{
CheckLifetime<KeyedServiceBuilder<TKey, TService>>(builderServiceDescription.Lifetime, ServiceLifetime.Singleton);
var factoryServiceDescriptor = services.SingleOrDefault(x => x.ServiceType == typeof(TInjection));
CheckLifetime<TInjection>(factoryServiceDescriptor.Lifetime, serviceLifetime);
keyedServiceBuilder = (KeyedServiceBuilder<TKey, TService>)builderServiceDescription.ImplementationInstance;
}
return keyedServiceBuilder;
static void CheckLifetime<T>(ServiceLifetime actual, ServiceLifetime expected)
{
if (actual != expected)
throw new ApplicationException($"{typeof(T).FullName} is already registered with a different ServiceLifetime. Expected: '{expected}', Actual: '{actual}'");
}
}
private static Func<TKey, TService> DefaultImplementationFactory<TKey, TService>(IServiceProvider x) where TService : class
=> x.GetRequiredService<KeyedServiceBuilder<TKey, TService>>().Build(x);
private sealed class KeyedServiceBuilder<TKey, TService>
{
private readonly Dictionary<TKey, Type> _serviceImplementationTypes = new Dictionary<TKey, Type>();
internal void Add<TImplementation>(TKey key) where TImplementation : class, TService
{
if (_serviceImplementationTypes.TryGetValue(key, out var type) && type == typeof(TImplementation))
return; //this type is already registered under this key
_serviceImplementationTypes[key] = typeof(TImplementation);
}
internal Func<TKey, TService> Build(IServiceProvider serviceProvider)
{
var serviceTypeDictionary = _serviceImplementationTypes.Values.Distinct()
.ToDictionary(
type => type,
type => new Lazy<TService>(
() => (TService)serviceProvider.GetRequiredService(type),
LazyThreadSafetyMode.ExecutionAndPublication
)
);
var serviceDictionary = _serviceImplementationTypes
.ToDictionary(kvp => kvp.Key, kvp => serviceTypeDictionary[kvp.Value]);
return key => serviceDictionary[key].Value;
}
}
}
it's also possible to make a fluid interface on top of this, let me know if there is interest in that.
Example fluid usage:
var keyedService = services.KeyedSingleton<IService, ServiceKey>()
.As<ICustomKeyedService<TKey, IService>>((_, x) => new CustomKeyedServiceInterface<ServiceKey, IService>(x));
keyedService.Key(ServiceKey.A).Add<ServiceA>();
keyedService.Key(ServiceKey.B).Add(x => {
x.GetService<ILogger>.LogDebug("Instantiating ServiceB");
return new ServiceB();
});
Update1
be moved to a different question as injecting things in constructors is very different from working out which object to construct – Intreat