dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.35k stars 4.74k forks source link

[API Proposal]: IKeyedServiceProvider Have a method to get all keys for a ServiceType, an option to make the keys services unique and the option to getservices return all keyed services #91466

Open luizfbicalho opened 1 year ago

luizfbicalho commented 1 year ago

Background and motivation

I have some configurations in my process that I create for each step one service and store it in a EF class


    public class Step
    {
        public int Id { get; set; }
        public string StepName { get; set; }
        public string StepKeyService { get; set; }

        public IService1 GetService(IServiceProvider provider) {

            return provider.GetRequiredKeyedService<IService>(StepKeyService);
        }
    }

But to make the Editor for the steps I need to list all of the Keys that are registered in the ServiceProvider

One second utility would be to Make the Service Type and Key marked as unique

col.SetKeyedServiceUnique<IService>()

this way if I add two services I would get a validation exeption

            col.AddKeyedTransient<IService, ServiceA>(KeyedService.AnyKey);
            col.AddKeyedTransient<IService, ServiceB>(KeyedService.AnyKey);

and the third question is to have a method provider.GetAllServices() that return all services from all keys that could be a keyed dictionary if it would help organize

API Proposal


public static class  ServiceProviderExtensions
{
     // prevent this keyed service to have more than one implementation
     public void SetKeyedServiceUnique<T>(this IServiceCollection collection);
     // get all services from all keys
     public IEnumerable<T> GetAllServices<T>(this IKeyedServiceProvider provider);
     // get all services and all keys
     public IDictionary<object?,IEnumerable<T>> GetAllServicesDictionary<T>(this IKeyedServiceProvider provider);
     // get all keys
     public IEnumerable<object?> GetAllKeys<T>(this IKeyedServiceProvider provider);

}

