-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Description
Thanks @commonsensesoftware for the original proposal. I edited this post to show the current proposition.
Original proposal from @commonsensesoftware
### Background and MotivationI'm fairly certain this has been asked or proposed before. I did my due diligence, but I couldn't find an existing, similar issue. It may be lost to time from merging issues across repos over the years.
A similar question was asked in Issue 2937
The main reason this has not been supported is that IServiceProvider.GetService(Type type) does not afford a way to retrieve a service by key. IServiceProvider has been the staple interface for service location since .NET 1.0 and changing or ignoring its well-established place in history is a nonstarter. However... what if we could have our cake and eat it to? 🤔
A keyed service is a concept that comes up often in the IoC world. All, if not almost all, DI frameworks support registering and retrieving one or more services by a combination of type and key. There are ways to make keyed services work in the existing design, but they are clunky to use (ex: via Func<string, T>). The following proposal would add support for keyed services to the existing Microsoft.Extensions.DependencyInjection.* libraries without breaking the IServiceProvider contract nor requiring any container framework changes.
I currently have a small prototype that works with the default ServiceProvider, Autofac and Unity container.
Current proposal: https://gist.github.com/benjaminpetit/49a6b01692d0089b1d0d14558017efbc
Previous proposal
Overview
For completeness, a minimal, viable solution with E2E tests for the most common containers is available in the Keyed Service POC repo. It's probably incomplete from where the final solution would land, but it's enough to illustrate the feasibility of the approach.
API Proposal
The first requirement is to define a key for a service. Type is already a key. This proposal will use the novel idea of also using Type as a composite key. This design provides the following advantages:
- No magic strings or objects
- No attributes or other required metadata
- No hidden service location lookups (e.g. a la magic string)
- No name collisions (types are unique)
- No additional interfaces required for resolution (ex:
ISupportRequiredService,ISupportKeyedService) - No implementation changes to the existing containers
- No additional library references (from the FCL or otherwise)
- Resolution intuitively fails if a key and service combination does not exist in the container
The type names that follow are for illustration and might change if the proposal is accepted.
Resolving Services
To resolve a keyed dependency we'll define the following contracts:
// required to 'access' a keyed service via typeof(T)
public interface IDependency
{
object Value { get; }
}
public interface IDependency<in TKey, out TService> : IDependency
where TService : notnull
{
new TService Value { get; }
}The following extension methods will be added to ServiceProviderServiceExtensions:
public static class ServiceProviderServiceExtensions
{
public static object? GetService(this IServiceProvider serviceProvider, Type serviceType, Type key);
public static object GetRequiredService(this IServiceProvider serviceProvider, Type serviceType, Type key);
public static IEnumerable<object> GetServices(this IServiceProvider serviceProvider, Type serviceType, Type key);
public static T? GetService<T>(this IServiceProvider serviceProvider, Type key) where T : notnull;
public static T GetRequiredService<T>(this IServiceProvider serviceProvider, string key) where T : notnull;
public static IEnumerable<T> GetServices<T>(this IServiceProvider serviceProvider, string key) where T : notnull;
}Here is a partial example of how it would be implemented:
public static class ServiceProviderExtensions
{
public static object? GetService(this IServiceProvider serviceProvider, Type serviceType, Type key)
{
var keyedType = typeof(IDependency<,>).MakeGenericType(key, serviceType);
var dependency = (IDependency?)serviceProvider.GetService(keyedType);
return dependency?.Value;
}
public static TService? GetService<TKey, TService>(this IServiceProvider serviceProvider)
where TService : notnull
{
var dependency = serviceProvider.GetService<IDependency<TKey, TService>>();
return dependency is null ? default : dependency.Value;
}
public static IEnumerable<TService> GetServices<TKey, TService>(this IServiceProvider serviceProvider)
where TService : notnull
{
foreach (var dependency in serviceProvider.GetServices<IDependency<TKey, TService>>())
{
yield return dependency.Value;
}
}
}Registering Services
Now that we have a way to resolve a keyed service, how do we register one? Type is already used as a key, but we need a way to create an arbitrary composite key. To achieve this, we'll perform a little trickery on the Type which only affects how it is mapped in a container; thus making it a composite key. It does not change the runtime behavior nor require special Reflection magic. We are effectively taking advantage of the knowledge that Type will be used as a key for service resolution in all container implementations.
public static class KeyedType
{
public static Type Create(Type key, Type type) =>
new TypeWithKey(key,type);
public static Type Create<TKey, TType>() where TType : notnull =>
new TypeWithKey(typeof(TKey), typeof(TType));
private sealed class TypeWithKey : TypeDelegator
{
private readonly int hashCode;
public TypeWithKey(Type keyType, Type customType)
: base(customType) => hashCode = HashCode.Combine(typeImpl, keyType);
public override int GetHashCode() => hashCode;
// remainder is minimal, but ommitted for brevity
}
}This might look magical, but it's not. Type is already being used as a key when it's mapped in a container. TypeWithKey has all the appearance of the original type, but produces a different hash code when combined with another type. This affords for determinate, discrete unions of type registrations, which allows mapping the intended service multiple times.
Container implementers are free to perform the registration however they like, but the generic, out-of-the-box implementation would look like:
public sealed class Dependency<TKey, TService> : IDependency<TKey, TService>
where TService : notnull
{
private readonly IServiceProvider serviceProvider;
public Dependency(IServiceProvider serviceProvider) => this.serviceProvider = serviceProvider;
public TService Value => (TService)serviceProvider.GetRequiredService(KeyedType.Create<TKey, TService>());
object IDependency.Value => Value;
}Container implementers might provide their own extension methods to make registration more succinct, but it is not required. The following registrations would work today without any container implementation changes:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient(KeyedType.Create<Key.Thing1, IThing>(), typeof(Thing1));
services.AddTransient<IDependency<Key.Thing1, IThing>, Dependency<Key.Thing1, IThing>>();
}
public void ConfigureUnity(IUnityContainer container)
{
container.RegisterType(KeyedType.Create<Key.Thing1, IThing>(), typeof(Thing1));
container.RegisterType<IDependency<Key.Thing1, IThing>, Dependency<Key.Thing1, IThing>>();
}
public void ConfigureAutofac(ContainerBuilder builder)
{
builder.RegisterType(typeof(Thing1)).As(KeyedType.Create<Key.Thing1, IThing>());
builder.RegisterType<Dependency<Key.Thing1, IThing>>().As<IDependency<Key.Thing1, IThing>>();
}There is a minor drawback of requiring two registrations per keyed service in the container, but resolution for consumers is succintly:
var longForm = serviceProvider.GetRequiredService<IDependency<Key.Thing1, IThing>>().Value;
var shortForm = serviceProvider.GetRequiredService<Key.Thing1, IThing>();The following extension methods will be added to ServiceCollectionDescriptorExtensions to provide common registration through IServiceCollection for all container frameworks:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSingleton<TKey, TService, TImplementation>(this IServiceCollection services)
where TService : class
where TImplementation : class, TService;
public static IServiceCollection AddSingleton(
this IServiceCollection services,
Type keyType,
Type serviceType,
Type implementationType);
public static IServiceCollection TryAddSingleton<TKey, TService, TImplementation>(this IServiceCollection services)
where TService : class
where TImplementation : class, TService;
public static IServiceCollection TryAddSingleton(
this IServiceCollection services,
Type keyType,
Type serviceType,
Type implementationType);
public static IServiceCollection AddTransient<TKey, TService, TImplementation>(this IServiceCollection services)
where TService : class
where TImplementation : class, TService;
public static IServiceCollection AddTransient(
this IServiceCollection services,
Type keyType,
Type serviceType,
Type implementationType);
public static IServiceCollection TryAddTransient<TKey, TService, TImplementation>(this IServiceCollection services)
where TService : class
where TImplementation : class, TService;
public static IServiceCollection TryAddTransient(
this IServiceCollection services,
Type keyType,
Type serviceType,
Type implementationType);
public static IServiceCollection AddScoped<TKey, TService, TImplementation>(this IServiceCollection services)
where TService : class
where TImplementation : class, TService;
public static IServiceCollection AddScoped(
this IServiceCollection services,
Type keyType,
Type serviceType,
Type implementationType);
public static IServiceCollection TryAddScoped<TKey, TService, TImplementation>(this IServiceCollection services)
where TService : class
where TImplementation : class, TService;
public static IServiceCollection TryAddScoped(
this IServiceCollection services,
Type keyType,
Type serviceType,
Type implementationType);
public static IServiceCollection TryAddEnumerable<TKey, TService, TImplementation>(
this IServiceCollection services,
ServiceLifetime lifetime)
where TService : class
where TImplementation : class, TService;
public static IServiceCollection TryAddEnumerable(
this IServiceCollection services,
Type keyType,
Type serviceType,
Type implementationType,
ServiceLifetime lifetime);
}API Usage
Putting it all together, here's how the API can be leveraged for any container framework that supports registration through IServiceCollection.
public interface IThing
{
string ToString();
}
public abstract class ThingBase : IThing
{
protected ThingBase() { }
public override string ToString() => GetType().Name;
}
public sealed class Thing : ThingBase { }
public sealed class KeyedThing : ThingBase { }
public sealed class Thing1 : ThingBase { }
public sealed class Thing2 : ThingBase { }
public sealed class Thing3 : ThingBase { }
public static class Key
{
public sealed class Thingies { }
public sealed class Thing1 { }
public sealed class Thing2 { }
}
public class CatInTheHat
{
private readonly IDependency<Key.Thing1, IThing> thing1;
private readonly IDependency<Key.Thing2, IThing> thing2;
public CatInTheHat(
IDependency<Key.Thing1, IThing> thing1,
IDependency<Key.Thing2, IThing> thing2)
{
this.thing1 = thing1;
this.thing2 = thing2;
}
public IThing Thing1 => thing1.Value;
public IThing Thing2 => thing2.Value;
}
public void ConfigureServices(IServiceCollection collection)
{
// keyed types
services.AddSingleton<Key.Thing1, IThing, Thing1>();
services.AddTransient<Key.Thing2, IThing, Thing2>();
// non-keyed type with keyed type dependencies
services.AddSingleton<CatInTheHat>();
// keyed open generics
services.AddTransient(typeof(IGeneric<>), typeof(Generic<>));
services.AddSingleton(typeof(IDependency<,>), typeof(GenericDependency<,>));
// keyed IEnumerable<T>
services.TryAddEnumerable<Key.Thingies, IThing, Thing1>(ServiceLifetime.Transient);
services.TryAddEnumerable<Key.Thingies, IThing, Thing2>(ServiceLifetime.Transient);
services.TryAddEnumerable<Key.Thingies, IThing, Thing3>(ServiceLifetime.Transient);
var provider = services.BuildServiceProvider();
// resolve non-keyed type with keyed type dependencies
var catInTheHat = provider.GetRequiredService<CatInTheHat>();
// resolve keyed, open generic
var openGeneric = provider.GetRequiredService<Key.Thingy, IGeneric<object>>();
// resolve keyed IEnumerable<T>
var thingies = provider.GetServices<Key.Thingies, IThing>();
// related services such as IServiceProviderIsService
// new extension methods could be added to make this more succinct
var query = provider.GetRequiredService<IServiceProviderIsService>();
var thing1Registered = query.IsService(typeof(IDependency<Key.Thing1, IThing>));
var thing2Registered = query.IsService(typeof(IDependency<Key.Thing2, IThing>));
}Container Integration
The following is a summary of results from Keyed Service POC repo.
| Container | By Key | By Key (Generic) |
Many By Key |
Many By Key (Generic) |
Open Generics |
Existing Instance |
Implementation Factory |
|---|---|---|---|---|---|---|---|
| Default | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Autofac | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| DryIoc | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Grace | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Lamar | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| LightInject | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Stashbox | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| StructureMap | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Unity | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Container | Just Works |
No Container Changes |
No Adapter Changes |
|---|---|---|---|
| Default | ✅ | ✅ | ✅ |
| Autofac | ✅ | ✅ | ✅ |
| DryIoc | ❌ | ✅ | ❌ |
| Grace | ❌1 | ✅ | ❌1 |
| Lamar | ❌ | ✅ | ❌ |
| LightInject | ✅ | ✅ | ✅ |
| Stashbox | ❌ | ✅ | ❌ |
| StructureMap | ❌ | ✅ | ❌ |
| Unity | ✅ | ✅ | ✅ |
[1]: Only Implementation Factory doesn't work out-of-the-box
- Just Works: Works without any changes
- No Container Changes: Works without requiring fundamental container changes
- No Adapter Changes: Works without changing the way a container adapts to
IServiceCollection
Risks
- Container implementers may not be interested in adopting this approach
- Suboptimal experience for developers using containers that need adapter changes
- e.g. The feature doesn't work without a developer writing their own or relying on a 3rd party to bridge the gap
Alternate Proposals (TL;DR)
The remaining sections outline variations alternate designs that were rejected, but were retained for historical purposes.
Previous Code Iterations
Proposal 1 (Rejected)
Proposal 1 revolved around using string as a key. While this approach is feasible, it requires a lot of magical ceremony under the hood. For this solution to be truly effective, container implementers would have to opt into the new design. The main limitation of this approach, however, is that a string key is another form of hidden dependency that cannot, or cannot easily, be expressed to consumers. Resolution of a keyed dependency in this proposal would require an attribute at the call site that specifies the key or some type of lookup that resolves, but hides, the key used in the injected constructor. The comments below describes and highlights many of the issues with this design.
Keyed Services Using a String (KeyedServiceV1.zip)
API Proposal
The first thing we need is a way to provide a key for a service. The simplest way to do that is to add a new attribute to Microsoft.Extensions.DependencyInjection.Abstractions:
using static System.AttributeTargets;
[AttributeUsage(Class | Interface | Parameter, AllowMultiple = false, Inherited = false)]
public sealed class ServiceKeyAttribute : Attribute
{
public ServiceKeyAttribute(string key) => Key = key;
public string Key { get; }
}This attribute could be used in the following ways:
[ServiceKey("Bar")]
public interface IFoo { }
7
[ServiceKey("Foo")]
public class Foo { }
public class Bar
{
public Bar([ServiceKey("Bar")] IFoo foo) { }
}Using an attribute has to main advantages:
- There needs to be a way to specify the key at the call site when a dependency is injected
- An attribute can provide metadata (e.g. the key) to any type
What if we don't want to use an attribute on our class or interface? In fact, what if we can't apply an attribute to the target class or interface (because we don't control the source)? Using a little Bait & Switch, we can get around that limitation and achieve our goal using CustomReflectionContext. That will enable adding ServiceKeyAttribute to any arbitrary type. Moreover, the surrogate type doesn't change any runtime behavior; it is only used as a key in the container to lookup the corresponding resolver. This means that it's now possible to register a type more than once in combination with a key. The type is still the Type, but the key maps to different implementations. This also means that IServiceProvider.GetService(Type type) can support a key without breaking its contract.
The following extension methods would be added to ServiceProviderServiceExtensions:
public static class ServiceProviderServiceExtensions
{
public static object? GetService(this IServiceProvider serviceProvider, Type serviceType, string key);
public static IEnumerable<object> GetServices(this IServiceProvider serviceProvider, Type serviceType, string key);
public static T? GetService<T>(this IServiceProvider serviceProvider, string key);
public static object GetRequiredService(this IServiceProvider serviceProvider, Type serviceType, string key);
public static T GetRequiredService<T>(this IServiceProvider serviceProvider, string key) where T : notnull;
public static IEnumerable<T> GetServices<T>(this IServiceProvider serviceProvider, string key) where T : notnull;
}It is not required for this proposal to work, but as an optimization, it may be worth adding:
public interface IKeyedServiceProvider : IServiceProvider
{
object? GetService(Type serviceType, string key);
}for implementers that know how to deal with Type and key separately.
To abstract the container and mapping from the implementation, ServiceDescriptor will need to add the property:
public string? Key { get; set; }The aforementioned extension methods are static and cannot have their implementations changed in the future. To ensure that
container implementers have full control over how Type + key mappings are handled, I recommend the following be added
to Microsoft.Extensions.DependencyInjection.Abstractions:
public interface IKeyedTypeFactory
{
Type Create(Type type, string key);
}Microsoft.Extensions.DependencyInjection will provide a default implementation that leverages CustomReflectionContext.
The implementation might look like the following:
public static object? GetService(this IServiceProvider serviceProvider, Type serviceType, string key)
{
var provider = serviceProvider as IKeyedServiceProvider ?? serviceProvider.GetService<IKeyServiceProvider>();
if (provider != null)
{
return provider.GetService(serviceType, key);
}
var factory = serviceProvider.GetService<IKeyedTypeFactory>() ?? KeyedTypeFactory.Default;
return serviceProvider.GetService(factory.Create(serviceType, key));
}This approach would also work for new interfaces such as IServiceProviderIsService without requiring the
fundamental contract to change. It would make sense to add new extension methods for IServiceProviderIsService and potentially other interfaces as well.
API Usage
What we ultimately want to have is service registration that looks like:
class Team
{
public Team([ServiceKey("A-Team")] IPityTheFoo foo) { } // ← MrT is injected
}
// ...
var services = new ServiceCollection();
// Microsoft.Extensions.DependencyInjection.Abstractions
services.AddSingleton<IPityTheFoo, MrT>("A-Team");
services.TryAddEnumerable(ServiceDescriptor.AddTransient<IThing, Thing1>("Thingies"));
services.TryAddEnumerable(ServiceDescriptor.AddTransient<IThing, Thing2>("Thingies"));
services.TryAddEnumerable(ServiceDescriptor.AddTransient<IThing, Thing3>("Thingies"));
var provider = services.BuildServiceProvider();
var foo = provider.GetRequiredService<IPityTheFoo>("A-Team");
var team = provider.GetRequiredService<Team>();
var thingies = provider.GetServices<IThing>("Thingies");
// related services such as IServiceProviderIsService
var query = provider.GetRequiredService<IServiceProviderIsService>();
var shorthand = query.IsService<IPityTheFoo>("A-Team");
var factory = provider.GetRequiredService<IKeyedTypeService>();
var longhand = query.IsService(factory.Create<IPityTheFoo>("A-Team"));Alternative Designs
The ServiceKeyAttribute does not have to be applicable to classes or interfaces. That might make it easier to reason about without having to consider explicitly declared attributes and dynamically applied attributes. There still needs to be some attribute to apply to a parameter. Both scenarios can be achieved by restricting the value targets to AttributeTargets.Parameter. Dynamically adding the attribute does not have to abide by the same rules. A different attribute or method could also be used to map a key to the type.
This proposal does not mandate that CustomReflectionContext or even a custom attribute is the ideal solution. There may be other, more optimal ways to achieve it. IKeyedServiceProvider affords for optimization, while still ensuring that naive implementations will continue to work off of Type alone as input.
Risks
Microsoft.Extensions.DependencyInjectionwould require one of the following:- A dependency on
System.Reflection.Context(unless another solution is found) - An new, separate library that that references
System.Reflection.Contextand adds the keyed service capability
- A dependency on
- There is a potential explosion of overloads and/or extension methods
- The requirement that these exist can be mitigated via the
IKeyedServiceProviderand/orIKeyedTypeFactoryintefaces- The developer experience is less than ideal, but no functionality is lost
- The requirement that these exist can be mitigated via the
API Proposal
The API is optional
The API is optional, and will not break binary compatibility. If the service provider doesn't support the new methods, the user will get an exception at runtime.
The key type
The service key can be any object. It is important that Equals and GetHashCode have a proper implementation.
Service registration
ServiceDescriptor will be modified to include the ServiceKey. KeyedImplementationInstance, KeyedImplementationType and KeyedImplementationFactory will be added, matching their non-keyed equivalent.
When accessing a non-keyed property (like ImplementationInstance) on a keyed ServiceDescriptor will throw an exception: this way, if the developer added a keyed service and is using a non-compatible container, an error will be thrown during container build.
public class ServiceDescriptor
{
[...]
/// <summary>
/// Get the key of the service, if applicable.
/// </summary>
public object? ServiceKey { get; }
[...]
/// <summary>
/// Gets the instance that implements the service.
/// </summary>
public object? KeyedImplementationInstance { get; }
/// <summary>
/// Gets the <see cref="Type"/> that implements the service.
/// </summary>
public System.Type? KeyedImplementationType { get; }
/// <summary>
/// Gets the factory used for creating Keyed service instances.
/// </summary>
public Func<IServiceProvider, object, object>? KeyedImplementationFactory { get; }
[...]
/// <summary>
/// Returns true if a ServiceKey was provided.
/// </summary>
public bool IsKeyedService => ServiceKey != null;
}ServiceKey will stay null in non-keyed services.
Extension methods for IServiceCollection are added to support keyed services:
public static IServiceCollection AddKeyedScoped(this IServiceCollection services, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType, object serviceKey);
public static IServiceCollection AddKeyedScoped(this IServiceCollection services, Type serviceType, object serviceKey, Func<IServiceProvider, object, object> implementationFactory);
public static IServiceCollection AddKeyedScoped(this IServiceCollection services, Type serviceType, object serviceKey, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType);
public static IServiceCollection AddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection services, object serviceKey) where TService : class;
public static IServiceCollection AddKeyedScoped<TService>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class;
public static IServiceCollection AddKeyedScoped<TService, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection services, object serviceKey) where TService : class where TImplementation : class, TService;
public static IServiceCollection AddKeyedScoped<TService, TImplementation>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TImplementation> implementationFactory) where TService : class where TImplementation : class, TService;
public static IServiceCollection AddKeyedSingleton(this IServiceCollection services, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType, object serviceKey);
public static IServiceCollection AddKeyedSingleton(this IServiceCollection services, Type serviceType, object serviceKey, Func<IServiceProvider, object, object> implementationFactory);
public static IServiceCollection AddKeyedSingleton(this IServiceCollection services, Type serviceType, object serviceKey, object implementationInstance);
public static IServiceCollection AddKeyedSingleton(this IServiceCollection services, Type serviceType, object serviceKey, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType);
public static IServiceCollection AddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection services, object serviceKey) where TService : class;
public static IServiceCollection AddKeyedSingleton<TService>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class;
public static IServiceCollection AddKeyedSingleton<TService>(this IServiceCollection services, object serviceKey, TService implementationInstance) where TService : class;
public static IServiceCollection AddKeyedSingleton<TService, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection services, object serviceKey) where TService : class where TImplementation : class, TService;
public static IServiceCollection AddKeyedSingleton<TService, TImplementation>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TImplementation> implementationFactory) where TService : class where TImplementation : class, TService;
public static IServiceCollection AddKeyedTransient(this IServiceCollection services, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType, object serviceKey);
public static IServiceCollection AddKeyedTransient(this IServiceCollection services, Type serviceType, object serviceKey, Func<IServiceProvider, object, object> implementationFactory);
public static IServiceCollection AddKeyedTransient(this IServiceCollection services, Type serviceType, object serviceKey, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType);
public static IServiceCollection AddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection services, object serviceKey) where TService : class;
public static IServiceCollection AddKeyedTransient<TService>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class;
public static IServiceCollection AddKeyedTransient<TService, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection services, object serviceKey) where TService : class where TImplementation : class, TService;
public static IServiceCollection AddKeyedTransient<TService, TImplementation>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TImplementation> implementationFactory) where TService : class where TImplementation : class, TService;
public static void TryAddKeyedScoped(this IServiceCollection collection, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type service, object serviceKey) { }
public static void TryAddKeyedScoped(this IServiceCollection collection, Type service, object serviceKey, Func<IServiceProvider, object, object> implementationFactory) { }
public static void TryAddKeyedScoped(this IServiceCollection collection, Type service, object serviceKey, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType) { }
public static void TryAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection collection, object serviceKey) where TService : class { }
public static void TryAddKeyedScoped<TService>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class { }
public static void TryAddKeyedScoped<TService, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection collection, object serviceKey) where TService : class where TImplementation : class, TService { }
public static void TryAddKeyedSingleton(this IServiceCollection collection, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type service, object serviceKey) { }
public static void TryAddKeyedSingleton(this IServiceCollection collection, Type service, object serviceKey, Func<IServiceProvider, object, object> implementationFactory) { }
public static void TryAddKeyedSingleton(this IServiceCollection collection, Type service, object serviceKey, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType) { }
public static void TryAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection collection, object serviceKey) where TService : class { }
public static void TryAddKeyedSingleton<TService>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class { }
public static void TryAddKeyedSingleton<TService>(this IServiceCollection collection, object serviceKey, TService instance) where TService : class { }
public static void TryAddKeyedSingleton<TService, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection collection, object serviceKey) where TService : class where TImplementation : class, TService { }
public static void TryAddKeyedTransient(this IServiceCollection collection, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type service, object serviceKey) { }
public static void TryAddKeyedTransient(this IServiceCollection collection, Type service, object serviceKey, Func<IServiceProvider, object, object> implementationFactory) { }
public static void TryAddKeyedTransient(this IServiceCollection collection, Type service, object serviceKey, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType) { }
public static void TryAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection collection, object serviceKey) where TService : class { }
public static void TryAddKeyedTransient<TService>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class { }
public static void TryAddKeyedTransient<TService, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection collection, object serviceKey) where TService : class where TImplementation : class, TService { }
public static IServiceCollection RemoveAllKeyed(this IServiceCollection collection, Type serviceType, object serviceKey);
public static IServiceCollection RemoveAllKeyed<T>(this IServiceCollection collection, object serviceKey);I think it's important that all new methods supporting Keyed service have a different name from the non-keyed equivalent, to avoid ambiguity.
"Any key" registration
It is possible to register a "catch all" key with KeyedService.AnyKey:
serviceCollection.AddKeyedSingleton<IService>(KeyedService.AnyKey, defaultService);
serviceCollection.AddKeyedSingleton<IService>("other-service", otherService);
[...] // build the provider
s1 = provider.GetKeyedService<IService>("other-service"); // returns otherService
s1 = provider.GetKeyedService<IService>("another-random-key"); // returns defaultServiceResolving service
Basic keyed resolution
Two new optional interfaces will be introduced:
namespace Microsoft.Extensions.DependencyInjection;
public interface ISupportKeyedService
{
object? GetKeyedService(Type serviceType, object serviceKey);
object GetRequiredKeyedService(Type serviceType, object serviceKey);
}
public interface IServiceProviderIsServiceKeyed
{
bool IsService(Type serviceType, object serviceKey);
}This new interface will be accessible via the following extension methods:
public static IEnumerable<object?> GetKeyedServices(this IServiceProvider provider, Type serviceType, object serviceKey);
public static IEnumerable<T> GetKeyedServices<T>(this IServiceProvider provider, object serviceKey);
public static T? GetKeyedService<T>(this IServiceProvider provider, object serviceKey);
public static object GetRequiredKeyedService(this IServiceProvider provider, Type serviceType, object serviceKey);
public static T GetRequiredKeyedService<T>(this IServiceProvider provider, object serviceKey) where T : notnull;
}These methods will throw an InvalidOperationException if the provider doesn't support ISupportKeyedService.
Resolving services via attributes
We introduce two attributes: ServiceKeyAttribute and FromKeyedServicesAttribute.
ServiceKeyAttribute
ServiceKeyAttribute is used to inject the key that was used for registration/resolution in the constructor:
namespace Microsoft.Extensions.DependencyInjection;
[AttributeUsageAttribute(AttributeTargets.Parameter)]
public class ServiceKeyAttribute : Attribute
{
public ServiceKeyAttribute() { }
}
class Service
{
private readonly string _id;
public Service([ServiceKey] string id) => _id = id;
}
serviceCollection.AddKeyedSingleton<Service>("some-service");
[...] // build the provider
var service = provider.GetKeyedService<Service>("some-service"); // service._id will be set to "some-service"This attribute can be very useful when registering a service with KeyedService.AnyKey.
FromKeyedServicesAttribute
This attribute is used in a service constructor to mark parameters speficying which keyed service should be used:
namespace Microsoft.Extensions.DependencyInjection;
[AttributeUsageAttribute(AttributeTargets.Parameter)]
public class FromKeyedServicesAttribute : Attribute
{
public FromKeyedServicesAttribute(object key) { }
public object Key { get; }
}
class OtherService
{
public OtherService(
[FromKeyedServices("service1")] IService service1,
[FromKeyedServices("service2")] IService service2)
{
Service1 = service1;
Service2 = service2;
}
}Open generics
Open generics are supported:
serviceCollection.AddTransient(typeof(IGenericInterface<>), "my-service", typeof(GenericService<>));
[...] // build the provider
var service = provider.GetKeyedService<IGenericInterface<SomeType>("my-service")Enumeration
This kind of enumeration is possible:
serviceCollection.AddKeyedSingleton<IMyService, MyServiceA>("some-service");
serviceCollection.AddKeyedSingleton<IMyService, MyServiceB>("some-service");
serviceCollection.AddKeyedSingleton<IMyService, MyServiceC>("some-service");
[...] // build the provider
services = provider.GetKeyedServices<IMyService>("some-service"); // returns an instance of MyServiceA, MyServiceB and MyServiceCNote that enumeration will not mix keyed and non keyed registrations:
serviceCollection.AddKeyedSingleton<IMyService, MyServiceA>("some-service");
serviceCollection.AddKeyedSingleton<IMyService, MyServiceB>("some-service");
serviceCollection.AddSingleton<IMyService, MyServiceC>();
[...] // build the provider
keyedServices = provider.GetKeyedServices<IMyService>("some-service"); // returns an instance of MyServiceA, MyServiceB but NOT MyServiceC
services = provider.GetServices<IMyService>(); // only returns MyServiceCBut we do not support:
serviceCollection.AddKeyedSingleton<MyServiceA>("some-service");
serviceCollection.AddKeyedSingleton<MyServiceB>("some-service");
serviceCollection.AddKeyedSingleton<MyServiceC>("some-service");
[...] // build the provider
services = provider.GetKeyedServices("some-service"); // Not supported