Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API Proposal]: Keyed services; alternative registration API for existing non-keyed services #109017

Open
mgravell opened this issue Oct 18, 2024 · 1 comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Extensions-DependencyInjection untriaged New issue has not been triaged by the area owner

Comments

@mgravell
Copy link
Member

mgravell commented Oct 18, 2024

Background and motivation

A lot of services exist that do not support keyed DI - they use TryAddSingleton etc internally, without any notion of keyed DI. Migrating those services to keyed-DI often requires the library-author to add additional keyed DI APIs for the purpose. This can also be a problem when we have a need for 2 instances of the same service to interact, such as decorator APIs - as discussed in this "extensions" topic

This is a proposal for an alternative API that works around this problem.

API Proposal

Consider an existing API of the form:

services.AddStackExchangeRedisCache(...);

This adds SE.Redis as an IDistributedCache implementation on the default key. There is no current API to add SE.Redis as a keyed service, and the relevant type is not directly exposed, making it impossible to register as a keyed service manually.

Now consider something along the lines of:

services.WithServiceKey("some key").AddStackExchangeRedisCache(...);

or

services.WithServiceKey("some key", keyed => keyed.AddStackExchangeRedisCache(...));

The key point here is that a decorator implementation of IServiceCollection is created which changes the Add method to add the specified key on non-keyed services. The 2 alternative suggestions have different ways of expressing the lifetime of the decorator; arguably the second version is clearer as to "here's the bits that are keyed", but I don't care to die on any hills.

A rough proposed implementation of the idea is shown below and kind-of works. There is a complication, however: if we take the AddStackExchangeRedisCache as an example, this may use multiple other services, and if done naively, they'd all end up "keyed". I genuinely don't know whether keyed services work transitively, i.e. if a service Foo is keyed "some key", and requires sub-service Bar without specifying a key, does it look for a keyed-service ("some key") Bar and then fall back to the non-keyed service Bar, or does it only look for the non-keyed Bar? I wonder whether it might be necessary to say "I want to configure a specific type with a key", i.e.

- services.WithServiceKey("some key", keyed => keyed.AddStackExchangeRedisCache(...));
+ services.WithServiceKey<IDistributedCache>("some key", keyed => keyed.AddStackExchangeRedisCache(...));

In this case, the decorator would only magically add the key for matching service types (presumably with an API to allow multiple types to be specified if required)


Rough implementation, for reference only, doesn't consider the "which service" complication:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection WithServiceKey(this IServiceCollection services, object key)
        => new KeyedServiceCollection(services, key);
}
internal sealed class KeyedServiceCollection(IServiceCollection services, object serviceKey) : IServiceCollection
{
    void ICollection<ServiceDescriptor>.Add(ServiceDescriptor item) => services.Add(WithKey(item));
    private ServiceDescriptor WithKey(ServiceDescriptor item)
    {
        if (!item.IsKeyedService)
        {
            if (item.ImplementationInstance is not null)
            {
                item = new(item.ServiceType, serviceKey, item.ImplementationInstance);
            }
            else if (item.ImplementationFactory is { } factory)
            {
                item = new(item.ServiceType, serviceKey, (provider, _) => factory(provider), item.Lifetime);
            }
            else if (item.ImplementationType is not null)
            {
                item = new(item.ServiceType, serviceKey, item.ImplementationType, item.Lifetime);
            }
            else
            {
                throw new NotSupportedException("Unable to map service to key");
            }
        }
        return item;
    }
    // other methods not shown, but simply forward via "services"
}

API Usage

(shown above)

Alternative Designs

The alternative is to lean on library authors to add keyed-DI methods to all of their registration methods.

Risks

No response

@mgravell mgravell added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Oct 18, 2024
@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Oct 18, 2024
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Extensions-DependencyInjection untriaged New issue has not been triaged by the area owner
Projects
None yet
Development

No branches or pull requests

1 participant