API Usage

     [ApiController]
    public class StepController : ControllerBase

    {
        public StepController(IServiceProvider provider)
        {
            Provider = provider;
        }

        public IServiceProvider Provider { get; }

        public ActionResult<IEnumerable<object?>> GetStepKeys()
        {
            return Ok(Provider.GetAllKeys<IService>();
        }
    }   

Alternative Designs

I'm open to any alternative that could result in this functionalities

Risks

I can't imagine risks because there are new methods to implement this.

ghost commented 1 year ago

Tagging subscribers to this area: @dotnet/area-extensions-dependencyinjection See info in area-owners.md if you want to be subscribed.

Issue Details
### Background and motivation I have some configurations in my process that I create for each step one service and store it in a EF class ```C# public class Step { public int Id { get; set; } public string StepName { get; set; } public string StepKeyService { get; set; } public IService1 GetService(IServiceProvider provider) { return provider.GetRequiredKeyedService(StepKeyService); } } ``` But to make the Editor for the steps I need to list all of the Keys that are registered in the ServiceProvider One second utility would be to Make the Service Type and Key marked as unique ```c# col.SetKeyedServiceUnique() ``` this way if I add two services I would get a validation exeption ```C# col.AddKeyedTransient(KeyedService.AnyKey); col.AddKeyedTransient(KeyedService.AnyKey); ``` and the third question is to have a method provider.GetAllServices() that return all services from all keys that could be a keyed dictionary if it would help organize ### API Proposal ```csharp public static class ServiceProviderExtensions { // prevent this keyed service to have more than one implementation public void SetKeyedServiceUnique(this IServiceCollection collection); // get all services from all keys public IEnumerable GetAllServices(this IKeyedServiceProvider provider); // get all services and all keys public IDictionary> GetAllServicesDictionary(this IKeyedServiceProvider provider); // get all keys public IEnumerable GetAllKeys(this IKeyedServiceProvider provider); } ``` ### API Usage ```csharp [ApiController] public class StepController : ControllerBase { public StepController(IServiceProvider provider) { Provider = provider; } public IServiceProvider Provider { get; } public ActionResult> GetStepKeys() { return Ok(Provider.GetAllKeys(); } } ``` ### Alternative Designs I'm open to any alternative that could result in this functionalities ### Risks I can't imagine risks because there are new methods to implement this.
Author: luizfbicalho
Assignees: -
Labels: `api-suggestion`, `area-Extensions-DependencyInjection`
Milestone: -
Cassinotte commented 1 year ago

I would also like an alternative to the question

steveharter commented 1 year ago

@benjaminpetit any thoughts on the keyed-service ask for make the Service Type and Key unique along with validation?

steveharter commented 1 year ago

But to make the Editor for the steps I need to list all of the Keys that are registered in the ServiceProvider

For this scenario, is it possible to enumerate the ServiceCollection instead which can return the ServiceDescriptors?

Also since there can be several implementations of IServiceProvider\IKeyedServiceProvider, that has to be accounted for in any extension methods -- i.e. they need to be implemented in terms of types exposed in the Microsoft.Extensions.DependencyInjection.Abstractions assembly, not the types in Microsoft.Extensions.DependencyInjection for example. This likely means we'd have to add new interfaces as well to expose the keys.

luizfbicalho commented 1 year ago

But to make the Editor for the steps I need to list all of the Keys that are registered in the ServiceProvider

For this scenario, is it possible to enumerate the ServiceCollection instead which can return the ServiceDescriptors?

Also since there can be several implementations of IServiceProvider\IKeyedServiceProvider, that has to be accounted for in any extension methods -- i.e. they need to be implemented in terms of types exposed in the Microsoft.Extensions.DependencyInjection.Abstractions assembly, not the types in Microsoft.Extensions.DependencyInjection for example. This likely means we'd have to add new interfaces as well to expose the keys.

How can i get the service colector from the service provider?

benjaminpetit commented 1 year ago

@benjaminpetit any thoughts on the keyed-service ask for make the Service Type and Key unique along with validation?

If we add this, then we should add the same method for non-keyed DI in my opinion.

I think for this and the listing of all registered key, you could implement your own IServiceProviderFactory that will validate that you don't have two or more services registered with the same key, and build a list of available keys. You can then insert this list as a new service to the IServiceCollection before passing it to the "real" IServiceProviderFactory` implementation.

Does that make sense?

luizfbicalho commented 1 year ago

If we add this, then we should add the same method for non-keyed DI in my opinion.

Would it be useful to add something like this

serviceCollection.AddScoped<IBar, Bar>().AsUnique();

Or add some more validation with a func

serviceCollection.AddScoped<IBar, Bar>().WithValidation(collection=>VerifySomething(collection)); Or even mark some types as required, this could be even used with GetRequiredService to validate.

serviceCollection.Mark<IBar>().AsRequired();

benjaminpetit commented 1 year ago

ServiceProviderOptions.ValidateOnBuild should already allow you to check that your services are instantiable.

Otherwise, I think everything you want to do is doable with some kind of decorator around IServiceProviderFactory

luizfbicalho commented 1 year ago

Can you provide any example like this? How to validate with the factory

I'm interested in the @steveharter idea of get the service collection from the service provider

benjaminpetit commented 1 year ago

Here is what I have in mind: https://gist.github.com/benjaminpetit/ba60099a99f6cb315074c373b06d9d32

I implemented a custom service provider factory in CustomServiceProviderFactory. I tell the HostBuilder to use it instead of the default one.

AsUnique() extension change the service type of the descriptor to a type that implement IServiceMarker. When the host will build the service provider, it will call CustomServiceProviderFactory.CreateServiceProvider that can iterate through all the service descriptors. When it sees a service that implement IServiceMarker, it will call the method Validate, then "unwrap" the descriptor to use the correct service type.

When iterating though the descriptors, you could build a dictionary of keyed services too, for example.

With the same logic you could implement anything you wanted; I think.

EDIT: I think we could do much simpler without this generic descriptor wrapping/unwrapping thing.

benjaminpetit commented 1 year ago

Here is an example on how to generate a dictionary of all keyed services: https://gist.github.com/benjaminpetit/468741882f1ad6e4ec8dad761103a87d

luizfbicalho commented 1 year ago

Thanks a lot @benjaminpetit , I'll try to transform this in a usefull library

Maybe some of it can go into this future projetc

stephentoub commented 11 months ago

I just hit this hard as well. I needed to be able to get all of the instances for a particular service type, regardless of key (I didn't actually need the keys), and I thought GetKeyedServices(KeyedService.AnyKey) would work, but it doesn't, as the key matching doesn't factor AnyKey in for the key being searched. This is an unfortunate gap.

thomaslevesque commented 11 months ago

I was going to use keyed services for the first time, and also hit this limitation. I can work around it, but this gap should be closed IMO.

davhdavh commented 11 months ago

IMHO, this is more of a bug. Documentation for KeyedService.AnyKey specifically says:

Represents a key that matches any key.

So GetKeyedServices<T>(KeyedService.AnyKey) should most definitely return an enumeration of all T implementations. Similar, col.AddKeyedTransient<IService, ServiceA>(KeyedService.AnyKey); should give an error. If you want to have a fallback, null should be used.

robertmclaws commented 4 months ago

Hey folks! Just wanted to find out is this was planning on getting fixed at all? There shouldn't need to be hacks to make this work, since KeyedService.AnyKey exists and should be implemented. Thanks!

0xced commented 4 months ago

There's a discussion going on at #100105 (which is a duplicate of this issue).

luizfbicalho commented 4 months ago

There's a discussion going on at #100105 (which is a duplicate of this issue).

I don't think that all of the features here are in the #100105 issue

michael-hawker commented 2 weeks ago

Yeah, I have a scenario where I need to register services to get pulled specifically for different use cases to load specific types of data, but also need to gather them all (thus the common interface) to initialize them beforehand.

I got excited by the new keyed services as it seemed built for this scenario, but then got stuck in the last step after changing my code in trying to get all the services. I was hoping I could just call GetKeyServices<T>() to get them all, I didn't even find the docs on KeyedService.AnyKey, but then that seemed great too (as called out above)... but then that also didn't work. So, I think I'm back to being stuck without hacky workarounds...

Edit: Oh, just saw the KeyedService.AnyKey was fixed in .NET 9 https://github.com/dotnet/runtime/issues/109016 🎉🎉🎉, hopefully it'll get backported too. (Just a note for anyone here, you have to ensure to update your NuGet package to the latest .NET 9 version too, not just your TFM.)

julealgon commented 2 weeks ago

@michael-hawker

Edit: Oh, just saw the KeyedService.AnyKey was fixed in .NET 9 #109016 🎉🎉🎉, hopefully it'll get backported too. (Just a note for anyone here, you have to ensure to update your NuGet package to the latest .NET 9 version too, not just your TFM.)

I don't think you even need to update your TFM. Isn't this a behavior of the library itself, Microsoft.Extensions.DependencyInjection? Upgrading the library to v9 will be enough even if you are still using .NET 8, since the fix doesn't rely on any #if checks against a specific TFM.

michael-hawker commented 2 weeks ago

@julealgon indeed, thanks! It was late at night, so I had bumped the TFM first before realizing it was just a package dependency. 😅 Worked like a charm!