Description
Background and motivation
A lot of services exist that do not support keyed DI - they use TryAddSingleton
etc internally, without any notion of keyed DI. Migrating those services to keyed-DI often requires the library-author to add additional keyed DI APIs for the purpose. This can also be a problem when we have a need for 2 instances of the same service to interact, such as decorator APIs - as discussed in this "extensions" topic
This is a proposal for an alternative API that works around this problem.
API Proposal
Consider an existing API of the form:
services.AddStackExchangeRedisCache(...);
This adds SE.Redis as an IDistributedCache
implementation on the default key. There is no current API to add SE.Redis as a keyed service, and the relevant type is not directly exposed, making it impossible to register as a keyed service manually.
Now consider something along the lines of:
services.WithServiceKey("some key").AddStackExchangeRedisCache(...);
or
services.WithServiceKey("some key", keyed => keyed.AddStackExchangeRedisCache(...));
The key point here is that a decorator implementation of IServiceCollection
is created which changes the Add
method to add the specified key on non-keyed services. The 2 alternative suggestions have different ways of expressing the lifetime of the decorator; arguably the second version is clearer as to "here's the bits that are keyed", but I don't care to die on any hills.
A rough proposed implementation of the idea is shown below and kind-of works. There is a complication, however: if we take the AddStackExchangeRedisCache
as an example, this may use multiple other services, and if done naively, they'd all end up "keyed". I genuinely don't know whether keyed services work transitively, i.e. if a service Foo
is keyed "some key", and requires sub-service Bar
without specifying a key, does it look for a keyed-service ("some key") Bar
and then fall back to the non-keyed service Bar
, or does it only look for the non-keyed Bar
? I wonder whether it might be necessary to say "I want to configure a specific type with a key", i.e.
- services.WithServiceKey("some key", keyed => keyed.AddStackExchangeRedisCache(...));
+ services.WithServiceKey<IDistributedCache>("some key", keyed => keyed.AddStackExchangeRedisCache(...));
In this case, the decorator would only magically add the key for matching service types (presumably with an API to allow multiple types to be specified if required)
Rough implementation, for reference only, doesn't consider the "which service" complication:
public static class ServiceCollectionExtensions
{
public static IServiceCollection WithServiceKey(this IServiceCollection services, object key)
=> new KeyedServiceCollection(services, key);
}
internal sealed class KeyedServiceCollection(IServiceCollection services, object serviceKey) : IServiceCollection
{
void ICollection<ServiceDescriptor>.Add(ServiceDescriptor item) => services.Add(WithKey(item));
private ServiceDescriptor WithKey(ServiceDescriptor item)
{
if (!item.IsKeyedService)
{
if (item.ImplementationInstance is not null)
{
item = new(item.ServiceType, serviceKey, item.ImplementationInstance);
}
else if (item.ImplementationFactory is { } factory)
{
item = new(item.ServiceType, serviceKey, (provider, _) => factory(provider), item.Lifetime);
}
else if (item.ImplementationType is not null)
{
item = new(item.ServiceType, serviceKey, item.ImplementationType, item.Lifetime);
}
else
{
throw new NotSupportedException("Unable to map service to key");
}
}
return item;
}
// other methods not shown, but simply forward via "services"
}
API Usage
(shown above)
Alternative Designs
The alternative is to lean on library authors to add keyed-DI methods to all of their registration methods.
Risks
No response