Replies: 2 comments 4 replies
-
What prevents you from using abstractions as keys instead? public interface ISampleService;
public interface IAlphaSampleService : ISampleService;
public interface IBravoSampleService : ISampleService;
...
services.AddScoped<IAlphaSampleService, AlphaSampleService>()
.AddScoped<IBravoSampleService, BravoSampleService>()
.AddScoped<ISampleService, DefaultSampleService>();
...
public sealed class Foo(IAlphaSampleService sampleService); IMHO I prefer to have compile time enforcement of my services. If you're truly set on using runtime keys instead, then you could always create a factory for resolving things and an extension method on public interface IKeyedServiceFactory
{
// just an example
TService GetService<TService>(object key);
}
...
public static TService GetKeyedService<TService>(this IServiceProvider source, object key)
{
var factory = source.GetRequiredService<IKeyedServiceFactory>();
return factory.GetService<TService>(key);
}
...
var service = serviceProvider.GetKeyedService<SampleService>("alpha"); Though, again, I don't recommend this and will leave further exploration to the reader. |
Beta Was this translation helpful? Give feedback.
-
I really miss keyed services. If you have an interface with multiple concrete implementations, and you want DB or config file driven selection of the implementation, they are really great. I can implement handlers for a dozen different payment vendors, and in a multi tenant system allow my clients to configure their payment vendor, and have the appropriate implementation light up. Done properly all the other code on each side of the pipeline leverages the interface, and the configuration drives the selection of the concrete implementation. They are really great when used properly. When you show another dev how easy it is with a bit of declarative config to switch implementations, and eliminate repetitive blocks of switch cases and redundant concrete classes, they begin to really appreciate this approach. Usually counter examples that don't understand how to implement this properly will hard code a string during the service resolution. That's the wrong way to leverage this approach. You would pass a string, but it wouldn't be hardcoded, it would be determined at runtime based on some dynamic or data driven process. HttpClient was another great use of them. You generally want a single HttpClient instead per integration. But you don't want one HttpClient for the entire app, because there's alot of things like timeout property that cannot be changed on HttpClient after the first request. If you have multiple integrations, then you want one HttpClient per integration, so the BaseUri, Timeout, etc. can be specific to the remote integration. Keyed service registration solved this problem cleanly. Creating a bunch of class declarations for what differs only by some configuration is unnecessary bloat, and results in the consuming code being overly coupled to what would have otherwise just been an HttpClient. They all share the same interface, there's no need for different types. For this use case, having attributes on constructor parameters that name the desired implementation made more sense, because you usually wanted the specifically configured HttpClient singleton for a particular section of code. The big difference here is there aren't multiple implementations, but instead there's a single implementation with multiple singleton instances. I.e. if I have a integration with Google API and one with OpenAI, then I need a singleton HttpClient for Google API, and another singleton HttpClient for OpenAI. I thought it was real interesting when we got the extension methods for .AddHttpClient that take a name, which is basically exposing them as keyed service outside of DI. I felt vindicated. It would have made more sense to just fix the DI though IMO. |
Beta Was this translation helpful? Give feedback.
-
With the current implementation of DI there is no easy way of configuring a keyed service. I see two ways of implementing keyed options:
Add
ConfigureKeyed<TOption>(object? Key, // ...)
:Or by passing the key into the constructor and use named configurations:
Personally I would prefer
ConfigureKeyed
function as it could provide multiple ways to configure as well as adding named configurations likeConfigureKeyed<Service>("ServiceA", "MyName", o => { // ... });
rather than useConfigure<Service>("ServiceA.MyName", o => { // ... });
.Additionally using both
ConfigureKeyed
andConfigure
could make default options.Beta Was this translation helpful? Give feedback.
All reactions