dotnet / runtime

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

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

Closed commonsensesoftware closed 1 year ago

commonsensesoftware commented 2 years ago

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](https://github.com/dotnet/extensions/issues/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`). 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](https://github.com/commonsensesoftware/keyed-services-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: ```c# // required to 'access' a keyed service via typeof(T) public interface IDependency { object Value { get; } } public interface IDependency : IDependency where TService : notnull { new TService Value { get; } } ``` The following extension methods will be added to `ServiceProviderServiceExtensions`: ```c# public static class ServiceProviderServiceExtensions { public static object? GetService(this IServiceProvider serviceProvider, Type serviceType, Type key); public static object GetRequiredService(this IServiceProvider serviceProvider, Type serviceType, Type key); public static IEnumerable GetServices(this IServiceProvider serviceProvider, Type serviceType, Type key); public static T? GetService(this IServiceProvider serviceProvider, Type key) where T : notnull; public static T GetRequiredService(this IServiceProvider serviceProvider, string key) where T : notnull; public static IEnumerable GetServices(this IServiceProvider serviceProvider, string key) where T : notnull; } ``` Here is a partial example of how it would be implemented: ```c# public static class ServiceProviderExtensions { public static object? GetService(this IServiceProvider serviceProvider, Type serviceType, Type key) { var keyedType = typeof(IDependency<,>).MakeGenericType(key, serviceType); var dependency = (IDependency?)serviceProvider.GetService(keyedType); return dependency?.Value; } public static TService? GetService(this IServiceProvider serviceProvider) where TService : notnull { var dependency = serviceProvider.GetService>(); return dependency is null ? default : dependency.Value; } public static IEnumerable GetServices(this IServiceProvider serviceProvider) where TService : notnull { foreach (var dependency in serviceProvider.GetServices>()) { yield return dependency.Value; } } } ``` **Registering Services** Now that we have a way to _resolve_ a keyed service, how do we register one? `Type` is already used as a key, but we need a way to create an arbitrary composite key. To achieve this, we'll perform a little trickery on the `Type` which **only** affects how it is mapped in a container; thus making it a _composite key_. It does **not** change the runtime behavior nor require special Reflection _magic_. We are effectively taking advantage of the knowledge that `Type` will be used as a key for service resolution in all container implementations. ```c# public static class KeyedType { public static Type Create(Type key, Type type) => new TypeWithKey(key,type); public static Type Create() where TType : notnull => new TypeWithKey(typeof(TKey), typeof(TType)); private sealed class TypeWithKey : TypeDelegator { private readonly int hashCode; public TypeWithKey(Type keyType, Type customType) : base(customType) => hashCode = HashCode.Combine(typeImpl, keyType); public override int GetHashCode() => hashCode; // remainder is minimal, but ommitted for brevity } } ``` This might look _magical_, but it's not. `Type` is already being used as a key when it's mapped in a container. `TypeWithKey` has all the appearance of the original type, but produces a different hash code when combined with another type. This affords for determinate, discrete unions of type registrations, which allows mapping the intended service multiple times. Container implementers are free to perform the registration however they like, but the generic, out-of-the-box implementation would look like: ```c# public sealed class Dependency : IDependency where TService : notnull { private readonly IServiceProvider serviceProvider; public Dependency(IServiceProvider serviceProvider) => this.serviceProvider = serviceProvider; public TService Value => (TService)serviceProvider.GetRequiredService(KeyedType.Create()); object IDependency.Value => Value; } ``` Container implementers _might_ provide their own extension methods to make registration more succinct, but it is not required. The following registrations would work today without any container implementation changes: ```c# public void ConfigureServices(IServiceCollection services) { services.AddTransient(KeyedType.Create(), typeof(Thing1)); services.AddTransient, Dependency>(); } public void ConfigureUnity(IUnityContainer container) { container.RegisterType(KeyedType.Create(), typeof(Thing1)); container.RegisterType, Dependency>(); } public void ConfigureAutofac(ContainerBuilder builder) { builder.RegisterType(typeof(Thing1)).As(KeyedType.Create()); builder.RegisterType>().As>(); } ``` There is a minor drawback of requiring two registrations per keyed service in the container, but resolution for consumers is succintly: ```c# var longForm = serviceProvider.GetRequiredService>().Value; var shortForm = serviceProvider.GetRequiredService(); ``` The following extension methods will be added to `ServiceCollectionDescriptorExtensions` to provide common registration through `IServiceCollection` for all container frameworks: ```c# public static class ServiceCollectionExtensions { public static IServiceCollection AddSingleton(this IServiceCollection services) where TService : class where TImplementation : class, TService; public static IServiceCollection AddSingleton( this IServiceCollection services, Type keyType, Type serviceType, Type implementationType); public static IServiceCollection TryAddSingleton(this IServiceCollection services) where TService : class where TImplementation : class, TService; public static IServiceCollection TryAddSingleton( this IServiceCollection services, Type keyType, Type serviceType, Type implementationType); public static IServiceCollection AddTransient(this IServiceCollection services) where TService : class where TImplementation : class, TService; public static IServiceCollection AddTransient( this IServiceCollection services, Type keyType, Type serviceType, Type implementationType); public static IServiceCollection TryAddTransient(this IServiceCollection services) where TService : class where TImplementation : class, TService; public static IServiceCollection TryAddTransient( this IServiceCollection services, Type keyType, Type serviceType, Type implementationType); public static IServiceCollection AddScoped(this IServiceCollection services) where TService : class where TImplementation : class, TService; public static IServiceCollection AddScoped( this IServiceCollection services, Type keyType, Type serviceType, Type implementationType); public static IServiceCollection TryAddScoped(this IServiceCollection services) where TService : class where TImplementation : class, TService; public static IServiceCollection TryAddScoped( this IServiceCollection services, Type keyType, Type serviceType, Type implementationType); public static IServiceCollection TryAddEnumerable( this IServiceCollection services, ServiceLifetime lifetime) where TService : class where TImplementation : class, TService; public static IServiceCollection TryAddEnumerable( this IServiceCollection services, Type keyType, Type serviceType, Type implementationType, ServiceLifetime lifetime); } ``` ## API Usage Putting it all together, here's how the API can be leveraged for any container framework that supports registration through `IServiceCollection`. ```c# public interface IThing { string ToString(); } public abstract class ThingBase : IThing { protected ThingBase() { } public override string ToString() => GetType().Name; } public sealed class Thing : ThingBase { } public sealed class KeyedThing : ThingBase { } public sealed class Thing1 : ThingBase { } public sealed class Thing2 : ThingBase { } public sealed class Thing3 : ThingBase { } public static class Key { public sealed class Thingies { } public sealed class Thing1 { } public sealed class Thing2 { } } public class CatInTheHat { private readonly IDependency thing1; private readonly IDependency thing2; public CatInTheHat( IDependency thing1, IDependency thing2) { this.thing1 = thing1; this.thing2 = thing2; } public IThing Thing1 => thing1.Value; public IThing Thing2 => thing2.Value; } public void ConfigureServices(IServiceCollection collection) { // keyed types services.AddSingleton(); services.AddTransient(); // non-keyed type with keyed type dependencies services.AddSingleton(); // keyed open generics services.AddTransient(typeof(IGeneric<>), typeof(Generic<>)); services.AddSingleton(typeof(IDependency<,>), typeof(GenericDependency<,>)); // keyed IEnumerable services.TryAddEnumerable(ServiceLifetime.Transient); services.TryAddEnumerable(ServiceLifetime.Transient); services.TryAddEnumerable(ServiceLifetime.Transient); var provider = services.BuildServiceProvider(); // resolve non-keyed type with keyed type dependencies var catInTheHat = provider.GetRequiredService(); // resolve keyed, open generic var openGeneric = provider.GetRequiredService>(); // resolve keyed IEnumerable var thingies = provider.GetServices(); // related services such as IServiceProviderIsService // new extension methods could be added to make this more succinct var query = provider.GetRequiredService(); var thing1Registered = query.IsService(typeof(IDependency)); var thing2Registered = query.IsService(typeof(IDependency)); } ``` ## Container Integration The following is a summary of results from [Keyed Service POC](https://github.com/commonsensesoftware/keyed-services-poc) repo. | Container | By Key | By Key
(Generic) | Many
By Key | Many By
Key (Generic) | Open
Generics | Existing
Instance | Implementation
Factory | | ------------ | ------------------ | -------------------- | ------------------ | ------------------------- | ------------------ | --------------------- | -------------------------- | | Default | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Autofac | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | DryIoc | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Grace | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Lamar | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | LightInject | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Stashbox | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | StructureMap | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Unity | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Container | Just
Works | No Container
Changes | No Adapter
Changes | | ------------ | ------------------ | ------------------------ | ---------------------- | | Default | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Autofac | :white_check_mark: | :white_check_mark: | :white_check_mark: | | DryIoc | :x: | :white_check_mark: | :x: | | Grace | :x:1 | :white_check_mark: | :x:1 | | Lamar | :x: | :white_check_mark: | :x: | | LightInject | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Stashbox | :x: | :white_check_mark: | :x: | | StructureMap | :x: | :white_check_mark: | :x: | | Unity | :white_check_mark: | :white_check_mark: | :white_check_mark: | [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](https://github.com/dotnet/runtime/files/7999147/KeyedServiceV2.zip) 2. [Initial proof of concept](https://github.com/dotnet/runtime/files/7999147/KeyedServiceV3.zip) 3. [Practical API with a lot of ceremony removed](https://github.com/dotnet/runtime/files/7999351/KeyedServiceV4.zip) --- ## 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)](https://github.com/dotnet/runtime/files/7955155/KeyedService.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`: ```c# using static System.AttributeTargets; [AttributeUsage(Class | Interface | Parameter, AllowMultiple = false, Inherited = false)] public sealed class ServiceKeyAttribute : Attribute { public ServiceKeyAttribute(string key) => Key = key; public string Key { get; } } ``` This attribute _could_ be used in the following ways: ```c# [ServiceKey("Bar")] public interface IFoo { } 7 [ServiceKey("Foo")] public class Foo { } public class Bar { public Bar([ServiceKey("Bar")] IFoo foo) { } } ``` Using an attribute has to main advantages: 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](../blob/main/src/libraries/System.Reflection.Context/src/System/Reflection/Context/CustomReflectionContext.cs). 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`: ```c# public static class ServiceProviderServiceExtensions { public static object? GetService(this IServiceProvider serviceProvider, Type serviceType, string key); public static IEnumerable GetServices(this IServiceProvider serviceProvider, Type serviceType, string key); public static T? GetService(this IServiceProvider serviceProvider, string key); public static object GetRequiredService(this IServiceProvider serviceProvider, Type serviceType, string key); public static T GetRequiredService(this IServiceProvider serviceProvider, string key) where T : notnull; public static IEnumerable GetServices(this IServiceProvider serviceProvider, string key) where T : notnull; } ``` It is **not** required for this proposal to work, but as an optimization, it may be worth adding: ```c# public interface IKeyedServiceProvider : IServiceProvider { object? GetService(Type serviceType, string key); } ``` for implementers that know how to deal with `Type` and key separately. To abstract the container and mapping from the implementation, `ServiceDescriptor` will need to add the property: ```c# public string? Key { get; set; } ``` The aforementioned extension methods are static and cannot have their implementations changed in the future. To ensure that container implementers have full control over how `Type + key` mappings are handled, I _recommend_ the following be added to `Microsoft.Extensions.DependencyInjection.Abstractions`: ```c# public interface IKeyedTypeFactory { Type Create(Type type, string key); } ``` `Microsoft.Extensions.DependencyInjection` will provide a default implementation that leverages `CustomReflectionContext`. The implementation _might_ look like the following: ```c# public static object? GetService(this IServiceProvider serviceProvider, Type serviceType, string key) { var provider = serviceProvider as IKeyedServiceProvider ?? serviceProvider.GetService(); if (provider != null) { return provider.GetService(serviceType, key); } var factory = serviceProvider.GetService() ?? KeyedTypeFactory.Default; return serviceProvider.GetService(factory.Create(serviceType, key)); } ``` This approach would also work for new interfaces such as [IServiceProviderIsService](../pull/54047) 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: ```c# class Team { public Team([ServiceKey("A-Team")] IPityTheFoo foo) { } // ← MrT is injected } // ... var services = new ServiceCollection(); // Microsoft.Extensions.DependencyInjection.Abstractions services.AddSingleton("A-Team"); services.TryAddEnumerable(ServiceDescriptor.AddTransient("Thingies")); services.TryAddEnumerable(ServiceDescriptor.AddTransient("Thingies")); services.TryAddEnumerable(ServiceDescriptor.AddTransient("Thingies")); var provider = services.BuildServiceProvider(); var foo = provider.GetRequiredService("A-Team"); var team = provider.GetRequiredService(); var thingies = provider.GetServices("Thingies"); // related services such as IServiceProviderIsService var query = provider.GetRequiredService(); var shorthand = query.IsService("A-Team"); var factory = provider.GetRequiredService(); var longhand = query.IsService(factory.Create("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.

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

ServiceKey will stay null in non-keyed services.

Extension methods for IServiceCollection are added to support keyed services:

public static IServiceCollection AddKeyedScoped(this IServiceCollection services, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType, object serviceKey);
public static IServiceCollection AddKeyedScoped(this IServiceCollection services, Type serviceType, object serviceKey, Func<IServiceProvider, object, object> implementationFactory);
public static IServiceCollection AddKeyedScoped(this IServiceCollection services, Type serviceType, object serviceKey, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType);
public static IServiceCollection AddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection services, object serviceKey) where TService : class;
public static IServiceCollection AddKeyedScoped<TService>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class;
public static IServiceCollection AddKeyedScoped<TService, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection services, object serviceKey) where TService : class where TImplementation : class, TService;
public static IServiceCollection AddKeyedScoped<TService, TImplementation>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TImplementation> implementationFactory) where TService : class where TImplementation : class, TService;
public static IServiceCollection AddKeyedSingleton(this IServiceCollection services, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType, object serviceKey);
public static IServiceCollection AddKeyedSingleton(this IServiceCollection services, Type serviceType, object serviceKey, Func<IServiceProvider, object, object> implementationFactory);
public static IServiceCollection AddKeyedSingleton(this IServiceCollection services, Type serviceType, object serviceKey, object implementationInstance);
public static IServiceCollection AddKeyedSingleton(this IServiceCollection services, Type serviceType, object serviceKey, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType);
public static IServiceCollection AddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection services, object serviceKey) where TService : class;
public static IServiceCollection AddKeyedSingleton<TService>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class;
public static IServiceCollection AddKeyedSingleton<TService>(this IServiceCollection services, object serviceKey, TService implementationInstance) where TService : class;
public static IServiceCollection AddKeyedSingleton<TService, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection services, object serviceKey) where TService : class where TImplementation : class, TService;
public static IServiceCollection AddKeyedSingleton<TService, TImplementation>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TImplementation> implementationFactory) where TService : class where TImplementation : class, TService;
public static IServiceCollection AddKeyedTransient(this IServiceCollection services, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type serviceType, object serviceKey);
public static IServiceCollection AddKeyedTransient(this IServiceCollection services, Type serviceType, object serviceKey, Func<IServiceProvider, object, object> implementationFactory);
public static IServiceCollection AddKeyedTransient(this IServiceCollection services, Type serviceType, object serviceKey, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType);
public static IServiceCollection AddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection services, object serviceKey) where TService : class;
public static IServiceCollection AddKeyedTransient<TService>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class;
public static IServiceCollection AddKeyedTransient<TService, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection services, object serviceKey) where TService : class where TImplementation : class, TService;
public static IServiceCollection AddKeyedTransient<TService, TImplementation>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TImplementation> implementationFactory) where TService : class where TImplementation : class, TService;

public static void TryAddKeyedScoped(this IServiceCollection collection, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type service, object serviceKey) { }
public static void TryAddKeyedScoped(this IServiceCollection collection, Type service, object serviceKey, Func<IServiceProvider, object, object> implementationFactory) { }
public static void TryAddKeyedScoped(this IServiceCollection collection, Type service, object serviceKey, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType) { }
public static void TryAddKeyedScoped<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection collection, object serviceKey) where TService : class { }
public static void TryAddKeyedScoped<TService>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class { }
public static void TryAddKeyedScoped<TService, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection collection, object serviceKey) where TService : class where TImplementation : class, TService { }
public static void TryAddKeyedSingleton(this IServiceCollection collection, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type service, object serviceKey) { }
public static void TryAddKeyedSingleton(this IServiceCollection collection, Type service, object serviceKey, Func<IServiceProvider, object, object> implementationFactory) { }
public static void TryAddKeyedSingleton(this IServiceCollection collection, Type service, object serviceKey, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType) { }
public static void TryAddKeyedSingleton<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection collection, object serviceKey) where TService : class { }
public static void TryAddKeyedSingleton<TService>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class { }
public static void TryAddKeyedSingleton<TService>(this IServiceCollection collection, object serviceKey, TService instance) where TService : class { }
public static void TryAddKeyedSingleton<TService, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection collection, object serviceKey) where TService : class where TImplementation : class, TService { }
public static void TryAddKeyedTransient(this IServiceCollection collection, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type service, object serviceKey) { }
public static void TryAddKeyedTransient(this IServiceCollection collection, Type service, object serviceKey, Func<IServiceProvider, object, object> implementationFactory) { }
public static void TryAddKeyedTransient(this IServiceCollection collection, Type service, object serviceKey, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType) { }
public static void TryAddKeyedTransient<[Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TService>(this IServiceCollection collection, object serviceKey) where TService : class { }
public static void TryAddKeyedTransient<TService>(this IServiceCollection services, object serviceKey, Func<IServiceProvider, object, TService> implementationFactory) where TService : class { }
public static void TryAddKeyedTransient<TService, [Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] TImplementation>(this IServiceCollection collection, object serviceKey) where TService : class where TImplementation : class, TService { }

public static IServiceCollection RemoveAllKeyed(this IServiceCollection collection, Type serviceType, object serviceKey);
public static IServiceCollection RemoveAllKeyed<T>(this IServiceCollection collection, object serviceKey);

I think it's important that all new methods supporting Keyed service have a different name from the non-keyed equivalent, to avoid ambiguity.

"Any key" registration

It is possible to register a "catch all" key with KeyedService.AnyKey:

serviceCollection.AddKeyedSingleton<IService>(KeyedService.AnyKey, defaultService);
serviceCollection.AddKeyedSingleton<IService>("other-service", otherService);
[...] // build the provider
s1 = provider.GetKeyedService<IService>("other-service"); // returns otherService
s1 = provider.GetKeyedService<IService>("another-random-key"); // returns defaultService

Resolving service

Basic keyed resolution

Two new optional interfaces will be introduced:

namespace Microsoft.Extensions.DependencyInjection;

public interface ISupportKeyedService
{
    object? GetKeyedService(Type serviceType, object serviceKey);
    object GetRequiredKeyedService(Type serviceType, object serviceKey);
}

public interface IServiceProviderIsServiceKeyed
{
    bool IsService(Type serviceType, object serviceKey);
}

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

public static IEnumerable<object?> GetKeyedServices(this IServiceProvider provider, Type serviceType, object serviceKey);
public static IEnumerable<T> GetKeyedServices<T>(this IServiceProvider provider, object serviceKey);
public static T? GetKeyedService<T>(this IServiceProvider provider, object serviceKey);
public static object GetRequiredKeyedService(this IServiceProvider provider, Type serviceType, object serviceKey);
public static T GetRequiredKeyedService<T>(this IServiceProvider provider, object serviceKey) where T : notnull;
}

These methods will throw an InvalidOperationException if the provider doesn't support ISupportKeyedService.

Resolving services via attributes

We introduce two attributes: ServiceKeyAttribute and FromKeyedServicesAttribute.

ServiceKeyAttribute

ServiceKeyAttribute is used to inject the key that was used for registration/resolution in the constructor:

namespace Microsoft.Extensions.DependencyInjection;
[AttributeUsageAttribute(AttributeTargets.Parameter)]
public class ServiceKeyAttribute : Attribute
{
    public ServiceKeyAttribute() { }
}

class Service
{
    private readonly string _id;

    public Service([ServiceKey] string id) => _id = id;
}

serviceCollection.AddKeyedSingleton<Service>("some-service");
[...] // build the provider
var service = provider.GetKeyedService<Service>("some-service"); // service._id will be set to "some-service"

This attribute can be very useful when registering a service with KeyedService.AnyKey.

FromKeyedServicesAttribute

This attribute is used in a service constructor to mark parameters speficying which keyed service should be used:

namespace Microsoft.Extensions.DependencyInjection;
[AttributeUsageAttribute(AttributeTargets.Parameter)]
public class FromKeyedServicesAttribute : Attribute
{
    public FromKeyedServicesAttribute(object key) { }
    public object Key { get; }  
}

class OtherService
{
    public OtherService(
        [FromKeyedServices("service1")] IService service1,
        [FromKeyedServices("service2")] IService service2)
    {
        Service1 = service1;
        Service2 = service2;
    }
}

Open generics

Open generics are supported:

serviceCollection.AddTransient(typeof(IGenericInterface<>), "my-service", typeof(GenericService<>));
[...] // build the provider
var service = provider.GetKeyedService<IGenericInterface<SomeType>("my-service")

Enumeration

This kind of enumeration is possible:

serviceCollection.AddKeyedSingleton<IMyService, MyServiceA>("some-service");
serviceCollection.AddKeyedSingleton<IMyService, MyServiceB>("some-service");
serviceCollection.AddKeyedSingleton<IMyService, MyServiceC>("some-service");
[...] // build the provider
services = provider.GetKeyedServices<IMyService>("some-service"); // returns an instance of MyServiceA, MyServiceB and 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 provider
keyedServices = provider.GetKeyedServices<IMyService>("some-service"); // returns an instance of MyServiceA, MyServiceB but NOT MyServiceC
services = 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 provider
services = provider.GetKeyedServices("some-service"); // Not supported
ghost commented 2 years 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'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. 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. The following proposal would add support for keyed services to the existing `Microsoft.Extensions.DependencyInjection.*` libraries **without** breaking the `IServiceProvider` contract. The _clunkiness_ can be hidden behind extension methods and implementation details. For completeness, I have attached a [minimal, viable solution (KeyedService.zip)](https://github.com/dotnet/runtime/files/7955155/KeyedService.zip). It's incomplete from where the final solution would land, but it's enough to illustrate the feasibility of the approach. If approved, I'm happy to start an formal pull request. ### 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`: ```c# using static System.AttributeTargets; [AttributeUsage(Class | Interface | Parameter, AllowMultiple = false, Inherited = false)] public sealed class ServiceKeyAttribute : Attribute { public ServiceKeyAttribute(string key) => Key = key; public string Key { get; } } ``` This attribute _could_ be used in the following ways: ```c# [ServiceKey("Bar")] public interface IFoo { } [ServiceKey("Foo")] public class Foo { } public class Bar { public Bar([ServiceKey("Bar")] IFoo foo) { } } ``` 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](../blob/main/src/libraries/System.Reflection.Context/src/System/Reflection/Context/CustomReflectionContext.cs). 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`: ```c# public static class ServiceProviderServiceExtensions { public static object? GetService(this IServiceProvider serviceProvider, Type serviceType, string key); public static IEnumerable GetServices(this IServiceProvider serviceProvider, Type serviceType, string key); public static T? GetService(this IServiceProvider serviceProvider, string key); public static object GetRequiredService(this IServiceProvider serviceProvider, Type serviceType, string key); public static T GetRequiredService(this IServiceProvider serviceProvider, string key) where T : notnull; public static IEnumerable GetServices(this IServiceProvider serviceProvider, string key) where T : notnull; } ``` It is **not** required for this proposal to work, but as an optimization, it may be worth adding: ```c# public interface IKeyedServiceProvider : IServiceProvider { object? GetService(Type serviceType, string key); } ``` for implementers that know how to deal with `Type` and key separately. To abstract the container and mapping from the implementation, `ServiceDescriptor` will need to add the property: ```c# public string? Key { get; set; } ``` The aforementioned extension methods are static and cannot have their implementations changed in the future. To ensure that container implementers have full control over how `Type + key` mappings are handled, I _recommend_ the following be added to `Microsoft.Extensions.DependencyInjection.Abstractions`: ```c# public interface IKeyedTypeFactory { Type Create(Type type, string key); } ``` `Microsoft.Extensions.DependencyInjection` will provide a default implementation that leverages `CustomReflectionContext`. The implementation _might_ look like the following: ```c# public static object? GetService(this IServiceProvider serviceProvider, Type serviceType, string key) { var provider = serviceProvider as IKeyedServiceProvider ?? serviceProvider.GetService(); if (provider != null) { return provider.GetService(serviceType, key); } var factory = serviceProvider.GetService() ?? KeyedTypeFactory.Default; return serviceProvider.GetService(factory.Create(serviceType, key)); } ``` This approach would also work for new interfaces such as [IServiceProviderIsService](../pull/54047) 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: ```c# class Team { public Team([ServiceKey("A-Team")] IPityTheFoo foo) { } // ← MrT is injected } // ... var services = new ServiceCollection(); // Microsoft.Extensions.DependencyInjection.Abstractions services.AddSingleton("A-Team"); services.TryAddEnumerable(ServiceDescriptor.AddTransient("Thingies")); services.TryAddEnumerable(ServiceDescriptor.AddTransient("Thingies")); services.TryAddEnumerable(ServiceDescriptor.AddTransient("Thingies")); var provider = services.BuildServiceProvider(); var foo = provider.GetRequiredService("A-Team"); var team = provider.GetRequiredService(); var thingies = provider.GetServices("Thingies"); // related services such as IServiceProviderIsService var query = provider.GetRequiredService(); var shorthand = query.IsService("A-Team"); var factory = provider.GetRequiredService(); var longhand = query.IsService(factory.Create("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
Author: commonsensesoftware
Assignees: -
Labels: `api-suggestion`, `untriaged`, `area-Extensions-DependencyInjection`
Milestone: -
madelson commented 2 years ago

Is it worth considering a model where a keyed registration plays out as a registration of a keyed service resolver type (similar to IIndex in autofac)?

In that world, a service keyed T could be retrieved by first resolving a KeyedServiceResolver<T> and then calling it’s Resolve(object key) method. This indirection could be hidden by extension methods.

I think it would be nice to have a solution that isn’t based on attributes (attributes are a good option to have though).

EDIT: reading your post again, I see that you suggest that IKeyedServiceProvider can itself be a service retrieved from IServiceProvider, which is essentially the same thing I'm suggesting.

commonsensesoftware commented 2 years ago

@madelson you are correct and I agree. Attributes are not a requirement. I'd be surprised if any form of a proposal would be accepted if the IServiceProvider.GetService(Type) contract is broken though. Ultimately, this means that there has to be a fallback that centers around Type. There are multiple ways that could be facilitated, but attributes provide a way to add the metadata as a runtime annotation. In the strictest of sense, the result from GetCustomAttibutes is just a collection Object anyway. An implementation could put any annotation/metadata object they want in there using this approach. It doesn't have to be a real Attribute.

If supported, I think you still need to have an attribute at the call site to specify the key that should be used. There might already be attribute that could facilitate that, but none come to mind. Perhaps there's an established convention that can be used, but at attribute seems to be the most obvious choice. As long as it's an option, then there is still a single attribute whether it is used for a single purpose or multiple.

ASP.NET Core already has its own attribute that could fill a similar role with a minor modification:

public IActionResult Get([FromServices("B-Team")] ISpeaker speaker) => Ok({text = speaker.Speak()});
madelson commented 2 years ago

I think you still need to have an attribute at the call site to specify the key that should be used

There are alternatives, though. One is to use one of the func-based methods like TryAddTransient(IServiceCollection, Type, Func<IServiceProvider,Object>) for your registration of the service that consumes the keyed service rather than use reflection.

Another approach is to inject a key-based service lookup type instead of injecting the type itself. For example:

public MyConsumingService(IKeyedServiceLookup<MyKeyedService> keyedServiceLookup)
{
    this._keyedServiceLookup = keyedServiceLookup.Resolve("SomeKey");
}

Or even:

public MyConsumingService(Func<object, MyKeyedService> keyedServiceLookup)
{
    this._keyedServiceLookup = keyedServiceLookup("SomeKey");
}

While I see the appeal of the attribute, something I dislike about it is that (I think) it would force other IOC libraries that interop with IServiceProvider (e. g. Autofac) to know about and respect that attribute in their reflection-based systems, whereas injecting a specific service type is something any DI system should be able to do.

commonsensesoftware commented 2 years ago

I πŸ‘€ now. Yes, an attribute could be restrictive in that way. Using a Func<string, T> is one of the most obvious, but somewhat clunky, ways to support it today.

I definitely see how some type of holder could work. Perhaps something like:

public interface IDependency<T>
{
   T? Get(string? key = default); 
}

This approach would be similar to how IOptionsMonitor<T> works. Different resolution methods (like required) can be achieved via extension methods (ex: dependency.GetRequired("key")). That means that an implementer would always inject an implementation of IDependency<T> using the Null Object pattern - say Dependency<T>.Unresolvable (or something like that).

I didn't mean to steal your thunder. I guess this is effectively the same thing as whatever IKeyedServiceLookup<T> would look like.

commonsensesoftware commented 2 years ago

One thing I don't like about an attribute, function, or lookup interface is that it makes the key another form of hidden dependency to the caller.

It's a little far fetched, but I'm curious what people would think of a little abuse of the type system to raise that dependency out as a formal type.

Consider the following:

// intentional marker so that you can't just provide any old type as a key
public interface IKey { }

public interface IDependency<TKey, TDep>
    where TKey : IKey
    where TDep : notnull
{
    TDep Value { get; }
}

Now consider how it might be used:

public static class Key
{
    public sealed class ATeam : IKey { }
}

public interface IFoo { }
public interface IPityTheFoo : IFoo { } 
public sealed class MrT : IPityTheFoo { }

public class Bar
{
    private readonly IDependency<Key.ATeam, IPityTheFoo> foo;
    public Bar(IDependency<Key.ATeam, IPityTheFoo> foo) => this.foo = foo;
    public IFoo Foo => foo.Value;
}

This would expect that an implementer uses typeof(TKey).Name as the service key. An implementer would already have to understand IDependency - in any form.

Pros

  • The dependency key is conspicuously expressed as part of the dependency
  • The container knows the dependency doesn't exist before calling the constructor
  • Backing resolution, say through IKeyedServiceProvider.GetService(Type,string) still works
  • Largely removes magic strings, including at registration-time
    • ex: services.AddSingleton<Key.ATeam, IPityTheFoo, MrT>();
  • This could be a way to solve the IServiceProvider.GetService(Type) conundrum:
    • ex: sp.GetService<IDependency<Key.ATeam, IFoo>>()?.Value; // ← by 'key'

Cons

  • No guarantee an implementer consistently uses typeof(TKey).Name (caveat emptor?)
    • Maybe this is a non-issue; the Type implementing IKey is the key and string variants are simply unnecessary, except perhaps as a backing implementation detail
  • There are limitations on what the key can be since it must be a valid type name
    • IMO this is of little consequence given the tradeoff of expressing the dependency key
madelson commented 2 years ago

@commonsensesoftware the idea of having keys be types rather than strings is a really interesting one. If the system went in that direction I wouldn't want it to be strings at all under the hood: just types all the way down. Otherwise it feels too magical.

The main weakness of types over arbitrary objects/strings is in more dynamic cases. For example you might want to have a set of services keyed to values of an enum and resolve the appropriate service at runtime based on the enum value. You can still do that with type keys, but you need an extra mapping layer to go enum value -> type.

One other minor thing to note: I don't think service implementers injecting IDependency<Key, Service> would save off the whole type. Instead, they'd just have private readonly Service _service; and do this._service = serviceDependency.Value; in the constructor.

commonsensesoftware commented 2 years ago

@madelson I think we are largely on the same page. I don't like the idea of magic strings either (at all).

... you need an extra mapping layer to go enum value -> type

I'm trying to think of a scenario where Type wouldn't work. Yes, it's a little unnatural, but Type is effectively being used as an enumeration. I can't think of a scenario (off the top of my head) where Type couldn't be used this way. It should be able to be used in all the places a constant or enumeration could also be used. One counter argument that I like is that a Type cannot be abused in a way that an enumeration can. For example, var weekday = (DayOfWeek)8; is a lie, but happily compiles and executes at runtime.

I completely agree that consumers would want to unwrap the dependency. Where and how is at their discretion. It is anologous to IOptions<T>. You can unwrap it in the constructor, but consumers should be aware that the entire dependency graph may not be resolvable at that point in time. It doesn't happen often, but it is possible.

I decided I wanted to flush the idea of using Type as a key to see what it would look like in practice. Some interesting and positive results were revealed:

  • Using System.Reflection.Context is no longer required (e.g. no new assembly references)
  • No magic strings
  • No attributes (ServiceKeyAttribute was removed)
  • ServiceDescriptor.Key is unnecessary nor are any other changes to the existing type
  • IKeyedServiceProvider and IKeyedTypeFactory are unnecessary
  • IKey doesn't buy much value and was removed

The revised contract would be:

public interface IDependency<in TKey, out TService>
    where TKey : new()
    where TService : notnull
{
    TService Value { get; }
}

Enforcing the new() constraint on the key isn't strictly necessary. It's another variation of IKey, but with slightly more practical enforcement.

The extension methods now simply revolve around IDependency<TKey, TService>. For example:

public static TService? GetService<TKey, TService>(this IServiceProvider serviceProvider)
    where TKey : new()
    where TService : notnull
{
    var dependency = serviceProvider.GetService<IDependency<TKey, TService>>();
    return dependency is null ? default : dependency.Value;
}

Examples

Here are some revised examples:

[Fact]
public void get_service_generic_should_return_service_by_key()
{
    // arrange
    var services = new ServiceCollection();

    services.AddSingleton<Key.Thingy, IThing, Thing2>();
    services.AddSingleton<IThing, Thing>();

    var provider = services.BuildServiceProvider();

    // act
    var thing = provider.GetService<Key.Thingy, IThing>();

    // assert
    thing.Should().BeOfType<Thing2>();
}

[Fact]
public void get_services_generic_should_return_services_by_key()
{
    // arrange
    var services = new ServiceCollection();
    var expected = new[] { typeof(Thing1), typeof(Thing2), typeof(Thing3) };

    services.TryAddEnumerable<Key.Thingies, IThing, Thing1>(ServiceLifetime.Transient);
    services.TryAddEnumerable<Key.Thingies, IThing, Thing2>(ServiceLifetime.Transient);
    services.TryAddEnumerable<Key.Thingies, IThing, Thing3>(ServiceLifetime.Transient);

    var provider = services.BuildServiceProvider();

    // act
    var thingies = provider.GetServices<Key.Thingies, IThing>();

    // assert
    thingies.Select(t => t.GetType()).Should().BeEquivalentTo(expected);
}

[Fact]
public void get_required_service_should_inject_dependencies()
{
    // arrange
    var services = new ServiceCollection();

    services.AddSingleton<Key.Thing1, IThing, Thing1>();
    services.AddTransient<Key.Thing2, IThing, Thing2>();
    services.AddSingleton<CatInTheHat>();

    var provider = services.BuildServiceProvider();

    // act
    var catInTheHat = provider.GetRequiredService<CatInTheHat>();

    // assert
    catInTheHat.Thing1.Should().BeOfType<Thing1>();
    catInTheHat.Thing2.Should().BeOfType<Thing2>();
}

As previously mentioned, CatInTheHat.cs would look like:

public class CatInTheHat
{
    private readonly IDependency<Key.Thing1, IThing> thing1;
    private readonly IDependency<Key.Thing2, IThing> thing2;

    public CatInTheHat(
        IDependency<Key.Thing1, IThing> thing1,
        IDependency<Key.Thing2, IThing> thing2)
    {
        this.thing1 = thing1;
        this.thing2 = thing2;
    }

    public IThing Thing1 => thing1.Value;

    public IThing Thing2 => thing2.Value;
}

This second iteration doesn't require any fundamental changes to Microsoft.Extensions.DependencyInjection.*. All of the proposed changes are additive. While it is possible expose this functionality via an extension library, supporting keyed services as a first-class concept would be a nice edition.

The attached Keyed Service - Iteration 2 contains an end-to-end working example using only Type as a key.

madelson commented 2 years ago

I like this direction. One thought:

Enforcing the new() constraint on the key isn't strictly necessary. It's another variation of IKey, but with slightly more practical enforcement.

I'd vote for nixing the new() constraint as well. For example, in some cases I could see wanting to use the target service type as the injected key:

public class MySpecialService
{
    // implication is that MySpecialService needs an implementation of ISomeService customized just for it
    public MySpecialService(IDependency<MySpecialService, ISomeService> someService) { ... }
}
commonsensesoftware commented 2 years ago

@madelson I'm onboard with that. There's no strict reason why the new() constraint needs to exist. At the end of the day a Type is being used as a key. It doesn't really matter whether it makes sense to me. If it works and someone chooses to use it in an obscure way - so be it.

There are just a few other design points. There needs to be a non-generic way to get the value of a keyed dependency.

public interface IDependency
{
    object Value { get; }
}

This is already in the last iteration. A consumer still asks for typeof(IDependency<TKey,TService>), but there needs to be away to get IDependency<TKey,TService>.Value without resorting to Reflection or something. This potentially means the contract changes to:

public interface IDependency<in TKey, out TService> : IDependency where TService : notnull
{
    new TService Value { get; }
}

I don't really like that the Value property is shadowed this way, but it may be the best option. In the 2.0 version, I had them separate. The risk of them being separate is that an extender may not implement both interfaces, which breaks one path. The cast to IDependency would fail making it obvious, but that wouldn't happy until runtime. That feels like a bad experience. Since interfaces are not really inherited, shadowing the Value property with a more specific type should be fine. That would ensure any other provided implementation of IDependency<TKey,TService> provides a way to retrieve its Value without knowing/having the generic interface type. I validated this slight change and everything still works as expected.

The only other open item I have (so far) is how ServiceDescriptor is registered. Currently, this feature would require two registrations:

  1. Register the service with a keyed type
  2. Register IDependency<TKey,TService> which hides the keyed type mapping

This behavior could be done with a single registration, but the container implementer would have to know and understand that IDependency<TKey,TService> is special. Having two registrations may be a non-issue. There are truly two different, distinct service registrations that are ultimately both used.

The other question is whether IDependency<TKey,TService> should have the same lifetime as TService. The registration of IDependency<TKey,TService> can always be transient because the implementation is expected to be a simple facade over IServiceProvider.GetService(Type), which will always resolve TService using the correct scope. IDependency<TKey,TService> effectively serves as a transient accessor to TService. I'm not sure there is an advantage to holding on to an instance of IDependency<TKey,TService> longer. If it's always transient, the memory allocated for it can be reclaimed sooner. Ideally, it could be struct, but since it will always be boxed, I'm not sure that helps.

Beyond those basic mechanics, I think we're down to just the final naming, which I have no real strong opinions about, and whether this proposal would/will be accepted. Maybe I need to revise the original proposal with all the changes that have been made? I think we've landed in a much better and more sensible approach to keyed services than where we started. Is more support/evidence required to justify these additions being rolled in rather than exposed via some 3rd party extension?

davidfowl commented 2 years ago

Like all DI features, they need to be supported by a majority of the existing containers to be considered. I'm not sure this feature meets the bar but we can investigate.

DI council: @alexmg @tillig @pakrym @ENikS @ipjohnson @dadhi @seesharper @jeremydmiller @alistairjevans

dadhi commented 2 years ago

@davidfowl @commonsensesoftware

I would love to see a minimal set of features agreed upon (maybe with voting). The container authors may label on what is available in their libs.

And then the table with solution, pros, cons and comments.

Otherwise we will lost in the options.

davidfowl commented 2 years ago

Named DI has come up several times before (e.g. https://github.com/dotnet/extensions/issues/2937) and we've work around not having the feature in many systems (named options, named http clients etc). While I think the feature is useful, I am hesitant to add anything that couldn't be implemented super efficiently in the core container and that other containers don't support. The implementation shouldn't need to mess with the reflection context either. This feels very core to the DI system so the implementation cost would be large (we'd need changes to service descriptors etc).

The attribute seems like a reasonable approach if its supported everywhere. If the set of keys is known at compile time, then open generics can be used to solve this problem today.

Those are some late night thoughts before I go to bed πŸ˜„

tillig commented 2 years ago

I think Autofac could cover this without too much effort in adapting the service registrations from MS format to Autofac. Some specific behavior would need to be determined, though, before I could say more conclusively. Here is how Autofac handles some of the stuff I haven't seen talked about above.

First, simple non-generic types:

var builder = new ContainerBuilder();

// Keyed
builder.RegisterType<Thing1>().Keyed<IThing>("key");
builder.RegisterType<Thing2>().Keyed<IThing>("key");

// Not keyed
builder.RegisterType<Thing3>().As<IThing>();

// BOTH keyed AND not keyed
builder.RegisterType<Thing4>().As<IThing>().Keyed<IThing>("key");

var container = builder.Build();

// Resolving the set of keyed items will have
// Thing1, Thing2, Thing4.
var allKeyed = container.ResolveKeyed<IEnumerable<IThing>>("key");
Assert.Equal(3, allKeyed.Count());

// Resolving the non-keyed items only returns
// things that were explicitly registered without a key.
// Thing3, Thing4
var notKeyed = container.Resolve<IEnumerable<IThing>>();
Assert.Equal(2, notKeyed.Count());
  • What's the expected behavior when an IEnumerable<T> is resolved without a key? Should it include both things that have keys and don't have keys? Or just the explicitly non-keyed items? Autofac won't easily be able to implement "return everything" in any sort of performant way. It's been "only keyed" or "only non-keyed" for a very long time so it's kinda baked into the core of the thing.
  • Can you register the same component both as keyed and not keyed? This can be important if you want to have a "default instance" and have that same instance be specifically named. Especially if it's not a transient lifestyle, that also means it's literally the same instance instead of two instances due to two different registrations.

Open generics add a bit of an interesting mix:

var builder = new ContainerBuilder();

// You can specify the same key for all the things.
builder.RegisterGeneric(typeof(GenericThing<>))
       .Keyed("key", typeof(IGeneric<>));

// This all works.
_ = container.ResolveKeyed<IGeneric<string>>("key");
_ = container.ResolveKeyed<IGeneric<object>>("key");

// ...but it's not registered as non-keyed.
Assert.Throws<DependencyResolutionException>(() => container.Resolve<IGeneric<string>>());

Do all the open generics in a single registration get the same key? As you can see above, that's how Autofac works. The alternative might be to have some sort of function provided during registration where the key can be generated based on the type being resolved, but Autofac doesn't support that.

Other thoughts that may get the juices flowing:

  • Attributes should always be an optional thing. There's a fairly vocal minority of folks who are very interested in not letting DI-related code intermingle with their other code. It ties the DI system specifically to the implementation and that's not super cool. Some folks go to a lot of work writing their own wrappers around DI systems to make sure they're totally decoupled. It's fine if it's an added feature, but it shouldn't be the primary/only way things work. Autofac does support resolving based on filter attributes (like adding an attribute to a constructor parameter) but doesn't have an attribute to allow you to register and have a name automatically attached. There is definitely a perf hit when someone uses the keyed attribute filter because now you have to look for that attribute on every constructor and obey it on every resolution. Autofac makes this explicitly opt-in.
  • Registration metadata may be more interesting. As long as you're looking things up about a registration, it may be interesting to allow each registration to support a general dictionary of metadata so you could filter by different things. Here are some Autofac examples.
  • Lazy<T> support gets used a lot with metadata registrations. That is, it seems pretty common that folks will want to register like 10 things with different keys/metadata, resolve all of them, then say "I want to use abc and def but not ghi" so there's a sort of LINQ over the set of items before resolving. Which isn't to say "keyed registrations are a slippery slope," but just be aware some of these patterns go together somewhat naturally.

Here's what I mean about the Lazy<T> thing:

var builder = new ContainerBuilder();
builder.RegisterType<Thing1>().As<IThing>().WithMetadata("name", "rest-1");
builder.RegisterType<Thing2>().As<IThing>().WithMetadata("name", "rest-2");
builder.RegisterType<Thing3>().As<IThing>().WithMetadata("name", "soap-1");
var container = builder.Build();

// Get a list of all the registered `IThings`
// with all their metadata
// but don't resolve them quite yet
var restThings = container.Resolve<IEnumerable<Meta<Lazy<IThing>>>()
  // then filter to just the REST things
  .Where(meta => meta.Metadata["name"].ToString().StartsWith("rest"))
  // Get the Lazy<IThing> for each of those
  .Select(meta => meta.Value)
  // Now do the resolve operation
  .Select(lazy => lazy.Value);

I recognize we're just talking about keyed/named things, so I don't want to confuse the issue by bringing registration metadata into the mix, just that the two concepts are somewhat near to each other and I thought it might open some ideas or thoughts here that might not otherwise have come up. Sorry if the mention of it derails things. I hope it doesn't.

jeremydmiller commented 2 years ago

Lamar already has support for named registrations, and I'd guess that many of the mainstream IoC containers do as well as that was very common before the ASP.Net team decided retroactively how everything was meant to behave. Heck, StructureMap had that in 2004. That being said, I'm basically against any extension to the core, conforming container behavior unless there's some really compelling reason to do so. That being said, what's the use case here that folks want? Especially knowing that they can always go down to the inner container at any time and generally have quite a bit more functionality than what's exposed through IServiceProvider.

At a minimum, I'd eliminate any kind of fancy behavior about "do named vs not named instances get returned from GetServices(Type)" as much as possible. And be very clear about how you'll handle naming collisions. First one wins? Last?

tillig commented 2 years ago

I'm basically against any extension to the core, conforming container behavior unless there's some really compelling reason to do so.

Yeah, I'll second this. The conforming container thing has been a sore spot since inception.

seesharper commented 2 years ago

LightInject has always supported named registrations. Was it a good idea? Well, it has gotten me out of trouble on a number of occasions. In fact, it is used in the Ms.Ex.DI adapter to work around some of the differences between LightInject and Ms.Ex.DI. πŸ˜€. When it comes to adding this feature to to the specifications I'm not so sure if that is a good idea. It significantly raises the bar for conforming containers which might or not might support this out of the box.

alistairjevans commented 2 years ago

Named/keyed registrations often have implications for how the service is consumed, rather than just how it is registered. I.e. do you use attributes, an IIndex style dependency, a direct call to a method on an injected scope of some kind, etc.

I believe there's a fair amount of strong opinion in the wild on how best to consume a keyed service (most of which have already been discussed here). I'm a little concerned that going down a specific path in the conforming container for how to consume a keyed service will lead to reduced choice in the ecosystem on this particular point, where there already exist plenty of community libraries people could use that allows them to take the approach they prefer. As a general note, I'd probably lean towards not adding features to the conforming container that add constraints on how user components can/should specify their dependencies.

On a specific technical note re:attributes, I believe Autofac would require a chunk of additional work to inspect the constructor of components for non-Autofac attributes, without the explicit opt-in we currently require on the component that says "please consider the KeyFilter attribute on this component". Not having that explicit opt-in may have knock-on consequences in core Autofac which I'd like to avoid thinking about if possible, and I don't believe conforming container registration currently has the concept of "opt-in some behaviour on this component when resolving dependencies".

commonsensesoftware commented 2 years ago

Apologies, but I had the assumption that the discussion would pick up and continue where it left off (with @madelson). After further thought and consideration, I've decided to revise the API proposal. Based on the additional comments, I feel that the revision aligns better with several of the callouts.

@davidfowl, I :100: agree. If this API proposal cannot work with the existing container implementations, then it should be DOA :skull:. I don't know that you're sold yet, but I think you might be surprised or pleased that the latest revision eliminates magic strings, unnecessary attributes, Reflection (context) hackery, no change to ServiceDescriptor, works with open generics, and still works through the super efficient mechanisms offered by existing containers. I've added integration tests for each of the existing container implementations to the example code and summarized the results in the proposal.

@tillig, in the revised proposal, Autofact just works without a single change :wink:. To answer your specific questions:

  1. IEnumerable<T> and IEnumerable<Key+T> are distinctly different registrations and never shall the two meet. IMHO that is clean and straight forward to understand. ServiceDescriptor and/or native container implementations provide ways to use multiple registrations for a particular instance, but that's in the hands of a developer.
  2. Yes - a component can be keyed and non-key. A use case I previously had a was a scoped DbContext<T> in the request pipeline, but a separate, transient DbContext<T> of the same type used in the background.
  3. A keyed and non-key type should be distinct IMO; therefore, when registering an open generic, IGeneric<> and Key+IGeneric<>, should be separate without any possibility of resolution in each other's context.
  4. The revised proposal ditches attributes, so hopefully this is a non-issue now.
  5. I think metadata may be out of scope here, but - I admit - I'm a fan. :smile:

@jeremymiller, you make an excellent point about the possible collisions of magic strings. In many cases, it's also a hidden dependency that a caller cannot know. Whether you agree with my revised proposal remains to be seen, but it would eliminate both of these issues without fancy ceremony for a consumer. In particular, by having to express key and type together, it's possible to know and validate whether a particular combination has been registered (which is difficult or impossible with magic strings).

@seesharper, agreed. In the revised proposal, LightInject just works out-of-the-box and without having to do any magic under the hood to use its native named registrations. :wink:.

@alistairjevans, these are fair points and, at least on some level, give credence to my revised proposal; specifically with regard to removing the use of attributes. All implementations already agree on consuming a service by asking for its Type (which is the key). This proposal takes it a step further with a thinly-veiled abstraction, where registering a more specific type in a container may not be achievable through other means such as inheritance. Whether that maps as a plain 'ol type or a type with a key/name in the container is an implementation detail.

tillig commented 2 years ago

ServiceDescriptor, I assume, would end up with some Key property that adapter libraries would need to use and, if not null, register a keyed entity.

I'm personally less concerned about magic strings because people do some interesting stuff out in the wild, assembly scanning to find their own custom attributes and add string-based keys due to the needs of their own homegrown "plugin" systems. I think folks could find the requirement of a key being a type to be too restrictive and not dynamic enough, but maybe in this lowest-common-denominator conforming container case that's OK.

@alistairjevans makes a good point about having a specific consumption mechanism, but from a devil's advocate standpoint, without some way to consume this more formally (e.g., the Meta<T> decorator in that Autofac metadata example), folks who need keyed services are going to have to start injecting IServiceProvider directly and doing service location in the constructor to get what they need. I guess maybe that's OK, but it kinda defeats the purpose of the DI in the first place and makes testing a pain.

Which isn't to say I think the attribute consumption model is the way to go, just trying to walk through what it means on both sides of this construct.

I suppose a workaround for that consumption model might be a class that abstracts away the service location...

public class NamedServiceProvider<T>
{
  private readonly IServiceProvider _provider;
  public NamedServiceProvider(IServiceProvider provider)
  {
    this._provider = provider;
  }

  public T GetService(Type key)
  {
    return (T)this._provider.GetService(key, typeof(T));
  }
}

...and have that registered/injected into the consumer.

public MyConstructor(NamedServiceProvider<IThing> factory)
{
  this._instance = factory.GetService(Key.Thing1);
}

However, I'm still just sorta iffy on the whole concept. I'm not totally against it, but in general expanding the conforming container beyond the most amazingly simple, basic functionality has sort of a "smell" to it for me. It always seems like all this should "just work" and then Autofac will have some difference in how it functions from the base container; or Autofac will differ from SimpleInjector; or something like that, and way, way downstream consumers will get bent out of shape that they chose different backing containers but it isn't all behaving literally identically to the M.E.DI container.

My understanding of the intent of the original conforming container was to be just enough to provide an abstraction for the base ASP.NET Core framework to function. It's definitely taken off with the community, which is great, but I don't think keyed registrations is required for the framework to function. Again, not saying I'm 100% against it, just thinking back to reasons to expand on the conforming container and what the original intent (my understanding) is.

This same functionality could be implemented already by just using the backing container of your choice and registering the keyed stuff in ConfigureContainer. I get that the target here is library owners wanting to register keyed stuff (if it's not library owners, then the use of ConfigureContainer or similar is a pretty clear solution) but I also wonder if that means the library owners might need to look at different ways to handle this stuff. Like, maybe you don't have five IRepository implementations and instead have IUserRepository, IRoleRepository, and so on, and instead differentiate by actual type.

commonsensesoftware commented 2 years ago

@tillig,

Actually, you do not end up with a Key property; it's not necessary. Type is the key. Consider this:

void Demo(IServiceProvider provider)
{
    // 'keyed' service location in the traditional way since day 1
    var key = typeof(IThing);
    var thing = (IThing) provider.GetService(key);

    // requiring 2 types forms a 'composite key'
    var compositeKey = typeof(IDependency<Key.Thingy, IThing>);
    var thingy = (IThing) provider.GetService(compositeKey);

    // this is technically the same thing, but is less 'natural' and
    // can't be used in all call sites where IThing could be requested
    var compositeKey = KeyedType.Create(typeof(Key.Thingy), typeof(IThing));
    var thingy = (IThing) provider.GetService(compositeKey);
}

IServiceProvider is consistently supported for all containers. I totally agree that eliminating magic strings is not what raises the bar here. That approach has been used for close to two decades. I have no intention of convincing people they have to ditch their strings if they want them. I am, however, suggesting that we already use Type as a key and I'm merely trying to expand upon that concept. If an implementer wants to use a string under the hood, they are in total control and free to do so.

I'm not entirely sure how Meta<T> would playout using this approach, but I'm willing to go the extra mile and think through what the impact would be or how it would work.

The idea of NamedServiceProvider<T> is exactly what IDependency<TKey,TService> is meant to provide. It's an interface so that a container can decide on its own how it should be satisfied. Having a dependency injected through this interface provides a way to define a composite key without relying on metadata (say from attributes) or manual lookup by a developer through IServiceProvider.GetService(Type). As you astutely pointed out, that would make testing more painful. Providing a mock or stub of IDependency`2 is very straight forward. More than anything, I think that using types this way addresses the issue of injecting keyed dependencies at the call site. The registration side is more straight forward; especially since a developer can drop down to the native container implementation without adding a lot of coupling elsewhere. The inverse is typically not true.

Consider injecting a keyed service into an ASP.NET Core action.

[HttpGet]
public IActionResult Get([FromServices] IDependency<Key.School, IGreeter> greeter) =>
    Ok(greeter.Value.Welcome());

This doesn't require anything special to be added to the core abstractions. Yes, obviously we need the interface or something that can be the placeholder. A container doesn't need to do any special to look this up. Furthermore, it is compatible with the established semantics of IServiceProvider for all containers. Resolution from a container has always been the how prerogative of the container.

I'm a visual person. I'm very much the "Show me the code" guy. I also realize that people are busy with their day jobs, side projects, family, etc. I've attached a working solution with test scenarios for all of the existing container support to enhance this conversation. Theory is great, but I'd rather see it in action. There are a lot more details and comments I've put in the code that are simply a lot to rehash in detail within the thread.

You'll be happy (maybe?) to know that Autofac works for the (now) complete set of scenarios (unless I missed something). The Autofact setup is still as simple as:

var builder = new ContainerBuilder();
builder.Populate(services);
return builder.Build().Resolve<IServiceProvider>();

:clap: Nice work. Due to how Autofac works, it's actually unnecessary to go through the native key/name service resolution APIs. Everything works as is.

What about a container where that isn't the case? LightInject (@seesharper) worked for all scenarios, except for Implementation Factory (which was previously untested). This likely has to do with how LightInject registers the function. This is the main barrier for automatic support across containers. KeyedType.Create returns a fake type that is a TypeDelegator. This Type cannot be compiled or invoked. Clearly there's a way around this because it works for some frameworks. This could be a mistake or ommission on my part. Regardless, the examples demonstrate that if that were to be a problem, then there is an escape hatch.

It took a few iterations, but now that I've tested this out against all of the supported containers, I noticed a repeating pattern and removed as much of the ceremony as possible. The core abstractions will provide two things that will make adapting them to any other container simple:

  1. Remove and collate keyed services from IServiceCollection into IReadOnlyDictionary<Type, IServiceCollection> a. The Type key in the dictionary is the key generated by KeyedType.Create when the descriptor was registered b. The IServiceCollection contains all descriptors mapped to that key
  2. Provide a base visitor implementation to remap descriptors to their native container variants a. This process doesn't change how the developer registered the service, only how the container resolves it

With that in mind, the LightInject adapter would define two native IDependency`2 implementations:

// remaps the core Dependency<,> abstraction
internal sealed class LightInjectDependency<TKey, TService> :
    IDependency<TKey, TService>
    where TService : notnull
{
    // LightInject can do this however it wants, but the hash code will be unique
    // important: this does have to match how LightInject defines the key (see below)
    private readonly string key =
        KeyedType.Create<TKey, TService>().GetHashCode().ToString();
    private readonly IServiceContainer container;
    protected LightInjectDependency(IServiceContainer container) =>
        this.container = container;
    public TService Value => container.GetInstance<TService>(key);
    object IDependency.Value => Value;
}

// remaps the core Dependency<,,> abstraction for IEnumerable<T> support
internal sealed class LightInjectDependency<TKey, TService, TImplementation> :
    IDependency<TKey, TService>
    where TService : notnull
    where TImplementation : notnull, TService
{
    // note: that here we want to key on the concrete type
    private readonly string key =
        KeyedType.Create<TKey, TImplementation>().GetHashCode().ToString();
    private readonly IServiceContainer container;
    protected LightInjectDependency(IServiceContainer container) =>
        this.container = container;
    public TService Value => container.GetInstance<TImplementation>(key);
    object IDependency.Value => Value;
}

Now that we have LightInject-specific resolvable dependencies, we'll use a visitor to remap the existing keyed service descriptors. The base implementation does all the heavy lifting of enumerating service and determinging which descriptors need to be passed when and with what keys. The following highlights the relevant parts of the implementation:

internal sealed class LightInjectKeyedServiceVisitor : KeyedServiceDescriptorVisitor
{
    private readonly IServiceContainer container;
    private readonly Scope rootScope;

    public LightInjectKeyedServiceVisitor(IServiceContainer container)
        : base(
            /* remap Dependency<,>  β†’ */ typeof(LightInjectDependency<,>),
            /* remap Dependency<,,> β†’ */ typeof(LightInjectDependency<,,>))
    {
        var self = new ServiceRegistration()
        {
            ServiceType = typeof(IServiceContainer),
            Value = container,
        };
        this.container = container;
        this.container.Register(self);
        rootScope = container.BeginScope();
    }

    protected override void VisitDependency(ServiceDescriptor serviceDescriptor)
    {
        // ServiceDescriptor.ServiceType = IDependency<TKey,TService>
        // ServiceDescriptor.ImplementationType =
        //     LightInjectDependency<TKey,TService> ||
        //     LightInjectDependency<TKey,TService,TImplementation>
        //
        // lifetime probably doesn't matter here and can be transient, but the descriptor
        // will contain the same lifetime associated with the underlying resolved type
        container.Register(
            serviceDescriptor.ServiceType,
            serviceDescriptor.ImplementationType,
            ToLifetime(serviceDescriptor));
    }

    // no need to know how 'Key' is defined or how it's mapped to ServiceDescriptor
    protected override void VisitService(Type key, ServiceDescriptor serviceDescriptor)
    {
        // key = KeyedType<TKey, TService>
        // serviceDescriptor = ServiceDescriptor with real types
        var registration = new ServiceRegistration()
        {
            // LightInject is deciding how to use the key for lookup (see above)
            ServiceName = key.GetHashCode().ToString(),
            ServiceType = serviceDescriptor.ServiceType,
            Lifetime = ToLifetime(serviceDescriptor),
        };

        if (serviceDescriptor.ImplementationType != null)
        {
            registration.ImplementingType = serviceDescriptor.ImplementationType;
        }
        else if (serviceDescriptor.ImplementationFactory != null)
        {
            registration.FactoryExpression = CreateFactoryDelegate(serviceDescriptor);
        }
        else
        {
            registration.Value = serviceDescriptor.ImplementationInstance;
        }

        container.Register(registration);
    }
}

LightInject has the following setup today:

var builder = new ContainerBuilder();
builder.Populate(services);
return services.CreateLightInjectServiceProvider();

To make it work with a keyed Implementation Factory, the setup would change as follows:

static IServiceProvider BuildServiceProvider(IServiceCollection services)
{
    var options = new ContainerOptions().WithMicrosoftSettings();
    var container = new ServiceContainer(options);
    var original = new ServiceCollection();

    // make a shallow copy of the current collection
    for (var i = 0; i < services.Count; i++)
    {
        original.Insert(i, services[i]);
    }

    // new, built-in extension method in the core abstractions
    // this removes and maps keyed service descriptors
    var keyedServices = services.RemoveKeyedServices();

    if (keyedServices.Count == 0)
    {
        // perf: remapping can be skipped if the developer didn't
        // use ServiceDescriptor.ImplementationFactory for a keyed service
        var remapNotRequired = services.Values
                                       .SelectMany(v => v)
                                       .All(sd => sd.ImplementationFactory == null);

        if (remapNotRequired)
        {
            return container.CreateServiceProvider(original);
        }
    }

    var visitor = new LightInjectKeyedServiceVisitor(container);

    // visiting the keyed services will re-register them in the container
    // with container-natively understood semantics
    visitor.Visit(keyedServices);

    return container.CreateServiceProvider(services);
}

The goal is not get containers to change the way they use or define keyed/named services. This is only meant to provide support of a keyed type. As it relates to IServiceCollection and ServiceDescriptor, some containers may choose to facilitate that through their native implementations, while others - like Autofac and the default engine - work without using any native keying mechanisms whatsoever. The developer consumption model should not be:

IServiceProvider.GetService(typeof(IDependency<My.Key,IThing>)) β†’
    IContainer.GetService("key", typeof(IThing))

Even if that might be what happens behind the scenes.

z4kn4fein commented 2 years ago

Hello there! I think the idea of supporting keyed services is great, and thank you for involving Stashbox in the testing project. It revealed a major deficiency that you pointed out correctly, the resolution of many services by key was broken. I just wanted to let you know that this issue is fixed in Stashbox v5.4.3 and Stashbox.Extensions.DependencyInjection v4.2.3 packages. With the current state of the API proposal, Stashbox still needs the KeyedServiceVisitor that bypasses the registration of TypeDelegator types. Thanks again!

@davidfowl I would be grateful if you could tag me also when you include the DI Council in a thread. This issue gave me useful information to improve my library, and I just found it accidentally. πŸ˜„ Thank you!

davidfowl commented 2 years ago

@commonsensesoftware Can you put your prototype up on github?

commonsensesoftware commented 2 years ago

@davidfowl Just to be clear, are you asking me to take the attached prototype (at the top) and turn it into a repo (instead)? If so, I'll work on that ASAP.

davidfowl commented 2 years ago

Yep!

mendozagit commented 2 years ago

I think can be useful.

GitRepo

davidfowl commented 1 year ago

I think we're at the point where we need to seriously consider the changes required to build this into the core container. Several of our primitives have worked around this (named options, ILogger) and the need is just growing.

tillig commented 1 year ago

In the examples given (options, logger), the key boils down to a string rather than a type, which differs from the initial API proposal here. Are there thoughts as to what the core container would be implementing?

davidfowl commented 1 year ago

I feel like strings are the 90% case. Do you see anything different in your experience?

tillig commented 1 year ago

String is definitely the majority case we've seen in Autofac, too.

seesharper commented 1 year ago

LightInject has always used a string for keyed services. It has proved to be very versatile and we never really thought about the key being anything else πŸ‘

madelson commented 1 year ago

FWIW, we frequently use object instances as "private" keys in scenarios where one module is registering keyed services and doesn't want to expose the keyed registrations to other modules. We're doing this through Autofac.

dadhi commented 1 year ago

Most examples in @DryIoc are using enum and string keys. Internally and when integrating with other libs I am using the private object keys to avoid the conflicts with the user defined keys.

ipjohnson commented 1 year ago

Grace supports registration by object key, that said I like string as the common denominator for all the packages. I currently use only strings & enum for registration in code that I write. For the private registrations I do something like Guid.NewGuid().ToString() to generate a unique key.

commonsensesoftware commented 1 year ago

@commonsensesoftware Can you put your prototype up on github?

@davidfowl apologies for the long overdue reply and upload. I've cleaned things up a little, updated all the latest container builds, and uploaded it to:

https://github.com/commonsensesoftware/keyed-services-poc

I'm happy to report that with the last container builds, all of the target container frameworks that (I believe) we've discussed work without any changes to the respective container. I've provided an example of how a container can integrate through their adapter, but this was just my approach to making it work. A container owner is likely to have a much more biased approach to the implementation details.

commonsensesoftware commented 1 year ago

As the person championing the proposal, it was important to me that it must be able to work via the already expected adapter without fundamental container changes. While a magic string might be the 90%, it's not the 100%. Type is already organically a key. There is no other dogma imposed. Exactly how a container uses is up to them. If they want to use a string (ex: Type.AssemblyQualifiedName), the Type itself (which I presume will be the hash code), or some other method, then that is an implementation detail IMHO.

If there is a scenario that simply cannot work unless it's a specific form - say a String, then that's a different matter. Thus far, I have not hit nor heard of such a barrier. I'm sure at least some agree that magic strings are undesirable, even if you're of the mindset that it's a perfectly workable solution.

In case you're wondering what your adapter might look like, here are some links for your convenience to take a quick peek:

This is by no way close to a final design or implementation. My goal was only to show that this proposal is a workable solution and has the ability to adapt to any container, but more specifically the most common implementations. Even where some additional work might be necessary in the adapter, the POC demonstrates an escape hatch that if there are no keyed services registered, then you can follow the exact same path that exists today.

vukovinski commented 1 year ago

Since my proposal was referenced above, I will just copy-paste parts of it here, liking to hear some feedback from the original proposer and the area experts.

I do agree that types are a more suitable key than strings, but also, I would add, that I am not as fond as the original proposer to model the key itself - a generic type parameter with no constraints - is truly generic enough for custom logic to be built atop by a more advanced user.

The idea is just to add an interface and a default implementation for a contextual service provider - which by definition, does not break any existing DI solution, but the authors would have to think about how to support the new more advanced scenarios from the core library.

API proposal

namespace Microsoft.Extensions.DependencyInjection.Contexts;

// the gist
public interface IContextualServiceProvider<Context>
{
    IServiceProvider ServiceProvider { get; }
}

// the provided default implementation
public class ContextualServiceProvider<Context> : IContextualServiceProvider<Context>
{
    public IServiceProvider ServiceProvider { get; }

    public ContextualServiceProvider(IServiceProvider serviceProvider) => this.ServiceProvider = serviceProvider;
}

// for extension purposes only
public interface IContextualServiceProviderFactory<Context>
{
   IContextualServiceProvider<Context> CreateNew();
}

API usage

// ServiceContexts.cs
public class ApiControllerContext {}
public class InfrastructureContext {}

// ApiController.cs
public class ApiController : Controller
{
    private readonly IServiceProvider AppServiceProvider;
    private readonly IContextualServiceProvider<ApiControllerContext> ApiControllerServiceProvider;
    private readonly IContextualServiceProvider<InfrastructureContext> InfrastructureServiceProvider;
}

// Program.cs
builder.Services.ConfigureServices(services =>
    services.AddContext<ApiControllerContext>(ConfigureApiControllerContext);
    services.AddContext<InfrastructureContext>(ConfigureInfrastructureContext);
);

The proposal does extend the IServiceCollection with a UseContext method, which essentially configures a nested service provider with its own services and registers it into the root provider. Internally, it builds a new ServiceCollection to use for it's own configuration and resolution.

The thorn in some purists eyes may well be the empty classes which are used as markers (or indeed, keys) to access the nested service providers.

Fear not, even Haskell has used the same method of abusing the type system to implement reflection, ie. coercing the type system to produce runtime proofs of type equalities. (see Peyton, Jones, Simon, "Reflecting on types").

Meaning, using memberless classes as keys should not be considered as something out of this world - and indeed, they may not be memberless at all, you could include some application- or context-level data there, and they could indeed be registered for DI, it's just that the Contextual Service Provider parameterized by their type would not necessarily, nor even logically need those instances.

Because, in general

G(TParam) is not TParam, also, G(TParam) doesn't have TParam,

other relations would also hold,

it is only in special cases that there would be a morphism (ie. concrete method) between for example, the generic type and the type which is the type param, or similarly in the obverse case.

Concerning DI, those cases are fully admonished by the existing compiler infrastructure. Which serves our not-breaking-things purpose well.

However, concerning exactly DI and IoC, a type registered in the container, which is generic over a TParam, could equally as well be a service provider, even providing TParam itself! Or indeed, a keyed service provider. This breaks no existing DI as those libraries should already support resolving generic closed types, or indeed, open generic types.

Now, imagine that that type (or interface) is included in the core library, well then, you have a contextual DI story, my friend.

benjaminpetit commented 1 year ago

Thanks @commonsensesoftware and vukovinski for your propositions. I spent some time prototyping several solutions, using different DI implemenation, and I have a proposal.

I currently have a small prototype that works with the default ServiceProvider and Autofac.

API Proposal

Mandatory or optional?

First the question is, should keyed service be mandatory for all DI implementation, or should it be optional? If it is optional, should we provide a wrapper so users can use keyed DI even if their DI system doesn't support it?

The best solution would be to have mandatory built-in support. A wrapper to enable keyed services would be nice, but could be hard to make sure it will be compatible with all existing DI implementation.

The key type

The service key is an object. I don't see a clear benefit to have a generic parameter here. With object the user will be able to use any type they wants.

Type seems to be a bit too restrictive. String seems to be what people wants most, but object shouldn't be harder to implement and will be much more flexible.

I get the sentiment against magic strings, but what about people that wants to configure their system from config files?

Service registration

A new member on ServiceDescriptor will be added:

public class ServiceDescriptor
{
    [...]
    /// <summary>
    /// Get the key of the service, if applicable.
    /// </summary>
    public object? ServiceKey { get; }
    [...]
}

ServiceKey will stay null in non-keyed services.

Extension methods for IServiceCollection are added to support keyed services:

public static class ServiceCollectionKeyedServiceExtensions
{
    [...]
    public static IServiceCollection AddKeyedSingleton<TService>(
        this IServiceCollection services,
        object serviceKey,
        TService implementationInstance)
    [...]
}

Of course AddKeyedTransient and AddKeyedScoped will also be supported (I didn't pasted all possible overload, but factory and types are supported).

I think it's important that all new methods supporting Keyed service have a different name from the non-keyed equivalent, to avoid ambiguity.

Resolving service

A new (optional?) interface will be introduced:

public interface ISupportKeyedService
{
    /// <summary>
    /// Gets the service object of the specified type.
    /// </summary>
    /// <param name="serviceType">An object that specifies the type of service object to get.</param>
    /// <param name="serviceKey">An object that specifies the key of service object to get.</param>
    /// <returns> A service object of type serviceType. -or- null if there is no service object of type serviceType.</returns>
    object GetKeyedService(Type serviceType, object serviceKey);
}

If we believe it should be mandatory, we should add it directly to IServiceProvider.

Open questions

Attributes

Resolving and registration via attributes is out of scope for the moment, but we could introduce in the future. We need to make sure that the implementation doesn't make that impossible.

Passing the key to the service constructor

I think it would be nice to have a way to pass the ServiceKey as a parameter to the service factory:

collection.AddKeyedSingleton<IMyService>("some-key", (sp, key) => myFactory(sp, key))

And even better, support key injection in the constructor:


public class MyService
{
    [...]
    public MyService([ServiceKey] key)
    [...]
}
collection.AddKeyedSingleton<IMyService, MyService>("some-key");
mendozagit commented 1 year ago

I like these proposal πŸ‘

vukovinski commented 1 year ago

I like it as well πŸ˜„

commonsensesoftware commented 1 year ago

@benjaminpetit At a high level, I have no objection to this type of proposal. It's irrelevant to me if the key is an object, but that is certainly flexible in allowing any type of key. This approach to the problem has existed since, at least, the Common Service Locator interface circa 2008.

This type of approach is simple and reasonable on the surface, but introduces several challenges:

  • IServiceProvider has existed since .NET 1.0 and changing it is not really an option
    • A default interface implementation solves some problems, but then limits where it can be used
  • Adding a new interface adds a number of branch evaluation paths
  • Any value aside from the Type at the call site will require an attribute methinks
    • An attribute adds challenges to runtime evaluation
    • An attribute adds complexity to the forthcoming source code generation
    • An attribute exacerbates the hidden dependency problem as the method signature implies any implementation can be provided, but that is not true

As proposed, [ServiceKey] will not work. It would have to be something like [ServiceKey("some-key")] or [ServiceKey(typeof(SomeKey))].

A more fundamental issue IMO is that an addition to IServiceProvider or alternate interface with an attribute approach can lead to a runtime breaking change; that's bad. For example, swapping out DI container cannot seamlessly and safely be done. If the swapped container implementation doesn't support these, things fail in a pretty spectacular way at runtime. While there may be a requirement to provide an adapter to pair with a particular container implementation, the original proposal will not lead to a runtime failure, but it might lead to a container resolution failure, which is something that can already happen today.

I don't mean to split hairs on names, but Keyed probably isn't necessary as the presence of the key in the methods make that fairly obvious. I'm all for making things more succinct. For example, I'm onboard to shorten things up to:

public class MyComponent
{
   public MyComponent(IKeyed<SomeKey, IMyService> service) { }
}

I'm more interested in the concept than the names, which I would expect to change; however, I thought I'd throw that out there.

I ❀️ all of the additional ideas. I think it will take throwing a bunch of ideas at the problem and seeing what sticks.

rafal-mz commented 1 year ago

The proposal using static KeyedType class is counter intuitive for me:

  • Does it assume an API customer to create a wrapper type, that will represent a key (no string registration allowed)?
  • Looking at the API usage example, the resolved type is injecting IDependency<T1, T2>. IMHO it would be hard to discover, and also it would require changing class declarations to use (more on why I think this is disadvantage below).

The latest proposal is IMHO a step forward. Though, I feel strongly against APIs that are introduced in the class definitions. For me it is DI leaky abstraction. So far, the class that is injecting the dependency is fully abstracted away from any information about registration (lifetime of the dependency, resolve time, exact type to be resolved). When we will introduce something like [ServiceKey] (or public MyComponent(IKeyed<SomeKey, IMyService> service) { }) it will codify DI configuration data as part of the class definition. From the library author perspective, it requires part of my library configuration to be hardcoded. It also does not allow to use named instances feature in existing code that is not refactored to inject IKeyed<t1,t2>.

I also don't understand the flexibility requirement to use object as the key type. I am assuming that the DI registration is performed at the top level, so we can use either strings or static object instances. I don't see the scenario that object type enables, thus I would go with string-key API.

benjaminpetit commented 1 year ago

@commonsensesoftware

As proposed, [ServiceKey] will not work. It would have to be something like [ServiceKey("some-key")] or [ServiceKey(typeof(SomeKey))].

I am not sure to understand here? [ServiceKey] would just be a marker so the service provider knows to use the key here. If it brings too much complexity (at least for now), we can drop it, I consider it a "nice to have", especially if the factory method supports it oob.

A more fundamental issue IMO is that an addition to IServiceProvider or alternate interface with an attribute approach can lead to a runtime breaking change; that's bad. For example, swapping out DI container cannot seamlessly and safely be done. If the swapped container implementation doesn't support these, things fail in a pretty spectacular way at runtime. While there may be a requirement to provide an adapter to pair with a particular container implementation, the original proposal will not lead to a runtime failure, but it might lead to a container resolution failure, which is something that can already happen today.

We could do like what was done with IServiceProviderIsService or ISupportRequiredService and check that the container supports keyed service. If not, it could throw an error.

I don't mean to split hairs on names, but Keyed probably isn't necessary as the presence of the key in the methods make that fairly obvious. I'm all for making things more succinct

I agree with your, but unfortunately, if we don't put the Keyed in the method names, there will be some ambiguous methods:

// Non keyed
public static IServiceCollection AddSingleton(this IServiceCollection services, System.Type serviceType);
// Keyed equivalent:
public static IServiceCollection AddSingleton(this IServiceCollection services, System.Type serviceType, object serviceKey);
// That clashes with the existing:
public static IServiceCollection AddSingleton(this IServiceCollection services, System.Type serviceType, object implementationInstance);

@rafal-mz

The latest proposal is IMHO a step forward. Though, I feel strongly against APIs that are introduced in the class definitions. For me it is DI leaky abstraction. So far, the class that is injecting the dependency is fully abstracted away from any information about registration (lifetime of the dependency, resolve time, exact type to be resolved). When we will introduce something like [ServiceKey] (or public MyComponent(IKeyed<SomeKey, IMyService> service) { }) it will codify DI configuration data as part of the class definition. From the library author perspective, it requires part of my library configuration to be hardcoded. It also does not allow to use named instances feature in existing code that is not refactored to inject IKeyed<t1,t2>.

That's a great point. I am more and more convinced that [ServiceKey] wasn't a good idea :)

I also don't understand the flexibility requirement to use object as the key type. I am assuming that the DI registration is performed at the top level, so we can use either strings or static object instances. I don't see the scenario that object type enables, thus I would go with string-key API.

Some users might want to use enum, or Type (we do use Type today as a key in Orleans for example). I don't think using object will make the implementation any different (assuming Equals and GetHashCode are correctly implemented).

commonsensesoftware commented 1 year ago

@rafal-mz

  • There is no requirement that KeyedType be static nor hide an part of its implementation; it felt logical for a factory
    • The proposal is only meant to show how it can work, not how it must work
    • Names, scope, lifetimes, etc in the design are wide open in my mind
  • No; an API consumer does not have to create a wrapper type
    • Existing string registration is possible and allowed
    • The StringDependency shows one approach to mapping a string registration
    • The implementation is prescriptive - by design for an example, but there could be general purpose extensions like it for containers
    • The StructureMap implementation shows how things are glued together
    • Again, this is just one approach. There could be additional variations where the string key is passed in or provided another way
  • You make an excellent point about backward compatibility
    • Is that on the table? I'm clarifying, not opposed.
    • Is there some reason that the existing approaches would not continue to work?
    • My original thoughts were that this is in addition to (e.g. better) as opposed to replacing/disabling any existing methods
  • You make another good point when using configuration
    • Configuration can use a key or not and whatever key they want; it knows all
    • By convention, this has been the bane of our existence. I don't see how convention can be achieved without the introduction of a type, be it an interface, class, etc or an attribute
    • Is symmetry required here? It seems a reasonable compromise that if you don't want or can't annotate the call site with a type or attribute, then the only method of keyed service injection is via configuration. This can be achieved today using a factory activation method. Do we need something more?
  • It sounds like you prefer the shorter IKeyed<T1,T2> form
    • If that that is the preference, I can certainly iterate on that change
    • Although a bit verbose, I think of it the same way as injecting Lazy<T>
    • The only reason it's an interface is that the resolution can vary by container
commonsensesoftware commented 1 year ago

@benjaminpetit

I see how [ServiceKey] can be used as a marker, but then how do you know what the key is?

public class MyComponent
{                                  // ↓ which IMyService is this?
   public MyComponent([ServiceKey] IMyService service) { }
}

The possible solutions I see are (but there may be more):

  1. Undesirable, but use a derived type such as IMyKeyedService : IMyService or abstract MyKeyedService : IMyService
  2. The same approach can be used with [ServiceKey] itself (e.g. [MyKeyedService]), but suffers from the same things as 1.
  3. [ServiceKey] accepts a key value indicating that IMyService is keyed and what the key value is

You're completely correct about how IServiceProvider can retrofit an additional interface. Perhaps throwing an exception is the best and only option. You don't want to conflate resolution failure with an unsupported feature. It's kind of lame that you may not find that out until runtime. I totally goofed on the extension methods. I didn't run into that problem in my proposal, which let to some assumptions ("You know what happens when you make an 'assumption', don't ya?" - Sam Jackson). I see why Keyed would be necessary in that approach.

benjaminpetit commented 1 year ago

Sorry if I wasn't clear enough, [ServiceKey] would be used by the DI to inject the actual key that was used to access this service.

For example, if you do serviceCollection.AddSingleton<MyService>("some-key"); and you have:

public MyService([ServiceKey] object key)

Then key would be the string some-key here.

I totally agree that keyed resolution should throw if it is not supported, otherwise some people will have bad surprises :)

commonsensesoftware commented 1 year ago

@benjaminpetit Got it. How does a dependency resolve the service? I presume that it would be via something like IServiceProvider.GetKeyedService<IMyService>("some-key"), but I'm not connecting the dots as to how that would actually work. As shown, it appears the key value is simply injected into the type and I don't see how will be used to resolve a keyed instance.

benjaminpetit commented 1 year ago

Here I just proposed what the public surface would look like. The implementation would depend on the provider used.

I already have a prototype working with the runtime service provider and with autofac.

commonsensesoftware commented 1 year ago

@benjaminpetit if you have a repo link that can be shared, that could prove beneficial to all. I can revise the overview to include a section of proposed prototypes so people don't have to spelunk the thread, which is quite long. That is probably the best way to share and test what will or won't work. πŸ˜„