[API Proposal]: Add Keyed Services Support to Dependency Injection #64427

Description

@commonsensesoftware

Thanks @commonsensesoftware for the original proposal. I edited this post to show the current proposition.

Original proposal from @commonsensesoftware ### Background and Motivation

I'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)publicinterfaceIDependency{objectValue{get;}}publicinterfaceIDependency<inTKey,outTService>:IDependencywhereTService:notnull{newTServiceValue{get;}}

The following extension methods will be added to ServiceProviderServiceExtensions:

publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,Typekey)whereT:notnull;publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

Here is a partial example of how it would be implemented:

publicstaticclassServiceProviderExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey){varkeyedType=typeof(IDependency<,>).MakeGenericType(key,serviceType);vardependency=(IDependency?)serviceProvider.GetService(keyedType);returndependency?.Value;}publicstaticTService?GetService<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{vardependency=serviceProvider.GetService<IDependency<TKey,TService>>();returndependencyisnull?default:dependency.Value;}publicstaticIEnumerable<TService>GetServices<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{foreach(vardependencyinserviceProvider.GetServices<IDependency<TKey,TService>>()){yieldreturndependency.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.

publicstaticclassKeyedType{publicstaticTypeCreate(Typekey,Typetype)=>newTypeWithKey(key,type);publicstaticTypeCreate<TKey,TType>()whereTType:notnull=>newTypeWithKey(typeof(TKey),typeof(TType));privatesealedclassTypeWithKey:TypeDelegator{privatereadonlyinthashCode;publicTypeWithKey(TypekeyType,TypecustomType):base(customType)=>hashCode=HashCode.Combine(typeImpl,keyType);publicoverrideintGetHashCode()=>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:

publicsealedclassDependency<TKey,TService>:IDependency<TKey,TService>whereTService:notnull{privatereadonlyIServiceProviderserviceProvider;publicDependency(IServiceProviderserviceProvider)=>this.serviceProvider=serviceProvider;publicTServiceValue=>(TService)serviceProvider.GetRequiredService(KeyedType.Create<TKey,TService>());objectIDependency.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:

publicvoidConfigureServices(IServiceCollectionservices){services.AddTransient(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));services.AddTransient<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureUnity(IUnityContainercontainer){container.RegisterType(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));container.RegisterType<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureAutofac(ContainerBuilderbuilder){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:

varlongForm=serviceProvider.GetRequiredService<IDependency<Key.Thing1,IThing>>().Value;varshortForm=serviceProvider.GetRequiredService<Key.Thing1,IThing>();

The following extension methods will be added to ServiceCollectionDescriptorExtensions to provide common registration through IServiceCollection for all container frameworks:

publicstaticclassServiceCollectionExtensions{publicstaticIServiceCollectionAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddEnumerable<TKey,TService,TImplementation>(thisIServiceCollectionservices,ServiceLifetimelifetime)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddEnumerable(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType,ServiceLifetimelifetime);}

API Usage

Putting it all together, here's how the API can be leveraged for any container framework that supports registration through IServiceCollection.

publicinterfaceIThing{stringToString();}publicabstractclassThingBase:IThing{protectedThingBase(){}publicoverridestringToString()=>GetType().Name;}publicsealedclassThing:ThingBase{}publicsealedclassKeyedThing:ThingBase{}publicsealedclassThing1:ThingBase{}publicsealedclassThing2:ThingBase{}publicsealedclassThing3:ThingBase{}publicstaticclassKey{publicsealedclassThingies{}publicsealedclassThing1{}publicsealedclassThing2{}}publicclassCatInTheHat{privatereadonlyIDependency<Key.Thing1,IThing>thing1;privatereadonlyIDependency<Key.Thing2,IThing>thing2;publicCatInTheHat(IDependency<Key.Thing1,IThing>thing1,IDependency<Key.Thing2,IThing>thing2){this.thing1=thing1;this.thing2=thing2;}publicIThingThing1=>thing1.Value;publicIThingThing2=>thing2.Value;}publicvoidConfigureServices(IServiceCollectioncollection){// keyed typesservices.AddSingleton<Key.Thing1,IThing,Thing1>();services.AddTransient<Key.Thing2,IThing,Thing2>();// non-keyed type with keyed type dependenciesservices.AddSingleton<CatInTheHat>();// keyed open genericsservices.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);varprovider=services.BuildServiceProvider();// resolve non-keyed type with keyed type dependenciesvarcatInTheHat=provider.GetRequiredService<CatInTheHat>();// resolve keyed, open genericvaropenGeneric=provider.GetRequiredService<Key.Thingy,IGeneric<object>>();// resolve keyed IEnumerable<T>varthingies=provider.GetServices<Key.Thingies,IThing>();// related services such as IServiceProviderIsService// new extension methods could be added to make this more succinctvarquery=provider.GetRequiredService<IServiceProviderIsService>();varthing1Registered=query.IsService(typeof(IDependency<Key.Thing1,IThing>));varthing2Registered=query.IsService(typeof(IDependency<Key.Thing2,IThing>));}

Container Integration

The following is a summary of results from Keyed Service POC repo.

ContainerBy KeyBy Key
(Generic)
Many
By Key
Many By
Key (Generic)
Open
Generics
Existing
Instance
Implementation
Factory
Default
Autofac
DryIoc
Grace
Lamar
LightInject
Stashbox
StructureMap
Unity
ContainerJust
Works
No Container
Changes
No Adapter
Changes
Default
Autofac
DryIoc
Grace11
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

  1. Thought experiment
  2. Initial proof of concept
  3. Practical API with a lot of ceremony removed

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:

usingstaticSystem.AttributeTargets;[AttributeUsage(Class|Interface|Parameter,AllowMultiple=false,Inherited=false)]publicsealedclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(stringkey)=>Key=key;publicstringKey{get;}}

This attribute could be used in the following ways:

[ServiceKey("Bar")]publicinterfaceIFoo{}7[ServiceKey("Foo")]publicclassFoo{}publicclassBar{publicBar([ServiceKey("Bar")]IFoofoo){}}

Using an attribute has to main advantages:

  1. There needs to be a way to specify the key at the call site when a dependency is injected
  2. 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:

publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,stringkey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

It is not required for this proposal to work, but as an optimization, it may be worth adding:

publicinterfaceIKeyedServiceProvider:IServiceProvider{object?GetService(TypeserviceType,stringkey);}

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:

publicstring?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:

publicinterfaceIKeyedTypeFactory{TypeCreate(Typetype,stringkey);}

Microsoft.Extensions.DependencyInjection will provide a default implementation that leverages CustomReflectionContext.

The implementation might look like the following:

publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey){varprovider=serviceProviderasIKeyedServiceProvider??serviceProvider.GetService<IKeyServiceProvider>();if(provider!=null){returnprovider.GetService(serviceType,key);}varfactory=serviceProvider.GetService<IKeyedTypeFactory>()??KeyedTypeFactory.Default;returnserviceProvider.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:

classTeam{publicTeam([ServiceKey("A-Team")]IPityTheFoofoo){}// ← MrT is injected}// ...varservices=newServiceCollection();// Microsoft.Extensions.DependencyInjection.Abstractionsservices.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"));varprovider=services.BuildServiceProvider();varfoo=provider.GetRequiredService<IPityTheFoo>("A-Team");varteam=provider.GetRequiredService<Team>();varthingies=provider.GetServices<IThing>("Thingies");// related services such as IServiceProviderIsServicevarquery=provider.GetRequiredService<IServiceProviderIsService>();varshorthand=query.IsService<IPityTheFoo>("A-Team");varfactory=provider.GetRequiredService<IKeyedTypeService>();varlonghand=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.DependencyInjection would require one of the following:
    1. A dependency on System.Reflection.Context (unless another solution is found)
    2. An new, separate library that that references System.Reflection.Context and adds the keyed service capability
  • There is a potential explosion of overloads and/or extension methods
    • The requirement that these exist can be mitigated via the IKeyedServiceProvider and/or IKeyedTypeFactory intefaces
      • The developer experience is less than ideal, but no functionality is lost

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.

publicclassServiceDescriptor{[...]/// <summary>/// Get the key of the service, if applicable./// </summary>publicobject?ServiceKey{get;}[...]/// <summary>/// Gets the instance that implements the service./// </summary>publicobject?KeyedImplementationInstance{get;}/// <summary>/// Gets the <see cref="Type"/> that implements the service./// </summary>publicSystem.Type?KeyedImplementationType{get;}/// <summary>/// Gets the factory used for creating Keyed service instances./// </summary>publicFunc<IServiceProvider,object,object>?KeyedImplementationFactory{get;}[...]/// <summary>/// Returns true if a ServiceKey was provided./// </summary> publicboolIsKeyedService=>ServiceKey!=null;}

ServiceKey will stay null in non-keyed services.

Extension methods for IServiceCollection are added to support keyed services:

publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedScoped<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,objectimplementationInstance);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,TServiceimplementationInstance)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectioncollection,objectserviceKey,TServiceinstance)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticIServiceCollectionRemoveAllKeyed(thisIServiceCollectioncollection,TypeserviceType,objectserviceKey);publicstaticIServiceCollectionRemoveAllKeyed<T>(thisIServiceCollectioncollection,objectserviceKey);

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 providers1=provider.GetKeyedService<IService>("other-service");// returns otherServices1=provider.GetKeyedService<IService>("another-random-key");// returns defaultService

Resolving service

Basic keyed resolution

Two new optional interfaces will be introduced:

namespaceMicrosoft.Extensions.DependencyInjection;publicinterfaceISupportKeyedService{object?GetKeyedService(TypeserviceType,objectserviceKey);objectGetRequiredKeyedService(TypeserviceType,objectserviceKey);}publicinterfaceIServiceProviderIsServiceKeyed{boolIsService(TypeserviceType,objectserviceKey);}

This new interface will be accessible via the following extension methods:

publicstaticIEnumerable<object?>GetKeyedServices(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticIEnumerable<T>GetKeyedServices<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticT?GetKeyedService<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticobjectGetRequiredKeyedService(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticTGetRequiredKeyedService<T>(thisIServiceProviderprovider,objectserviceKey)whereT: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:

namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(){}}classService{privatereadonlystring_id;publicService([ServiceKey]stringid)=>_id=id;}serviceCollection.AddKeyedSingleton<Service>("some-service");[...]// build the providervar 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:

namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassFromKeyedServicesAttribute:Attribute{publicFromKeyedServicesAttribute(objectkey){}publicobjectKey{get;}}classOtherService{publicOtherService([FromKeyedServices("service1")]IServiceservice1,[FromKeyedServices("service2")]IServiceservice2){Service1=service1;Service2=service2;}}

Open generics

Open generics are supported:

serviceCollection.AddTransient(typeof(IGenericInterface<>),"my-service",typeof(GenericService<>));[...]// build the providervar 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 providerservices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB and MyServiceC

Note 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 providerkeyedServices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB but NOT MyServiceCservices=provider.GetServices<IMyService>();// only returns MyServiceC

But we do not support:

serviceCollection.AddKeyedSingleton<MyServiceA>("some-service");serviceCollection.AddKeyedSingleton<MyServiceB>("some-service");serviceCollection.AddKeyedSingleton<MyServiceC>("some-service");[...]// build the providerservices=provider.GetKeyedServices("some-service");// Not supported

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implementedarea-Extensions-DependencyInjectionblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    [API Proposal]: Add Keyed Services Support to Dependency Injection #64427

    Description

    @commonsensesoftware

    Thanks @commonsensesoftware for the original proposal. I edited this post to show the current proposition.

    Original proposal from @commonsensesoftware ### Background and Motivation

    I'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)publicinterfaceIDependency{objectValue{get;}}publicinterfaceIDependency<inTKey,outTService>:IDependencywhereTService:notnull{newTServiceValue{get;}}

    The following extension methods will be added to ServiceProviderServiceExtensions:

    publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,Typekey)whereT:notnull;publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

    Here is a partial example of how it would be implemented:

    publicstaticclassServiceProviderExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey){varkeyedType=typeof(IDependency<,>).MakeGenericType(key,serviceType);vardependency=(IDependency?)serviceProvider.GetService(keyedType);returndependency?.Value;}publicstaticTService?GetService<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{vardependency=serviceProvider.GetService<IDependency<TKey,TService>>();returndependencyisnull?default:dependency.Value;}publicstaticIEnumerable<TService>GetServices<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{foreach(vardependencyinserviceProvider.GetServices<IDependency<TKey,TService>>()){yieldreturndependency.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.

    publicstaticclassKeyedType{publicstaticTypeCreate(Typekey,Typetype)=>newTypeWithKey(key,type);publicstaticTypeCreate<TKey,TType>()whereTType:notnull=>newTypeWithKey(typeof(TKey),typeof(TType));privatesealedclassTypeWithKey:TypeDelegator{privatereadonlyinthashCode;publicTypeWithKey(TypekeyType,TypecustomType):base(customType)=>hashCode=HashCode.Combine(typeImpl,keyType);publicoverrideintGetHashCode()=>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:

    publicsealedclassDependency<TKey,TService>:IDependency<TKey,TService>whereTService:notnull{privatereadonlyIServiceProviderserviceProvider;publicDependency(IServiceProviderserviceProvider)=>this.serviceProvider=serviceProvider;publicTServiceValue=>(TService)serviceProvider.GetRequiredService(KeyedType.Create<TKey,TService>());objectIDependency.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:

    publicvoidConfigureServices(IServiceCollectionservices){services.AddTransient(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));services.AddTransient<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureUnity(IUnityContainercontainer){container.RegisterType(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));container.RegisterType<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureAutofac(ContainerBuilderbuilder){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:

    varlongForm=serviceProvider.GetRequiredService<IDependency<Key.Thing1,IThing>>().Value;varshortForm=serviceProvider.GetRequiredService<Key.Thing1,IThing>();

    The following extension methods will be added to ServiceCollectionDescriptorExtensions to provide common registration through IServiceCollection for all container frameworks:

    publicstaticclassServiceCollectionExtensions{publicstaticIServiceCollectionAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddEnumerable<TKey,TService,TImplementation>(thisIServiceCollectionservices,ServiceLifetimelifetime)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddEnumerable(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType,ServiceLifetimelifetime);}

    API Usage

    Putting it all together, here's how the API can be leveraged for any container framework that supports registration through IServiceCollection.

    publicinterfaceIThing{stringToString();}publicabstractclassThingBase:IThing{protectedThingBase(){}publicoverridestringToString()=>GetType().Name;}publicsealedclassThing:ThingBase{}publicsealedclassKeyedThing:ThingBase{}publicsealedclassThing1:ThingBase{}publicsealedclassThing2:ThingBase{}publicsealedclassThing3:ThingBase{}publicstaticclassKey{publicsealedclassThingies{}publicsealedclassThing1{}publicsealedclassThing2{}}publicclassCatInTheHat{privatereadonlyIDependency<Key.Thing1,IThing>thing1;privatereadonlyIDependency<Key.Thing2,IThing>thing2;publicCatInTheHat(IDependency<Key.Thing1,IThing>thing1,IDependency<Key.Thing2,IThing>thing2){this.thing1=thing1;this.thing2=thing2;}publicIThingThing1=>thing1.Value;publicIThingThing2=>thing2.Value;}publicvoidConfigureServices(IServiceCollectioncollection){// keyed typesservices.AddSingleton<Key.Thing1,IThing,Thing1>();services.AddTransient<Key.Thing2,IThing,Thing2>();// non-keyed type with keyed type dependenciesservices.AddSingleton<CatInTheHat>();// keyed open genericsservices.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);varprovider=services.BuildServiceProvider();// resolve non-keyed type with keyed type dependenciesvarcatInTheHat=provider.GetRequiredService<CatInTheHat>();// resolve keyed, open genericvaropenGeneric=provider.GetRequiredService<Key.Thingy,IGeneric<object>>();// resolve keyed IEnumerable<T>varthingies=provider.GetServices<Key.Thingies,IThing>();// related services such as IServiceProviderIsService// new extension methods could be added to make this more succinctvarquery=provider.GetRequiredService<IServiceProviderIsService>();varthing1Registered=query.IsService(typeof(IDependency<Key.Thing1,IThing>));varthing2Registered=query.IsService(typeof(IDependency<Key.Thing2,IThing>));}

    Container Integration

    The following is a summary of results from Keyed Service POC repo.

    ContainerBy KeyBy Key
    (Generic)
    Many
    By Key
    Many By
    Key (Generic)
    Open
    Generics
    Existing
    Instance
    Implementation
    Factory
    Default
    Autofac
    DryIoc
    Grace
    Lamar
    LightInject
    Stashbox
    StructureMap
    Unity
    ContainerJust
    Works
    No Container
    Changes
    No Adapter
    Changes
    Default
    Autofac
    DryIoc
    Grace11
    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

    1. Thought experiment
    2. Initial proof of concept
    3. Practical API with a lot of ceremony removed

    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:

    usingstaticSystem.AttributeTargets;[AttributeUsage(Class|Interface|Parameter,AllowMultiple=false,Inherited=false)]publicsealedclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(stringkey)=>Key=key;publicstringKey{get;}}

    This attribute could be used in the following ways:

    [ServiceKey("Bar")]publicinterfaceIFoo{}7[ServiceKey("Foo")]publicclassFoo{}publicclassBar{publicBar([ServiceKey("Bar")]IFoofoo){}}

    Using an attribute has to main advantages:

    1. There needs to be a way to specify the key at the call site when a dependency is injected
    2. 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:

    publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,stringkey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

    It is not required for this proposal to work, but as an optimization, it may be worth adding:

    publicinterfaceIKeyedServiceProvider:IServiceProvider{object?GetService(TypeserviceType,stringkey);}

    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:

    publicstring?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:

    publicinterfaceIKeyedTypeFactory{TypeCreate(Typetype,stringkey);}

    Microsoft.Extensions.DependencyInjection will provide a default implementation that leverages CustomReflectionContext.

    The implementation might look like the following:

    publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey){varprovider=serviceProviderasIKeyedServiceProvider??serviceProvider.GetService<IKeyServiceProvider>();if(provider!=null){returnprovider.GetService(serviceType,key);}varfactory=serviceProvider.GetService<IKeyedTypeFactory>()??KeyedTypeFactory.Default;returnserviceProvider.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:

    classTeam{publicTeam([ServiceKey("A-Team")]IPityTheFoofoo){}// ← MrT is injected}// ...varservices=newServiceCollection();// Microsoft.Extensions.DependencyInjection.Abstractionsservices.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"));varprovider=services.BuildServiceProvider();varfoo=provider.GetRequiredService<IPityTheFoo>("A-Team");varteam=provider.GetRequiredService<Team>();varthingies=provider.GetServices<IThing>("Thingies");// related services such as IServiceProviderIsServicevarquery=provider.GetRequiredService<IServiceProviderIsService>();varshorthand=query.IsService<IPityTheFoo>("A-Team");varfactory=provider.GetRequiredService<IKeyedTypeService>();varlonghand=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.DependencyInjection would require one of the following:
      1. A dependency on System.Reflection.Context (unless another solution is found)
      2. An new, separate library that that references System.Reflection.Context and adds the keyed service capability
    • There is a potential explosion of overloads and/or extension methods
      • The requirement that these exist can be mitigated via the IKeyedServiceProvider and/or IKeyedTypeFactory intefaces
        • The developer experience is less than ideal, but no functionality is lost

    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.

    publicclassServiceDescriptor{[...]/// <summary>/// Get the key of the service, if applicable./// </summary>publicobject?ServiceKey{get;}[...]/// <summary>/// Gets the instance that implements the service./// </summary>publicobject?KeyedImplementationInstance{get;}/// <summary>/// Gets the <see cref="Type"/> that implements the service./// </summary>publicSystem.Type?KeyedImplementationType{get;}/// <summary>/// Gets the factory used for creating Keyed service instances./// </summary>publicFunc<IServiceProvider,object,object>?KeyedImplementationFactory{get;}[...]/// <summary>/// Returns true if a ServiceKey was provided./// </summary> publicboolIsKeyedService=>ServiceKey!=null;}

    ServiceKey will stay null in non-keyed services.

    Extension methods for IServiceCollection are added to support keyed services:

    publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedScoped<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,objectimplementationInstance);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,TServiceimplementationInstance)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectioncollection,objectserviceKey,TServiceinstance)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticIServiceCollectionRemoveAllKeyed(thisIServiceCollectioncollection,TypeserviceType,objectserviceKey);publicstaticIServiceCollectionRemoveAllKeyed<T>(thisIServiceCollectioncollection,objectserviceKey);

    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 providers1=provider.GetKeyedService<IService>("other-service");// returns otherServices1=provider.GetKeyedService<IService>("another-random-key");// returns defaultService

    Resolving service

    Basic keyed resolution

    Two new optional interfaces will be introduced:

    namespaceMicrosoft.Extensions.DependencyInjection;publicinterfaceISupportKeyedService{object?GetKeyedService(TypeserviceType,objectserviceKey);objectGetRequiredKeyedService(TypeserviceType,objectserviceKey);}publicinterfaceIServiceProviderIsServiceKeyed{boolIsService(TypeserviceType,objectserviceKey);}

    This new interface will be accessible via the following extension methods:

    publicstaticIEnumerable<object?>GetKeyedServices(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticIEnumerable<T>GetKeyedServices<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticT?GetKeyedService<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticobjectGetRequiredKeyedService(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticTGetRequiredKeyedService<T>(thisIServiceProviderprovider,objectserviceKey)whereT: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:

    namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(){}}classService{privatereadonlystring_id;publicService([ServiceKey]stringid)=>_id=id;}serviceCollection.AddKeyedSingleton<Service>("some-service");[...]// build the providervar 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:

    namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassFromKeyedServicesAttribute:Attribute{publicFromKeyedServicesAttribute(objectkey){}publicobjectKey{get;}}classOtherService{publicOtherService([FromKeyedServices("service1")]IServiceservice1,[FromKeyedServices("service2")]IServiceservice2){Service1=service1;Service2=service2;}}

    Open generics

    Open generics are supported:

    serviceCollection.AddTransient(typeof(IGenericInterface<>),"my-service",typeof(GenericService<>));[...]// build the providervar 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 providerservices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB and MyServiceC

    Note 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 providerkeyedServices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB but NOT MyServiceCservices=provider.GetServices<IMyService>();// only returns MyServiceC

    But we do not support:

    serviceCollection.AddKeyedSingleton<MyServiceA>("some-service");serviceCollection.AddKeyedSingleton<MyServiceB>("some-service");serviceCollection.AddKeyedSingleton<MyServiceC>("some-service");[...]// build the providerservices=provider.GetKeyedServices("some-service");// Not supported

    Metadata

    Metadata

    Assignees

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-Extensions-DependencyInjectionblockingMarks issues that we want to fast track in order to unblock other important work

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      [API Proposal]: Add Keyed Services Support to Dependency Injection #64427

      Description

      @commonsensesoftware

      Thanks @commonsensesoftware for the original proposal. I edited this post to show the current proposition.

      Original proposal from @commonsensesoftware ### Background and Motivation

      I'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)publicinterfaceIDependency{objectValue{get;}}publicinterfaceIDependency<inTKey,outTService>:IDependencywhereTService:notnull{newTServiceValue{get;}}

      The following extension methods will be added to ServiceProviderServiceExtensions:

      publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,Typekey)whereT:notnull;publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

      Here is a partial example of how it would be implemented:

      publicstaticclassServiceProviderExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey){varkeyedType=typeof(IDependency<,>).MakeGenericType(key,serviceType);vardependency=(IDependency?)serviceProvider.GetService(keyedType);returndependency?.Value;}publicstaticTService?GetService<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{vardependency=serviceProvider.GetService<IDependency<TKey,TService>>();returndependencyisnull?default:dependency.Value;}publicstaticIEnumerable<TService>GetServices<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{foreach(vardependencyinserviceProvider.GetServices<IDependency<TKey,TService>>()){yieldreturndependency.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.

      publicstaticclassKeyedType{publicstaticTypeCreate(Typekey,Typetype)=>newTypeWithKey(key,type);publicstaticTypeCreate<TKey,TType>()whereTType:notnull=>newTypeWithKey(typeof(TKey),typeof(TType));privatesealedclassTypeWithKey:TypeDelegator{privatereadonlyinthashCode;publicTypeWithKey(TypekeyType,TypecustomType):base(customType)=>hashCode=HashCode.Combine(typeImpl,keyType);publicoverrideintGetHashCode()=>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:

      publicsealedclassDependency<TKey,TService>:IDependency<TKey,TService>whereTService:notnull{privatereadonlyIServiceProviderserviceProvider;publicDependency(IServiceProviderserviceProvider)=>this.serviceProvider=serviceProvider;publicTServiceValue=>(TService)serviceProvider.GetRequiredService(KeyedType.Create<TKey,TService>());objectIDependency.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:

      publicvoidConfigureServices(IServiceCollectionservices){services.AddTransient(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));services.AddTransient<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureUnity(IUnityContainercontainer){container.RegisterType(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));container.RegisterType<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureAutofac(ContainerBuilderbuilder){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:

      varlongForm=serviceProvider.GetRequiredService<IDependency<Key.Thing1,IThing>>().Value;varshortForm=serviceProvider.GetRequiredService<Key.Thing1,IThing>();

      The following extension methods will be added to ServiceCollectionDescriptorExtensions to provide common registration through IServiceCollection for all container frameworks:

      publicstaticclassServiceCollectionExtensions{publicstaticIServiceCollectionAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddEnumerable<TKey,TService,TImplementation>(thisIServiceCollectionservices,ServiceLifetimelifetime)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddEnumerable(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType,ServiceLifetimelifetime);}

      API Usage

      Putting it all together, here's how the API can be leveraged for any container framework that supports registration through IServiceCollection.

      publicinterfaceIThing{stringToString();}publicabstractclassThingBase:IThing{protectedThingBase(){}publicoverridestringToString()=>GetType().Name;}publicsealedclassThing:ThingBase{}publicsealedclassKeyedThing:ThingBase{}publicsealedclassThing1:ThingBase{}publicsealedclassThing2:ThingBase{}publicsealedclassThing3:ThingBase{}publicstaticclassKey{publicsealedclassThingies{}publicsealedclassThing1{}publicsealedclassThing2{}}publicclassCatInTheHat{privatereadonlyIDependency<Key.Thing1,IThing>thing1;privatereadonlyIDependency<Key.Thing2,IThing>thing2;publicCatInTheHat(IDependency<Key.Thing1,IThing>thing1,IDependency<Key.Thing2,IThing>thing2){this.thing1=thing1;this.thing2=thing2;}publicIThingThing1=>thing1.Value;publicIThingThing2=>thing2.Value;}publicvoidConfigureServices(IServiceCollectioncollection){// keyed typesservices.AddSingleton<Key.Thing1,IThing,Thing1>();services.AddTransient<Key.Thing2,IThing,Thing2>();// non-keyed type with keyed type dependenciesservices.AddSingleton<CatInTheHat>();// keyed open genericsservices.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);varprovider=services.BuildServiceProvider();// resolve non-keyed type with keyed type dependenciesvarcatInTheHat=provider.GetRequiredService<CatInTheHat>();// resolve keyed, open genericvaropenGeneric=provider.GetRequiredService<Key.Thingy,IGeneric<object>>();// resolve keyed IEnumerable<T>varthingies=provider.GetServices<Key.Thingies,IThing>();// related services such as IServiceProviderIsService// new extension methods could be added to make this more succinctvarquery=provider.GetRequiredService<IServiceProviderIsService>();varthing1Registered=query.IsService(typeof(IDependency<Key.Thing1,IThing>));varthing2Registered=query.IsService(typeof(IDependency<Key.Thing2,IThing>));}

      Container Integration

      The following is a summary of results from Keyed Service POC repo.

      ContainerBy KeyBy Key
      (Generic)
      Many
      By Key
      Many By
      Key (Generic)
      Open
      Generics
      Existing
      Instance
      Implementation
      Factory
      Default
      Autofac
      DryIoc
      Grace
      Lamar
      LightInject
      Stashbox
      StructureMap
      Unity
      ContainerJust
      Works
      No Container
      Changes
      No Adapter
      Changes
      Default
      Autofac
      DryIoc
      Grace11
      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

      1. Thought experiment
      2. Initial proof of concept
      3. Practical API with a lot of ceremony removed

      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:

      usingstaticSystem.AttributeTargets;[AttributeUsage(Class|Interface|Parameter,AllowMultiple=false,Inherited=false)]publicsealedclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(stringkey)=>Key=key;publicstringKey{get;}}

      This attribute could be used in the following ways:

      [ServiceKey("Bar")]publicinterfaceIFoo{}7[ServiceKey("Foo")]publicclassFoo{}publicclassBar{publicBar([ServiceKey("Bar")]IFoofoo){}}

      Using an attribute has to main advantages:

      1. There needs to be a way to specify the key at the call site when a dependency is injected
      2. 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:

      publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,stringkey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

      It is not required for this proposal to work, but as an optimization, it may be worth adding:

      publicinterfaceIKeyedServiceProvider:IServiceProvider{object?GetService(TypeserviceType,stringkey);}

      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:

      publicstring?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:

      publicinterfaceIKeyedTypeFactory{TypeCreate(Typetype,stringkey);}

      Microsoft.Extensions.DependencyInjection will provide a default implementation that leverages CustomReflectionContext.

      The implementation might look like the following:

      publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey){varprovider=serviceProviderasIKeyedServiceProvider??serviceProvider.GetService<IKeyServiceProvider>();if(provider!=null){returnprovider.GetService(serviceType,key);}varfactory=serviceProvider.GetService<IKeyedTypeFactory>()??KeyedTypeFactory.Default;returnserviceProvider.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:

      classTeam{publicTeam([ServiceKey("A-Team")]IPityTheFoofoo){}// ← MrT is injected}// ...varservices=newServiceCollection();// Microsoft.Extensions.DependencyInjection.Abstractionsservices.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"));varprovider=services.BuildServiceProvider();varfoo=provider.GetRequiredService<IPityTheFoo>("A-Team");varteam=provider.GetRequiredService<Team>();varthingies=provider.GetServices<IThing>("Thingies");// related services such as IServiceProviderIsServicevarquery=provider.GetRequiredService<IServiceProviderIsService>();varshorthand=query.IsService<IPityTheFoo>("A-Team");varfactory=provider.GetRequiredService<IKeyedTypeService>();varlonghand=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.DependencyInjection would require one of the following:
        1. A dependency on System.Reflection.Context (unless another solution is found)
        2. An new, separate library that that references System.Reflection.Context and adds the keyed service capability
      • There is a potential explosion of overloads and/or extension methods
        • The requirement that these exist can be mitigated via the IKeyedServiceProvider and/or IKeyedTypeFactory intefaces
          • The developer experience is less than ideal, but no functionality is lost

      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.

      publicclassServiceDescriptor{[...]/// <summary>/// Get the key of the service, if applicable./// </summary>publicobject?ServiceKey{get;}[...]/// <summary>/// Gets the instance that implements the service./// </summary>publicobject?KeyedImplementationInstance{get;}/// <summary>/// Gets the <see cref="Type"/> that implements the service./// </summary>publicSystem.Type?KeyedImplementationType{get;}/// <summary>/// Gets the factory used for creating Keyed service instances./// </summary>publicFunc<IServiceProvider,object,object>?KeyedImplementationFactory{get;}[...]/// <summary>/// Returns true if a ServiceKey was provided./// </summary> publicboolIsKeyedService=>ServiceKey!=null;}

      ServiceKey will stay null in non-keyed services.

      Extension methods for IServiceCollection are added to support keyed services:

      publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedScoped<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,objectimplementationInstance);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,TServiceimplementationInstance)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectioncollection,objectserviceKey,TServiceinstance)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticIServiceCollectionRemoveAllKeyed(thisIServiceCollectioncollection,TypeserviceType,objectserviceKey);publicstaticIServiceCollectionRemoveAllKeyed<T>(thisIServiceCollectioncollection,objectserviceKey);

      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 providers1=provider.GetKeyedService<IService>("other-service");// returns otherServices1=provider.GetKeyedService<IService>("another-random-key");// returns defaultService

      Resolving service

      Basic keyed resolution

      Two new optional interfaces will be introduced:

      namespaceMicrosoft.Extensions.DependencyInjection;publicinterfaceISupportKeyedService{object?GetKeyedService(TypeserviceType,objectserviceKey);objectGetRequiredKeyedService(TypeserviceType,objectserviceKey);}publicinterfaceIServiceProviderIsServiceKeyed{boolIsService(TypeserviceType,objectserviceKey);}

      This new interface will be accessible via the following extension methods:

      publicstaticIEnumerable<object?>GetKeyedServices(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticIEnumerable<T>GetKeyedServices<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticT?GetKeyedService<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticobjectGetRequiredKeyedService(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticTGetRequiredKeyedService<T>(thisIServiceProviderprovider,objectserviceKey)whereT: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:

      namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(){}}classService{privatereadonlystring_id;publicService([ServiceKey]stringid)=>_id=id;}serviceCollection.AddKeyedSingleton<Service>("some-service");[...]// build the providervar 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:

      namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassFromKeyedServicesAttribute:Attribute{publicFromKeyedServicesAttribute(objectkey){}publicobjectKey{get;}}classOtherService{publicOtherService([FromKeyedServices("service1")]IServiceservice1,[FromKeyedServices("service2")]IServiceservice2){Service1=service1;Service2=service2;}}

      Open generics

      Open generics are supported:

      serviceCollection.AddTransient(typeof(IGenericInterface<>),"my-service",typeof(GenericService<>));[...]// build the providervar 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 providerservices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB and MyServiceC

      Note 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 providerkeyedServices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB but NOT MyServiceCservices=provider.GetServices<IMyService>();// only returns MyServiceC

      But we do not support:

      serviceCollection.AddKeyedSingleton<MyServiceA>("some-service");serviceCollection.AddKeyedSingleton<MyServiceB>("some-service");serviceCollection.AddKeyedSingleton<MyServiceC>("some-service");[...]// build the providerservices=provider.GetKeyedServices("some-service");// Not supported

      Metadata

      Metadata

      Assignees

      Labels

      api-approvedAPI was approved in API review, it can be implementedarea-Extensions-DependencyInjectionblockingMarks issues that we want to fast track in order to unblock other important work

      Type

      No type

      Projects

      No projects

        Milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        [API Proposal]: Add Keyed Services Support to Dependency Injection #64427

        Description

        @commonsensesoftware

        Thanks @commonsensesoftware for the original proposal. I edited this post to show the current proposition.

        Original proposal from @commonsensesoftware ### Background and Motivation

        I'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)publicinterfaceIDependency{objectValue{get;}}publicinterfaceIDependency<inTKey,outTService>:IDependencywhereTService:notnull{newTServiceValue{get;}}

        The following extension methods will be added to ServiceProviderServiceExtensions:

        publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,Typekey)whereT:notnull;publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

        Here is a partial example of how it would be implemented:

        publicstaticclassServiceProviderExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey){varkeyedType=typeof(IDependency<,>).MakeGenericType(key,serviceType);vardependency=(IDependency?)serviceProvider.GetService(keyedType);returndependency?.Value;}publicstaticTService?GetService<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{vardependency=serviceProvider.GetService<IDependency<TKey,TService>>();returndependencyisnull?default:dependency.Value;}publicstaticIEnumerable<TService>GetServices<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{foreach(vardependencyinserviceProvider.GetServices<IDependency<TKey,TService>>()){yieldreturndependency.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.

        publicstaticclassKeyedType{publicstaticTypeCreate(Typekey,Typetype)=>newTypeWithKey(key,type);publicstaticTypeCreate<TKey,TType>()whereTType:notnull=>newTypeWithKey(typeof(TKey),typeof(TType));privatesealedclassTypeWithKey:TypeDelegator{privatereadonlyinthashCode;publicTypeWithKey(TypekeyType,TypecustomType):base(customType)=>hashCode=HashCode.Combine(typeImpl,keyType);publicoverrideintGetHashCode()=>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:

        publicsealedclassDependency<TKey,TService>:IDependency<TKey,TService>whereTService:notnull{privatereadonlyIServiceProviderserviceProvider;publicDependency(IServiceProviderserviceProvider)=>this.serviceProvider=serviceProvider;publicTServiceValue=>(TService)serviceProvider.GetRequiredService(KeyedType.Create<TKey,TService>());objectIDependency.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:

        publicvoidConfigureServices(IServiceCollectionservices){services.AddTransient(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));services.AddTransient<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureUnity(IUnityContainercontainer){container.RegisterType(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));container.RegisterType<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureAutofac(ContainerBuilderbuilder){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:

        varlongForm=serviceProvider.GetRequiredService<IDependency<Key.Thing1,IThing>>().Value;varshortForm=serviceProvider.GetRequiredService<Key.Thing1,IThing>();

        The following extension methods will be added to ServiceCollectionDescriptorExtensions to provide common registration through IServiceCollection for all container frameworks:

        publicstaticclassServiceCollectionExtensions{publicstaticIServiceCollectionAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddEnumerable<TKey,TService,TImplementation>(thisIServiceCollectionservices,ServiceLifetimelifetime)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddEnumerable(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType,ServiceLifetimelifetime);}

        API Usage

        Putting it all together, here's how the API can be leveraged for any container framework that supports registration through IServiceCollection.

        publicinterfaceIThing{stringToString();}publicabstractclassThingBase:IThing{protectedThingBase(){}publicoverridestringToString()=>GetType().Name;}publicsealedclassThing:ThingBase{}publicsealedclassKeyedThing:ThingBase{}publicsealedclassThing1:ThingBase{}publicsealedclassThing2:ThingBase{}publicsealedclassThing3:ThingBase{}publicstaticclassKey{publicsealedclassThingies{}publicsealedclassThing1{}publicsealedclassThing2{}}publicclassCatInTheHat{privatereadonlyIDependency<Key.Thing1,IThing>thing1;privatereadonlyIDependency<Key.Thing2,IThing>thing2;publicCatInTheHat(IDependency<Key.Thing1,IThing>thing1,IDependency<Key.Thing2,IThing>thing2){this.thing1=thing1;this.thing2=thing2;}publicIThingThing1=>thing1.Value;publicIThingThing2=>thing2.Value;}publicvoidConfigureServices(IServiceCollectioncollection){// keyed typesservices.AddSingleton<Key.Thing1,IThing,Thing1>();services.AddTransient<Key.Thing2,IThing,Thing2>();// non-keyed type with keyed type dependenciesservices.AddSingleton<CatInTheHat>();// keyed open genericsservices.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);varprovider=services.BuildServiceProvider();// resolve non-keyed type with keyed type dependenciesvarcatInTheHat=provider.GetRequiredService<CatInTheHat>();// resolve keyed, open genericvaropenGeneric=provider.GetRequiredService<Key.Thingy,IGeneric<object>>();// resolve keyed IEnumerable<T>varthingies=provider.GetServices<Key.Thingies,IThing>();// related services such as IServiceProviderIsService// new extension methods could be added to make this more succinctvarquery=provider.GetRequiredService<IServiceProviderIsService>();varthing1Registered=query.IsService(typeof(IDependency<Key.Thing1,IThing>));varthing2Registered=query.IsService(typeof(IDependency<Key.Thing2,IThing>));}

        Container Integration

        The following is a summary of results from Keyed Service POC repo.

        ContainerBy KeyBy Key
        (Generic)
        Many
        By Key
        Many By
        Key (Generic)
        Open
        Generics
        Existing
        Instance
        Implementation
        Factory
        Default
        Autofac
        DryIoc
        Grace
        Lamar
        LightInject
        Stashbox
        StructureMap
        Unity
        ContainerJust
        Works
        No Container
        Changes
        No Adapter
        Changes
        Default
        Autofac
        DryIoc
        Grace11
        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

        1. Thought experiment
        2. Initial proof of concept
        3. Practical API with a lot of ceremony removed

        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:

        usingstaticSystem.AttributeTargets;[AttributeUsage(Class|Interface|Parameter,AllowMultiple=false,Inherited=false)]publicsealedclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(stringkey)=>Key=key;publicstringKey{get;}}

        This attribute could be used in the following ways:

        [ServiceKey("Bar")]publicinterfaceIFoo{}7[ServiceKey("Foo")]publicclassFoo{}publicclassBar{publicBar([ServiceKey("Bar")]IFoofoo){}}

        Using an attribute has to main advantages:

        1. There needs to be a way to specify the key at the call site when a dependency is injected
        2. 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:

        publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,stringkey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

        It is not required for this proposal to work, but as an optimization, it may be worth adding:

        publicinterfaceIKeyedServiceProvider:IServiceProvider{object?GetService(TypeserviceType,stringkey);}

        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:

        publicstring?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:

        publicinterfaceIKeyedTypeFactory{TypeCreate(Typetype,stringkey);}

        Microsoft.Extensions.DependencyInjection will provide a default implementation that leverages CustomReflectionContext.

        The implementation might look like the following:

        publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey){varprovider=serviceProviderasIKeyedServiceProvider??serviceProvider.GetService<IKeyServiceProvider>();if(provider!=null){returnprovider.GetService(serviceType,key);}varfactory=serviceProvider.GetService<IKeyedTypeFactory>()??KeyedTypeFactory.Default;returnserviceProvider.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:

        classTeam{publicTeam([ServiceKey("A-Team")]IPityTheFoofoo){}// ← MrT is injected}// ...varservices=newServiceCollection();// Microsoft.Extensions.DependencyInjection.Abstractionsservices.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"));varprovider=services.BuildServiceProvider();varfoo=provider.GetRequiredService<IPityTheFoo>("A-Team");varteam=provider.GetRequiredService<Team>();varthingies=provider.GetServices<IThing>("Thingies");// related services such as IServiceProviderIsServicevarquery=provider.GetRequiredService<IServiceProviderIsService>();varshorthand=query.IsService<IPityTheFoo>("A-Team");varfactory=provider.GetRequiredService<IKeyedTypeService>();varlonghand=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.DependencyInjection would require one of the following:
          1. A dependency on System.Reflection.Context (unless another solution is found)
          2. An new, separate library that that references System.Reflection.Context and adds the keyed service capability
        • There is a potential explosion of overloads and/or extension methods
          • The requirement that these exist can be mitigated via the IKeyedServiceProvider and/or IKeyedTypeFactory intefaces
            • The developer experience is less than ideal, but no functionality is lost

        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.

        publicclassServiceDescriptor{[...]/// <summary>/// Get the key of the service, if applicable./// </summary>publicobject?ServiceKey{get;}[...]/// <summary>/// Gets the instance that implements the service./// </summary>publicobject?KeyedImplementationInstance{get;}/// <summary>/// Gets the <see cref="Type"/> that implements the service./// </summary>publicSystem.Type?KeyedImplementationType{get;}/// <summary>/// Gets the factory used for creating Keyed service instances./// </summary>publicFunc<IServiceProvider,object,object>?KeyedImplementationFactory{get;}[...]/// <summary>/// Returns true if a ServiceKey was provided./// </summary> publicboolIsKeyedService=>ServiceKey!=null;}

        ServiceKey will stay null in non-keyed services.

        Extension methods for IServiceCollection are added to support keyed services:

        publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedScoped<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,objectimplementationInstance);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,TServiceimplementationInstance)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectioncollection,objectserviceKey,TServiceinstance)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticIServiceCollectionRemoveAllKeyed(thisIServiceCollectioncollection,TypeserviceType,objectserviceKey);publicstaticIServiceCollectionRemoveAllKeyed<T>(thisIServiceCollectioncollection,objectserviceKey);

        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 providers1=provider.GetKeyedService<IService>("other-service");// returns otherServices1=provider.GetKeyedService<IService>("another-random-key");// returns defaultService

        Resolving service

        Basic keyed resolution

        Two new optional interfaces will be introduced:

        namespaceMicrosoft.Extensions.DependencyInjection;publicinterfaceISupportKeyedService{object?GetKeyedService(TypeserviceType,objectserviceKey);objectGetRequiredKeyedService(TypeserviceType,objectserviceKey);}publicinterfaceIServiceProviderIsServiceKeyed{boolIsService(TypeserviceType,objectserviceKey);}

        This new interface will be accessible via the following extension methods:

        publicstaticIEnumerable<object?>GetKeyedServices(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticIEnumerable<T>GetKeyedServices<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticT?GetKeyedService<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticobjectGetRequiredKeyedService(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticTGetRequiredKeyedService<T>(thisIServiceProviderprovider,objectserviceKey)whereT: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:

        namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(){}}classService{privatereadonlystring_id;publicService([ServiceKey]stringid)=>_id=id;}serviceCollection.AddKeyedSingleton<Service>("some-service");[...]// build the providervar 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:

        namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassFromKeyedServicesAttribute:Attribute{publicFromKeyedServicesAttribute(objectkey){}publicobjectKey{get;}}classOtherService{publicOtherService([FromKeyedServices("service1")]IServiceservice1,[FromKeyedServices("service2")]IServiceservice2){Service1=service1;Service2=service2;}}

        Open generics

        Open generics are supported:

        serviceCollection.AddTransient(typeof(IGenericInterface<>),"my-service",typeof(GenericService<>));[...]// build the providervar 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 providerservices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB and MyServiceC

        Note 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 providerkeyedServices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB but NOT MyServiceCservices=provider.GetServices<IMyService>();// only returns MyServiceC

        But we do not support:

        serviceCollection.AddKeyedSingleton<MyServiceA>("some-service");serviceCollection.AddKeyedSingleton<MyServiceB>("some-service");serviceCollection.AddKeyedSingleton<MyServiceC>("some-service");[...]// build the providerservices=provider.GetKeyedServices("some-service");// Not supported

        Metadata

        Metadata

        Assignees

        Labels

        api-approvedAPI was approved in API review, it can be implementedarea-Extensions-DependencyInjectionblockingMarks issues that we want to fast track in order to unblock other important work

        Type

        No type

        Projects

        No projects

          Milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          [API Proposal]: Add Keyed Services Support to Dependency Injection #64427

          Description

          @commonsensesoftware

          Thanks @commonsensesoftware for the original proposal. I edited this post to show the current proposition.

          Original proposal from @commonsensesoftware ### Background and Motivation

          I'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)publicinterfaceIDependency{objectValue{get;}}publicinterfaceIDependency<inTKey,outTService>:IDependencywhereTService:notnull{newTServiceValue{get;}}

          The following extension methods will be added to ServiceProviderServiceExtensions:

          publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,Typekey)whereT:notnull;publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

          Here is a partial example of how it would be implemented:

          publicstaticclassServiceProviderExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey){varkeyedType=typeof(IDependency<,>).MakeGenericType(key,serviceType);vardependency=(IDependency?)serviceProvider.GetService(keyedType);returndependency?.Value;}publicstaticTService?GetService<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{vardependency=serviceProvider.GetService<IDependency<TKey,TService>>();returndependencyisnull?default:dependency.Value;}publicstaticIEnumerable<TService>GetServices<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{foreach(vardependencyinserviceProvider.GetServices<IDependency<TKey,TService>>()){yieldreturndependency.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.

          publicstaticclassKeyedType{publicstaticTypeCreate(Typekey,Typetype)=>newTypeWithKey(key,type);publicstaticTypeCreate<TKey,TType>()whereTType:notnull=>newTypeWithKey(typeof(TKey),typeof(TType));privatesealedclassTypeWithKey:TypeDelegator{privatereadonlyinthashCode;publicTypeWithKey(TypekeyType,TypecustomType):base(customType)=>hashCode=HashCode.Combine(typeImpl,keyType);publicoverrideintGetHashCode()=>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:

          publicsealedclassDependency<TKey,TService>:IDependency<TKey,TService>whereTService:notnull{privatereadonlyIServiceProviderserviceProvider;publicDependency(IServiceProviderserviceProvider)=>this.serviceProvider=serviceProvider;publicTServiceValue=>(TService)serviceProvider.GetRequiredService(KeyedType.Create<TKey,TService>());objectIDependency.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:

          publicvoidConfigureServices(IServiceCollectionservices){services.AddTransient(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));services.AddTransient<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureUnity(IUnityContainercontainer){container.RegisterType(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));container.RegisterType<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureAutofac(ContainerBuilderbuilder){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:

          varlongForm=serviceProvider.GetRequiredService<IDependency<Key.Thing1,IThing>>().Value;varshortForm=serviceProvider.GetRequiredService<Key.Thing1,IThing>();

          The following extension methods will be added to ServiceCollectionDescriptorExtensions to provide common registration through IServiceCollection for all container frameworks:

          publicstaticclassServiceCollectionExtensions{publicstaticIServiceCollectionAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddEnumerable<TKey,TService,TImplementation>(thisIServiceCollectionservices,ServiceLifetimelifetime)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddEnumerable(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType,ServiceLifetimelifetime);}

          API Usage

          Putting it all together, here's how the API can be leveraged for any container framework that supports registration through IServiceCollection.

          publicinterfaceIThing{stringToString();}publicabstractclassThingBase:IThing{protectedThingBase(){}publicoverridestringToString()=>GetType().Name;}publicsealedclassThing:ThingBase{}publicsealedclassKeyedThing:ThingBase{}publicsealedclassThing1:ThingBase{}publicsealedclassThing2:ThingBase{}publicsealedclassThing3:ThingBase{}publicstaticclassKey{publicsealedclassThingies{}publicsealedclassThing1{}publicsealedclassThing2{}}publicclassCatInTheHat{privatereadonlyIDependency<Key.Thing1,IThing>thing1;privatereadonlyIDependency<Key.Thing2,IThing>thing2;publicCatInTheHat(IDependency<Key.Thing1,IThing>thing1,IDependency<Key.Thing2,IThing>thing2){this.thing1=thing1;this.thing2=thing2;}publicIThingThing1=>thing1.Value;publicIThingThing2=>thing2.Value;}publicvoidConfigureServices(IServiceCollectioncollection){// keyed typesservices.AddSingleton<Key.Thing1,IThing,Thing1>();services.AddTransient<Key.Thing2,IThing,Thing2>();// non-keyed type with keyed type dependenciesservices.AddSingleton<CatInTheHat>();// keyed open genericsservices.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);varprovider=services.BuildServiceProvider();// resolve non-keyed type with keyed type dependenciesvarcatInTheHat=provider.GetRequiredService<CatInTheHat>();// resolve keyed, open genericvaropenGeneric=provider.GetRequiredService<Key.Thingy,IGeneric<object>>();// resolve keyed IEnumerable<T>varthingies=provider.GetServices<Key.Thingies,IThing>();// related services such as IServiceProviderIsService// new extension methods could be added to make this more succinctvarquery=provider.GetRequiredService<IServiceProviderIsService>();varthing1Registered=query.IsService(typeof(IDependency<Key.Thing1,IThing>));varthing2Registered=query.IsService(typeof(IDependency<Key.Thing2,IThing>));}

          Container Integration

          The following is a summary of results from Keyed Service POC repo.

          ContainerBy KeyBy Key
          (Generic)
          Many
          By Key
          Many By
          Key (Generic)
          Open
          Generics
          Existing
          Instance
          Implementation
          Factory
          Default
          Autofac
          DryIoc
          Grace
          Lamar
          LightInject
          Stashbox
          StructureMap
          Unity
          ContainerJust
          Works
          No Container
          Changes
          No Adapter
          Changes
          Default
          Autofac
          DryIoc
          Grace11
          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

          1. Thought experiment
          2. Initial proof of concept
          3. Practical API with a lot of ceremony removed

          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:

          usingstaticSystem.AttributeTargets;[AttributeUsage(Class|Interface|Parameter,AllowMultiple=false,Inherited=false)]publicsealedclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(stringkey)=>Key=key;publicstringKey{get;}}

          This attribute could be used in the following ways:

          [ServiceKey("Bar")]publicinterfaceIFoo{}7[ServiceKey("Foo")]publicclassFoo{}publicclassBar{publicBar([ServiceKey("Bar")]IFoofoo){}}

          Using an attribute has to main advantages:

          1. There needs to be a way to specify the key at the call site when a dependency is injected
          2. 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:

          publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,stringkey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

          It is not required for this proposal to work, but as an optimization, it may be worth adding:

          publicinterfaceIKeyedServiceProvider:IServiceProvider{object?GetService(TypeserviceType,stringkey);}

          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:

          publicstring?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:

          publicinterfaceIKeyedTypeFactory{TypeCreate(Typetype,stringkey);}

          Microsoft.Extensions.DependencyInjection will provide a default implementation that leverages CustomReflectionContext.

          The implementation might look like the following:

          publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey){varprovider=serviceProviderasIKeyedServiceProvider??serviceProvider.GetService<IKeyServiceProvider>();if(provider!=null){returnprovider.GetService(serviceType,key);}varfactory=serviceProvider.GetService<IKeyedTypeFactory>()??KeyedTypeFactory.Default;returnserviceProvider.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:

          classTeam{publicTeam([ServiceKey("A-Team")]IPityTheFoofoo){}// ← MrT is injected}// ...varservices=newServiceCollection();// Microsoft.Extensions.DependencyInjection.Abstractionsservices.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"));varprovider=services.BuildServiceProvider();varfoo=provider.GetRequiredService<IPityTheFoo>("A-Team");varteam=provider.GetRequiredService<Team>();varthingies=provider.GetServices<IThing>("Thingies");// related services such as IServiceProviderIsServicevarquery=provider.GetRequiredService<IServiceProviderIsService>();varshorthand=query.IsService<IPityTheFoo>("A-Team");varfactory=provider.GetRequiredService<IKeyedTypeService>();varlonghand=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.DependencyInjection would require one of the following:
            1. A dependency on System.Reflection.Context (unless another solution is found)
            2. An new, separate library that that references System.Reflection.Context and adds the keyed service capability
          • There is a potential explosion of overloads and/or extension methods
            • The requirement that these exist can be mitigated via the IKeyedServiceProvider and/or IKeyedTypeFactory intefaces
              • The developer experience is less than ideal, but no functionality is lost

          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.

          publicclassServiceDescriptor{[...]/// <summary>/// Get the key of the service, if applicable./// </summary>publicobject?ServiceKey{get;}[...]/// <summary>/// Gets the instance that implements the service./// </summary>publicobject?KeyedImplementationInstance{get;}/// <summary>/// Gets the <see cref="Type"/> that implements the service./// </summary>publicSystem.Type?KeyedImplementationType{get;}/// <summary>/// Gets the factory used for creating Keyed service instances./// </summary>publicFunc<IServiceProvider,object,object>?KeyedImplementationFactory{get;}[...]/// <summary>/// Returns true if a ServiceKey was provided./// </summary> publicboolIsKeyedService=>ServiceKey!=null;}

          ServiceKey will stay null in non-keyed services.

          Extension methods for IServiceCollection are added to support keyed services:

          publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedScoped<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,objectimplementationInstance);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,TServiceimplementationInstance)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectioncollection,objectserviceKey,TServiceinstance)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticIServiceCollectionRemoveAllKeyed(thisIServiceCollectioncollection,TypeserviceType,objectserviceKey);publicstaticIServiceCollectionRemoveAllKeyed<T>(thisIServiceCollectioncollection,objectserviceKey);

          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 providers1=provider.GetKeyedService<IService>("other-service");// returns otherServices1=provider.GetKeyedService<IService>("another-random-key");// returns defaultService

          Resolving service

          Basic keyed resolution

          Two new optional interfaces will be introduced:

          namespaceMicrosoft.Extensions.DependencyInjection;publicinterfaceISupportKeyedService{object?GetKeyedService(TypeserviceType,objectserviceKey);objectGetRequiredKeyedService(TypeserviceType,objectserviceKey);}publicinterfaceIServiceProviderIsServiceKeyed{boolIsService(TypeserviceType,objectserviceKey);}

          This new interface will be accessible via the following extension methods:

          publicstaticIEnumerable<object?>GetKeyedServices(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticIEnumerable<T>GetKeyedServices<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticT?GetKeyedService<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticobjectGetRequiredKeyedService(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticTGetRequiredKeyedService<T>(thisIServiceProviderprovider,objectserviceKey)whereT: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:

          namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(){}}classService{privatereadonlystring_id;publicService([ServiceKey]stringid)=>_id=id;}serviceCollection.AddKeyedSingleton<Service>("some-service");[...]// build the providervar 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:

          namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassFromKeyedServicesAttribute:Attribute{publicFromKeyedServicesAttribute(objectkey){}publicobjectKey{get;}}classOtherService{publicOtherService([FromKeyedServices("service1")]IServiceservice1,[FromKeyedServices("service2")]IServiceservice2){Service1=service1;Service2=service2;}}

          Open generics

          Open generics are supported:

          serviceCollection.AddTransient(typeof(IGenericInterface<>),"my-service",typeof(GenericService<>));[...]// build the providervar 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 providerservices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB and MyServiceC

          Note 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 providerkeyedServices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB but NOT MyServiceCservices=provider.GetServices<IMyService>();// only returns MyServiceC

          But we do not support:

          serviceCollection.AddKeyedSingleton<MyServiceA>("some-service");serviceCollection.AddKeyedSingleton<MyServiceB>("some-service");serviceCollection.AddKeyedSingleton<MyServiceC>("some-service");[...]// build the providerservices=provider.GetKeyedServices("some-service");// Not supported

          Metadata

          Metadata

          Assignees

          Labels

          api-approvedAPI was approved in API review, it can be implementedarea-Extensions-DependencyInjectionblockingMarks issues that we want to fast track in order to unblock other important work

          Type

          No type

          Projects

          No projects

            Milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            [API Proposal]: Add Keyed Services Support to Dependency Injection #64427

            Description

            @commonsensesoftware

            Thanks @commonsensesoftware for the original proposal. I edited this post to show the current proposition.

            Original proposal from @commonsensesoftware ### Background and Motivation

            I'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)publicinterfaceIDependency{objectValue{get;}}publicinterfaceIDependency<inTKey,outTService>:IDependencywhereTService:notnull{newTServiceValue{get;}}

            The following extension methods will be added to ServiceProviderServiceExtensions:

            publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,Typekey)whereT:notnull;publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

            Here is a partial example of how it would be implemented:

            publicstaticclassServiceProviderExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey){varkeyedType=typeof(IDependency<,>).MakeGenericType(key,serviceType);vardependency=(IDependency?)serviceProvider.GetService(keyedType);returndependency?.Value;}publicstaticTService?GetService<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{vardependency=serviceProvider.GetService<IDependency<TKey,TService>>();returndependencyisnull?default:dependency.Value;}publicstaticIEnumerable<TService>GetServices<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{foreach(vardependencyinserviceProvider.GetServices<IDependency<TKey,TService>>()){yieldreturndependency.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.

            publicstaticclassKeyedType{publicstaticTypeCreate(Typekey,Typetype)=>newTypeWithKey(key,type);publicstaticTypeCreate<TKey,TType>()whereTType:notnull=>newTypeWithKey(typeof(TKey),typeof(TType));privatesealedclassTypeWithKey:TypeDelegator{privatereadonlyinthashCode;publicTypeWithKey(TypekeyType,TypecustomType):base(customType)=>hashCode=HashCode.Combine(typeImpl,keyType);publicoverrideintGetHashCode()=>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:

            publicsealedclassDependency<TKey,TService>:IDependency<TKey,TService>whereTService:notnull{privatereadonlyIServiceProviderserviceProvider;publicDependency(IServiceProviderserviceProvider)=>this.serviceProvider=serviceProvider;publicTServiceValue=>(TService)serviceProvider.GetRequiredService(KeyedType.Create<TKey,TService>());objectIDependency.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:

            publicvoidConfigureServices(IServiceCollectionservices){services.AddTransient(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));services.AddTransient<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureUnity(IUnityContainercontainer){container.RegisterType(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));container.RegisterType<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureAutofac(ContainerBuilderbuilder){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:

            varlongForm=serviceProvider.GetRequiredService<IDependency<Key.Thing1,IThing>>().Value;varshortForm=serviceProvider.GetRequiredService<Key.Thing1,IThing>();

            The following extension methods will be added to ServiceCollectionDescriptorExtensions to provide common registration through IServiceCollection for all container frameworks:

            publicstaticclassServiceCollectionExtensions{publicstaticIServiceCollectionAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddEnumerable<TKey,TService,TImplementation>(thisIServiceCollectionservices,ServiceLifetimelifetime)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddEnumerable(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType,ServiceLifetimelifetime);}

            API Usage

            Putting it all together, here's how the API can be leveraged for any container framework that supports registration through IServiceCollection.

            publicinterfaceIThing{stringToString();}publicabstractclassThingBase:IThing{protectedThingBase(){}publicoverridestringToString()=>GetType().Name;}publicsealedclassThing:ThingBase{}publicsealedclassKeyedThing:ThingBase{}publicsealedclassThing1:ThingBase{}publicsealedclassThing2:ThingBase{}publicsealedclassThing3:ThingBase{}publicstaticclassKey{publicsealedclassThingies{}publicsealedclassThing1{}publicsealedclassThing2{}}publicclassCatInTheHat{privatereadonlyIDependency<Key.Thing1,IThing>thing1;privatereadonlyIDependency<Key.Thing2,IThing>thing2;publicCatInTheHat(IDependency<Key.Thing1,IThing>thing1,IDependency<Key.Thing2,IThing>thing2){this.thing1=thing1;this.thing2=thing2;}publicIThingThing1=>thing1.Value;publicIThingThing2=>thing2.Value;}publicvoidConfigureServices(IServiceCollectioncollection){// keyed typesservices.AddSingleton<Key.Thing1,IThing,Thing1>();services.AddTransient<Key.Thing2,IThing,Thing2>();// non-keyed type with keyed type dependenciesservices.AddSingleton<CatInTheHat>();// keyed open genericsservices.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);varprovider=services.BuildServiceProvider();// resolve non-keyed type with keyed type dependenciesvarcatInTheHat=provider.GetRequiredService<CatInTheHat>();// resolve keyed, open genericvaropenGeneric=provider.GetRequiredService<Key.Thingy,IGeneric<object>>();// resolve keyed IEnumerable<T>varthingies=provider.GetServices<Key.Thingies,IThing>();// related services such as IServiceProviderIsService// new extension methods could be added to make this more succinctvarquery=provider.GetRequiredService<IServiceProviderIsService>();varthing1Registered=query.IsService(typeof(IDependency<Key.Thing1,IThing>));varthing2Registered=query.IsService(typeof(IDependency<Key.Thing2,IThing>));}

            Container Integration

            The following is a summary of results from Keyed Service POC repo.

            ContainerBy KeyBy Key
            (Generic)
            Many
            By Key
            Many By
            Key (Generic)
            Open
            Generics
            Existing
            Instance
            Implementation
            Factory
            Default
            Autofac
            DryIoc
            Grace
            Lamar
            LightInject
            Stashbox
            StructureMap
            Unity
            ContainerJust
            Works
            No Container
            Changes
            No Adapter
            Changes
            Default
            Autofac
            DryIoc
            Grace11
            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

            1. Thought experiment
            2. Initial proof of concept
            3. Practical API with a lot of ceremony removed

            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:

            usingstaticSystem.AttributeTargets;[AttributeUsage(Class|Interface|Parameter,AllowMultiple=false,Inherited=false)]publicsealedclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(stringkey)=>Key=key;publicstringKey{get;}}

            This attribute could be used in the following ways:

            [ServiceKey("Bar")]publicinterfaceIFoo{}7[ServiceKey("Foo")]publicclassFoo{}publicclassBar{publicBar([ServiceKey("Bar")]IFoofoo){}}

            Using an attribute has to main advantages:

            1. There needs to be a way to specify the key at the call site when a dependency is injected
            2. 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:

            publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,stringkey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

            It is not required for this proposal to work, but as an optimization, it may be worth adding:

            publicinterfaceIKeyedServiceProvider:IServiceProvider{object?GetService(TypeserviceType,stringkey);}

            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:

            publicstring?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:

            publicinterfaceIKeyedTypeFactory{TypeCreate(Typetype,stringkey);}

            Microsoft.Extensions.DependencyInjection will provide a default implementation that leverages CustomReflectionContext.

            The implementation might look like the following:

            publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey){varprovider=serviceProviderasIKeyedServiceProvider??serviceProvider.GetService<IKeyServiceProvider>();if(provider!=null){returnprovider.GetService(serviceType,key);}varfactory=serviceProvider.GetService<IKeyedTypeFactory>()??KeyedTypeFactory.Default;returnserviceProvider.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:

            classTeam{publicTeam([ServiceKey("A-Team")]IPityTheFoofoo){}// ← MrT is injected}// ...varservices=newServiceCollection();// Microsoft.Extensions.DependencyInjection.Abstractionsservices.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"));varprovider=services.BuildServiceProvider();varfoo=provider.GetRequiredService<IPityTheFoo>("A-Team");varteam=provider.GetRequiredService<Team>();varthingies=provider.GetServices<IThing>("Thingies");// related services such as IServiceProviderIsServicevarquery=provider.GetRequiredService<IServiceProviderIsService>();varshorthand=query.IsService<IPityTheFoo>("A-Team");varfactory=provider.GetRequiredService<IKeyedTypeService>();varlonghand=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.DependencyInjection would require one of the following:
              1. A dependency on System.Reflection.Context (unless another solution is found)
              2. An new, separate library that that references System.Reflection.Context and adds the keyed service capability
            • There is a potential explosion of overloads and/or extension methods
              • The requirement that these exist can be mitigated via the IKeyedServiceProvider and/or IKeyedTypeFactory intefaces
                • The developer experience is less than ideal, but no functionality is lost

            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.

            publicclassServiceDescriptor{[...]/// <summary>/// Get the key of the service, if applicable./// </summary>publicobject?ServiceKey{get;}[...]/// <summary>/// Gets the instance that implements the service./// </summary>publicobject?KeyedImplementationInstance{get;}/// <summary>/// Gets the <see cref="Type"/> that implements the service./// </summary>publicSystem.Type?KeyedImplementationType{get;}/// <summary>/// Gets the factory used for creating Keyed service instances./// </summary>publicFunc<IServiceProvider,object,object>?KeyedImplementationFactory{get;}[...]/// <summary>/// Returns true if a ServiceKey was provided./// </summary> publicboolIsKeyedService=>ServiceKey!=null;}

            ServiceKey will stay null in non-keyed services.

            Extension methods for IServiceCollection are added to support keyed services:

            publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedScoped<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,objectimplementationInstance);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,TServiceimplementationInstance)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectioncollection,objectserviceKey,TServiceinstance)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticIServiceCollectionRemoveAllKeyed(thisIServiceCollectioncollection,TypeserviceType,objectserviceKey);publicstaticIServiceCollectionRemoveAllKeyed<T>(thisIServiceCollectioncollection,objectserviceKey);

            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 providers1=provider.GetKeyedService<IService>("other-service");// returns otherServices1=provider.GetKeyedService<IService>("another-random-key");// returns defaultService

            Resolving service

            Basic keyed resolution

            Two new optional interfaces will be introduced:

            namespaceMicrosoft.Extensions.DependencyInjection;publicinterfaceISupportKeyedService{object?GetKeyedService(TypeserviceType,objectserviceKey);objectGetRequiredKeyedService(TypeserviceType,objectserviceKey);}publicinterfaceIServiceProviderIsServiceKeyed{boolIsService(TypeserviceType,objectserviceKey);}

            This new interface will be accessible via the following extension methods:

            publicstaticIEnumerable<object?>GetKeyedServices(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticIEnumerable<T>GetKeyedServices<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticT?GetKeyedService<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticobjectGetRequiredKeyedService(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticTGetRequiredKeyedService<T>(thisIServiceProviderprovider,objectserviceKey)whereT: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:

            namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(){}}classService{privatereadonlystring_id;publicService([ServiceKey]stringid)=>_id=id;}serviceCollection.AddKeyedSingleton<Service>("some-service");[...]// build the providervar 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:

            namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassFromKeyedServicesAttribute:Attribute{publicFromKeyedServicesAttribute(objectkey){}publicobjectKey{get;}}classOtherService{publicOtherService([FromKeyedServices("service1")]IServiceservice1,[FromKeyedServices("service2")]IServiceservice2){Service1=service1;Service2=service2;}}

            Open generics

            Open generics are supported:

            serviceCollection.AddTransient(typeof(IGenericInterface<>),"my-service",typeof(GenericService<>));[...]// build the providervar 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 providerservices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB and MyServiceC

            Note 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 providerkeyedServices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB but NOT MyServiceCservices=provider.GetServices<IMyService>();// only returns MyServiceC

            But we do not support:

            serviceCollection.AddKeyedSingleton<MyServiceA>("some-service");serviceCollection.AddKeyedSingleton<MyServiceB>("some-service");serviceCollection.AddKeyedSingleton<MyServiceC>("some-service");[...]// build the providerservices=provider.GetKeyedServices("some-service");// Not supported

            Metadata

            Metadata

            Assignees

            Labels

            api-approvedAPI was approved in API review, it can be implementedarea-Extensions-DependencyInjectionblockingMarks issues that we want to fast track in order to unblock other important work

            Type

            No type

            Projects

            No projects

              Milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              [API Proposal]: Add Keyed Services Support to Dependency Injection #64427

              Description

              @commonsensesoftware

              Thanks @commonsensesoftware for the original proposal. I edited this post to show the current proposition.

              Original proposal from @commonsensesoftware ### Background and Motivation

              I'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)publicinterfaceIDependency{objectValue{get;}}publicinterfaceIDependency<inTKey,outTService>:IDependencywhereTService:notnull{newTServiceValue{get;}}

              The following extension methods will be added to ServiceProviderServiceExtensions:

              publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,Typekey)whereT:notnull;publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

              Here is a partial example of how it would be implemented:

              publicstaticclassServiceProviderExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey){varkeyedType=typeof(IDependency<,>).MakeGenericType(key,serviceType);vardependency=(IDependency?)serviceProvider.GetService(keyedType);returndependency?.Value;}publicstaticTService?GetService<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{vardependency=serviceProvider.GetService<IDependency<TKey,TService>>();returndependencyisnull?default:dependency.Value;}publicstaticIEnumerable<TService>GetServices<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{foreach(vardependencyinserviceProvider.GetServices<IDependency<TKey,TService>>()){yieldreturndependency.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.

              publicstaticclassKeyedType{publicstaticTypeCreate(Typekey,Typetype)=>newTypeWithKey(key,type);publicstaticTypeCreate<TKey,TType>()whereTType:notnull=>newTypeWithKey(typeof(TKey),typeof(TType));privatesealedclassTypeWithKey:TypeDelegator{privatereadonlyinthashCode;publicTypeWithKey(TypekeyType,TypecustomType):base(customType)=>hashCode=HashCode.Combine(typeImpl,keyType);publicoverrideintGetHashCode()=>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:

              publicsealedclassDependency<TKey,TService>:IDependency<TKey,TService>whereTService:notnull{privatereadonlyIServiceProviderserviceProvider;publicDependency(IServiceProviderserviceProvider)=>this.serviceProvider=serviceProvider;publicTServiceValue=>(TService)serviceProvider.GetRequiredService(KeyedType.Create<TKey,TService>());objectIDependency.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:

              publicvoidConfigureServices(IServiceCollectionservices){services.AddTransient(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));services.AddTransient<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureUnity(IUnityContainercontainer){container.RegisterType(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));container.RegisterType<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureAutofac(ContainerBuilderbuilder){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:

              varlongForm=serviceProvider.GetRequiredService<IDependency<Key.Thing1,IThing>>().Value;varshortForm=serviceProvider.GetRequiredService<Key.Thing1,IThing>();

              The following extension methods will be added to ServiceCollectionDescriptorExtensions to provide common registration through IServiceCollection for all container frameworks:

              publicstaticclassServiceCollectionExtensions{publicstaticIServiceCollectionAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddEnumerable<TKey,TService,TImplementation>(thisIServiceCollectionservices,ServiceLifetimelifetime)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddEnumerable(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType,ServiceLifetimelifetime);}

              API Usage

              Putting it all together, here's how the API can be leveraged for any container framework that supports registration through IServiceCollection.

              publicinterfaceIThing{stringToString();}publicabstractclassThingBase:IThing{protectedThingBase(){}publicoverridestringToString()=>GetType().Name;}publicsealedclassThing:ThingBase{}publicsealedclassKeyedThing:ThingBase{}publicsealedclassThing1:ThingBase{}publicsealedclassThing2:ThingBase{}publicsealedclassThing3:ThingBase{}publicstaticclassKey{publicsealedclassThingies{}publicsealedclassThing1{}publicsealedclassThing2{}}publicclassCatInTheHat{privatereadonlyIDependency<Key.Thing1,IThing>thing1;privatereadonlyIDependency<Key.Thing2,IThing>thing2;publicCatInTheHat(IDependency<Key.Thing1,IThing>thing1,IDependency<Key.Thing2,IThing>thing2){this.thing1=thing1;this.thing2=thing2;}publicIThingThing1=>thing1.Value;publicIThingThing2=>thing2.Value;}publicvoidConfigureServices(IServiceCollectioncollection){// keyed typesservices.AddSingleton<Key.Thing1,IThing,Thing1>();services.AddTransient<Key.Thing2,IThing,Thing2>();// non-keyed type with keyed type dependenciesservices.AddSingleton<CatInTheHat>();// keyed open genericsservices.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);varprovider=services.BuildServiceProvider();// resolve non-keyed type with keyed type dependenciesvarcatInTheHat=provider.GetRequiredService<CatInTheHat>();// resolve keyed, open genericvaropenGeneric=provider.GetRequiredService<Key.Thingy,IGeneric<object>>();// resolve keyed IEnumerable<T>varthingies=provider.GetServices<Key.Thingies,IThing>();// related services such as IServiceProviderIsService// new extension methods could be added to make this more succinctvarquery=provider.GetRequiredService<IServiceProviderIsService>();varthing1Registered=query.IsService(typeof(IDependency<Key.Thing1,IThing>));varthing2Registered=query.IsService(typeof(IDependency<Key.Thing2,IThing>));}

              Container Integration

              The following is a summary of results from Keyed Service POC repo.

              ContainerBy KeyBy Key
              (Generic)
              Many
              By Key
              Many By
              Key (Generic)
              Open
              Generics
              Existing
              Instance
              Implementation
              Factory
              Default
              Autofac
              DryIoc
              Grace
              Lamar
              LightInject
              Stashbox
              StructureMap
              Unity
              ContainerJust
              Works
              No Container
              Changes
              No Adapter
              Changes
              Default
              Autofac
              DryIoc
              Grace11
              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

              1. Thought experiment
              2. Initial proof of concept
              3. Practical API with a lot of ceremony removed

              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:

              usingstaticSystem.AttributeTargets;[AttributeUsage(Class|Interface|Parameter,AllowMultiple=false,Inherited=false)]publicsealedclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(stringkey)=>Key=key;publicstringKey{get;}}

              This attribute could be used in the following ways:

              [ServiceKey("Bar")]publicinterfaceIFoo{}7[ServiceKey("Foo")]publicclassFoo{}publicclassBar{publicBar([ServiceKey("Bar")]IFoofoo){}}

              Using an attribute has to main advantages:

              1. There needs to be a way to specify the key at the call site when a dependency is injected
              2. 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:

              publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,stringkey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

              It is not required for this proposal to work, but as an optimization, it may be worth adding:

              publicinterfaceIKeyedServiceProvider:IServiceProvider{object?GetService(TypeserviceType,stringkey);}

              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:

              publicstring?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:

              publicinterfaceIKeyedTypeFactory{TypeCreate(Typetype,stringkey);}

              Microsoft.Extensions.DependencyInjection will provide a default implementation that leverages CustomReflectionContext.

              The implementation might look like the following:

              publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey){varprovider=serviceProviderasIKeyedServiceProvider??serviceProvider.GetService<IKeyServiceProvider>();if(provider!=null){returnprovider.GetService(serviceType,key);}varfactory=serviceProvider.GetService<IKeyedTypeFactory>()??KeyedTypeFactory.Default;returnserviceProvider.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:

              classTeam{publicTeam([ServiceKey("A-Team")]IPityTheFoofoo){}// ← MrT is injected}// ...varservices=newServiceCollection();// Microsoft.Extensions.DependencyInjection.Abstractionsservices.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"));varprovider=services.BuildServiceProvider();varfoo=provider.GetRequiredService<IPityTheFoo>("A-Team");varteam=provider.GetRequiredService<Team>();varthingies=provider.GetServices<IThing>("Thingies");// related services such as IServiceProviderIsServicevarquery=provider.GetRequiredService<IServiceProviderIsService>();varshorthand=query.IsService<IPityTheFoo>("A-Team");varfactory=provider.GetRequiredService<IKeyedTypeService>();varlonghand=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.DependencyInjection would require one of the following:
                1. A dependency on System.Reflection.Context (unless another solution is found)
                2. An new, separate library that that references System.Reflection.Context and adds the keyed service capability
              • There is a potential explosion of overloads and/or extension methods
                • The requirement that these exist can be mitigated via the IKeyedServiceProvider and/or IKeyedTypeFactory intefaces
                  • The developer experience is less than ideal, but no functionality is lost

              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.

              publicclassServiceDescriptor{[...]/// <summary>/// Get the key of the service, if applicable./// </summary>publicobject?ServiceKey{get;}[...]/// <summary>/// Gets the instance that implements the service./// </summary>publicobject?KeyedImplementationInstance{get;}/// <summary>/// Gets the <see cref="Type"/> that implements the service./// </summary>publicSystem.Type?KeyedImplementationType{get;}/// <summary>/// Gets the factory used for creating Keyed service instances./// </summary>publicFunc<IServiceProvider,object,object>?KeyedImplementationFactory{get;}[...]/// <summary>/// Returns true if a ServiceKey was provided./// </summary> publicboolIsKeyedService=>ServiceKey!=null;}

              ServiceKey will stay null in non-keyed services.

              Extension methods for IServiceCollection are added to support keyed services:

              publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedScoped<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,objectimplementationInstance);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,TServiceimplementationInstance)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectioncollection,objectserviceKey,TServiceinstance)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticIServiceCollectionRemoveAllKeyed(thisIServiceCollectioncollection,TypeserviceType,objectserviceKey);publicstaticIServiceCollectionRemoveAllKeyed<T>(thisIServiceCollectioncollection,objectserviceKey);

              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 providers1=provider.GetKeyedService<IService>("other-service");// returns otherServices1=provider.GetKeyedService<IService>("another-random-key");// returns defaultService

              Resolving service

              Basic keyed resolution

              Two new optional interfaces will be introduced:

              namespaceMicrosoft.Extensions.DependencyInjection;publicinterfaceISupportKeyedService{object?GetKeyedService(TypeserviceType,objectserviceKey);objectGetRequiredKeyedService(TypeserviceType,objectserviceKey);}publicinterfaceIServiceProviderIsServiceKeyed{boolIsService(TypeserviceType,objectserviceKey);}

              This new interface will be accessible via the following extension methods:

              publicstaticIEnumerable<object?>GetKeyedServices(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticIEnumerable<T>GetKeyedServices<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticT?GetKeyedService<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticobjectGetRequiredKeyedService(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticTGetRequiredKeyedService<T>(thisIServiceProviderprovider,objectserviceKey)whereT: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:

              namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(){}}classService{privatereadonlystring_id;publicService([ServiceKey]stringid)=>_id=id;}serviceCollection.AddKeyedSingleton<Service>("some-service");[...]// build the providervar 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:

              namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassFromKeyedServicesAttribute:Attribute{publicFromKeyedServicesAttribute(objectkey){}publicobjectKey{get;}}classOtherService{publicOtherService([FromKeyedServices("service1")]IServiceservice1,[FromKeyedServices("service2")]IServiceservice2){Service1=service1;Service2=service2;}}

              Open generics

              Open generics are supported:

              serviceCollection.AddTransient(typeof(IGenericInterface<>),"my-service",typeof(GenericService<>));[...]// build the providervar 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 providerservices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB and MyServiceC

              Note 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 providerkeyedServices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB but NOT MyServiceCservices=provider.GetServices<IMyService>();// only returns MyServiceC

              But we do not support:

              serviceCollection.AddKeyedSingleton<MyServiceA>("some-service");serviceCollection.AddKeyedSingleton<MyServiceB>("some-service");serviceCollection.AddKeyedSingleton<MyServiceC>("some-service");[...]// build the providerservices=provider.GetKeyedServices("some-service");// Not supported

              Metadata

              Metadata

              Assignees

              Labels

              api-approvedAPI was approved in API review, it can be implementedarea-Extensions-DependencyInjectionblockingMarks issues that we want to fast track in order to unblock other important work

              Type

              No type

              Projects

              No projects

                Milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                [API Proposal]: Add Keyed Services Support to Dependency Injection #64427

                Description

                @commonsensesoftware

                Thanks @commonsensesoftware for the original proposal. I edited this post to show the current proposition.

                Original proposal from @commonsensesoftware ### Background and Motivation

                I'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)publicinterfaceIDependency{objectValue{get;}}publicinterfaceIDependency<inTKey,outTService>:IDependencywhereTService:notnull{newTServiceValue{get;}}

                The following extension methods will be added to ServiceProviderServiceExtensions:

                publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,Typekey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,Typekey)whereT:notnull;publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

                Here is a partial example of how it would be implemented:

                publicstaticclassServiceProviderExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,Typekey){varkeyedType=typeof(IDependency<,>).MakeGenericType(key,serviceType);vardependency=(IDependency?)serviceProvider.GetService(keyedType);returndependency?.Value;}publicstaticTService?GetService<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{vardependency=serviceProvider.GetService<IDependency<TKey,TService>>();returndependencyisnull?default:dependency.Value;}publicstaticIEnumerable<TService>GetServices<TKey,TService>(thisIServiceProviderserviceProvider)whereTService:notnull{foreach(vardependencyinserviceProvider.GetServices<IDependency<TKey,TService>>()){yieldreturndependency.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.

                publicstaticclassKeyedType{publicstaticTypeCreate(Typekey,Typetype)=>newTypeWithKey(key,type);publicstaticTypeCreate<TKey,TType>()whereTType:notnull=>newTypeWithKey(typeof(TKey),typeof(TType));privatesealedclassTypeWithKey:TypeDelegator{privatereadonlyinthashCode;publicTypeWithKey(TypekeyType,TypecustomType):base(customType)=>hashCode=HashCode.Combine(typeImpl,keyType);publicoverrideintGetHashCode()=>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:

                publicsealedclassDependency<TKey,TService>:IDependency<TKey,TService>whereTService:notnull{privatereadonlyIServiceProviderserviceProvider;publicDependency(IServiceProviderserviceProvider)=>this.serviceProvider=serviceProvider;publicTServiceValue=>(TService)serviceProvider.GetRequiredService(KeyedType.Create<TKey,TService>());objectIDependency.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:

                publicvoidConfigureServices(IServiceCollectionservices){services.AddTransient(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));services.AddTransient<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureUnity(IUnityContainercontainer){container.RegisterType(KeyedType.Create<Key.Thing1,IThing>(),typeof(Thing1));container.RegisterType<IDependency<Key.Thing1,IThing>,Dependency<Key.Thing1,IThing>>();}publicvoidConfigureAutofac(ContainerBuilderbuilder){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:

                varlongForm=serviceProvider.GetRequiredService<IDependency<Key.Thing1,IThing>>().Value;varshortForm=serviceProvider.GetRequiredService<Key.Thing1,IThing>();

                The following extension methods will be added to ServiceCollectionDescriptorExtensions to provide common registration through IServiceCollection for all container frameworks:

                publicstaticclassServiceCollectionExtensions{publicstaticIServiceCollectionAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddSingleton<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddSingleton(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddTransient<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddTransient(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddScoped<TKey,TService,TImplementation>(thisIServiceCollectionservices)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddScoped(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType);publicstaticIServiceCollectionTryAddEnumerable<TKey,TService,TImplementation>(thisIServiceCollectionservices,ServiceLifetimelifetime)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionTryAddEnumerable(thisIServiceCollectionservices,TypekeyType,TypeserviceType,TypeimplementationType,ServiceLifetimelifetime);}

                API Usage

                Putting it all together, here's how the API can be leveraged for any container framework that supports registration through IServiceCollection.

                publicinterfaceIThing{stringToString();}publicabstractclassThingBase:IThing{protectedThingBase(){}publicoverridestringToString()=>GetType().Name;}publicsealedclassThing:ThingBase{}publicsealedclassKeyedThing:ThingBase{}publicsealedclassThing1:ThingBase{}publicsealedclassThing2:ThingBase{}publicsealedclassThing3:ThingBase{}publicstaticclassKey{publicsealedclassThingies{}publicsealedclassThing1{}publicsealedclassThing2{}}publicclassCatInTheHat{privatereadonlyIDependency<Key.Thing1,IThing>thing1;privatereadonlyIDependency<Key.Thing2,IThing>thing2;publicCatInTheHat(IDependency<Key.Thing1,IThing>thing1,IDependency<Key.Thing2,IThing>thing2){this.thing1=thing1;this.thing2=thing2;}publicIThingThing1=>thing1.Value;publicIThingThing2=>thing2.Value;}publicvoidConfigureServices(IServiceCollectioncollection){// keyed typesservices.AddSingleton<Key.Thing1,IThing,Thing1>();services.AddTransient<Key.Thing2,IThing,Thing2>();// non-keyed type with keyed type dependenciesservices.AddSingleton<CatInTheHat>();// keyed open genericsservices.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);varprovider=services.BuildServiceProvider();// resolve non-keyed type with keyed type dependenciesvarcatInTheHat=provider.GetRequiredService<CatInTheHat>();// resolve keyed, open genericvaropenGeneric=provider.GetRequiredService<Key.Thingy,IGeneric<object>>();// resolve keyed IEnumerable<T>varthingies=provider.GetServices<Key.Thingies,IThing>();// related services such as IServiceProviderIsService// new extension methods could be added to make this more succinctvarquery=provider.GetRequiredService<IServiceProviderIsService>();varthing1Registered=query.IsService(typeof(IDependency<Key.Thing1,IThing>));varthing2Registered=query.IsService(typeof(IDependency<Key.Thing2,IThing>));}

                Container Integration

                The following is a summary of results from Keyed Service POC repo.

                ContainerBy KeyBy Key
                (Generic)
                Many
                By Key
                Many By
                Key (Generic)
                Open
                Generics
                Existing
                Instance
                Implementation
                Factory
                Default
                Autofac
                DryIoc
                Grace
                Lamar
                LightInject
                Stashbox
                StructureMap
                Unity
                ContainerJust
                Works
                No Container
                Changes
                No Adapter
                Changes
                Default
                Autofac
                DryIoc
                Grace11
                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

                1. Thought experiment
                2. Initial proof of concept
                3. Practical API with a lot of ceremony removed

                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:

                usingstaticSystem.AttributeTargets;[AttributeUsage(Class|Interface|Parameter,AllowMultiple=false,Inherited=false)]publicsealedclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(stringkey)=>Key=key;publicstringKey{get;}}

                This attribute could be used in the following ways:

                [ServiceKey("Bar")]publicinterfaceIFoo{}7[ServiceKey("Foo")]publicclassFoo{}publicclassBar{publicBar([ServiceKey("Bar")]IFoofoo){}}

                Using an attribute has to main advantages:

                1. There needs to be a way to specify the key at the call site when a dependency is injected
                2. 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:

                publicstaticclassServiceProviderServiceExtensions{publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticIEnumerable<object>GetServices(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticT?GetService<T>(thisIServiceProviderserviceProvider,stringkey);publicstaticobjectGetRequiredService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey);publicstaticTGetRequiredService<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;publicstaticIEnumerable<T>GetServices<T>(thisIServiceProviderserviceProvider,stringkey)whereT:notnull;}

                It is not required for this proposal to work, but as an optimization, it may be worth adding:

                publicinterfaceIKeyedServiceProvider:IServiceProvider{object?GetService(TypeserviceType,stringkey);}

                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:

                publicstring?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:

                publicinterfaceIKeyedTypeFactory{TypeCreate(Typetype,stringkey);}

                Microsoft.Extensions.DependencyInjection will provide a default implementation that leverages CustomReflectionContext.

                The implementation might look like the following:

                publicstaticobject?GetService(thisIServiceProviderserviceProvider,TypeserviceType,stringkey){varprovider=serviceProviderasIKeyedServiceProvider??serviceProvider.GetService<IKeyServiceProvider>();if(provider!=null){returnprovider.GetService(serviceType,key);}varfactory=serviceProvider.GetService<IKeyedTypeFactory>()??KeyedTypeFactory.Default;returnserviceProvider.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:

                classTeam{publicTeam([ServiceKey("A-Team")]IPityTheFoofoo){}// ← MrT is injected}// ...varservices=newServiceCollection();// Microsoft.Extensions.DependencyInjection.Abstractionsservices.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"));varprovider=services.BuildServiceProvider();varfoo=provider.GetRequiredService<IPityTheFoo>("A-Team");varteam=provider.GetRequiredService<Team>();varthingies=provider.GetServices<IThing>("Thingies");// related services such as IServiceProviderIsServicevarquery=provider.GetRequiredService<IServiceProviderIsService>();varshorthand=query.IsService<IPityTheFoo>("A-Team");varfactory=provider.GetRequiredService<IKeyedTypeService>();varlonghand=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.DependencyInjection would require one of the following:
                  1. A dependency on System.Reflection.Context (unless another solution is found)
                  2. An new, separate library that that references System.Reflection.Context and adds the keyed service capability
                • There is a potential explosion of overloads and/or extension methods
                  • The requirement that these exist can be mitigated via the IKeyedServiceProvider and/or IKeyedTypeFactory intefaces
                    • The developer experience is less than ideal, but no functionality is lost

                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.

                publicclassServiceDescriptor{[...]/// <summary>/// Get the key of the service, if applicable./// </summary>publicobject?ServiceKey{get;}[...]/// <summary>/// Gets the instance that implements the service./// </summary>publicobject?KeyedImplementationInstance{get;}/// <summary>/// Gets the <see cref="Type"/> that implements the service./// </summary>publicSystem.Type?KeyedImplementationType{get;}/// <summary>/// Gets the factory used for creating Keyed service instances./// </summary>publicFunc<IServiceProvider,object,object>?KeyedImplementationFactory{get;}[...]/// <summary>/// Returns true if a ServiceKey was provided./// </summary> publicboolIsKeyedService=>ServiceKey!=null;}

                ServiceKey will stay null in non-keyed services.

                Extension methods for IServiceCollection are added to support keyed services:

                publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedScoped(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedScoped<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,objectimplementationInstance);publicstaticIServiceCollectionAddKeyedSingleton(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,TServiceimplementationInstance)whereTService:class;publicstaticIServiceCollectionAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedSingleton<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeserviceType,objectserviceKey);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory);publicstaticIServiceCollectionAddKeyedTransient(thisIServiceCollectionservices,TypeserviceType,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType);publicstaticIServiceCollectionAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectionservices,objectserviceKey)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class;publicstaticIServiceCollectionAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectionservices,objectserviceKey)whereTService:classwhereTImplementation:class,TService;publicstaticIServiceCollectionAddKeyedTransient<TService,TImplementation>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TImplementation>implementationFactory)whereTService:classwhereTImplementation:class,TService;publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedScoped(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedScoped<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedSingleton(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService>(thisIServiceCollectioncollection,objectserviceKey,TServiceinstance)whereTService:class{}publicstaticvoidTryAddKeyedSingleton<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]Typeservice,objectserviceKey){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,Func<IServiceProvider,object,object>implementationFactory){}publicstaticvoidTryAddKeyedTransient(thisIServiceCollectioncollection,Typeservice,objectserviceKey,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TypeimplementationType){}publicstaticvoidTryAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TService>(thisIServiceCollectioncollection,objectserviceKey)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService>(thisIServiceCollectionservices,objectserviceKey,Func<IServiceProvider,object,TService>implementationFactory)whereTService:class{}publicstaticvoidTryAddKeyedTransient<TService,[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)]TImplementation>(thisIServiceCollectioncollection,objectserviceKey)whereTService:classwhereTImplementation:class,TService{}publicstaticIServiceCollectionRemoveAllKeyed(thisIServiceCollectioncollection,TypeserviceType,objectserviceKey);publicstaticIServiceCollectionRemoveAllKeyed<T>(thisIServiceCollectioncollection,objectserviceKey);

                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 providers1=provider.GetKeyedService<IService>("other-service");// returns otherServices1=provider.GetKeyedService<IService>("another-random-key");// returns defaultService

                Resolving service

                Basic keyed resolution

                Two new optional interfaces will be introduced:

                namespaceMicrosoft.Extensions.DependencyInjection;publicinterfaceISupportKeyedService{object?GetKeyedService(TypeserviceType,objectserviceKey);objectGetRequiredKeyedService(TypeserviceType,objectserviceKey);}publicinterfaceIServiceProviderIsServiceKeyed{boolIsService(TypeserviceType,objectserviceKey);}

                This new interface will be accessible via the following extension methods:

                publicstaticIEnumerable<object?>GetKeyedServices(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticIEnumerable<T>GetKeyedServices<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticT?GetKeyedService<T>(thisIServiceProviderprovider,objectserviceKey);publicstaticobjectGetRequiredKeyedService(thisIServiceProviderprovider,TypeserviceType,objectserviceKey);publicstaticTGetRequiredKeyedService<T>(thisIServiceProviderprovider,objectserviceKey)whereT: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:

                namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassServiceKeyAttribute:Attribute{publicServiceKeyAttribute(){}}classService{privatereadonlystring_id;publicService([ServiceKey]stringid)=>_id=id;}serviceCollection.AddKeyedSingleton<Service>("some-service");[...]// build the providervar 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:

                namespaceMicrosoft.Extensions.DependencyInjection;[AttributeUsageAttribute(AttributeTargets.Parameter)]publicclassFromKeyedServicesAttribute:Attribute{publicFromKeyedServicesAttribute(objectkey){}publicobjectKey{get;}}classOtherService{publicOtherService([FromKeyedServices("service1")]IServiceservice1,[FromKeyedServices("service2")]IServiceservice2){Service1=service1;Service2=service2;}}

                Open generics

                Open generics are supported:

                serviceCollection.AddTransient(typeof(IGenericInterface<>),"my-service",typeof(GenericService<>));[...]// build the providervar 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 providerservices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB and MyServiceC

                Note 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 providerkeyedServices=provider.GetKeyedServices<IMyService>("some-service");// returns an instance of MyServiceA, MyServiceB but NOT MyServiceCservices=provider.GetServices<IMyService>();// only returns MyServiceC

                But we do not support:

                serviceCollection.AddKeyedSingleton<MyServiceA>("some-service");serviceCollection.AddKeyedSingleton<MyServiceB>("some-service");serviceCollection.AddKeyedSingleton<MyServiceC>("some-service");[...]// build the providerservices=provider.GetKeyedServices("some-service");// Not supported

                Metadata

                Metadata

                Assignees

                Labels

                api-approvedAPI was approved in API review, it can be implementedarea-Extensions-DependencyInjectionblockingMarks issues that we want to fast track in order to unblock other important work

                Type

                No type

                Projects

                No projects

                  Milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions