dadhi / DryIoc

DryIoc is fast, small, full-featured IoC Container for .NET
MIT License
1.01k stars 123 forks source link

Support Keyed Services in DryIoc.MS.DI for the Microsoft.Extensions.DependencyInjection V8 #587

Closed lassevk closed 8 months ago

lassevk commented 1 year ago

You probably have it on your radar, but if not then here is a reminder to look into expanding the support for using DryIoc as the container in applications using the Microsoft.Extensions.Hosting framework.

When .NET 8 is released, and the Microsoft.Extensions.Hosting package version 8 is released, Microsoft has finally added keyed service support. The bridge between DryIoc and IServiceCollection need to be added so that these can be used also when DryIoc is the underlying container.

The following example runs with the new hosting nuget package, but uncomment the DryIoc configuration and it throws an InvalidOperationException with the message This service descriptor is keyed. Your service provider may not support keyed services..

using ConsoleApp1;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder();
// builder.ConfigureContainer(new DryIocServiceProviderFactory(new Container()));
builder.Services.AddKeyedTransient<ITest, Test1>("T1");
builder.Services.AddKeyedTransient<ITest, Test2>("T2");

var host = builder.Build();
host.Services.GetKeyedService<ITest>("T1")?.Execute();
host.Services.GetKeyedService<ITest>("T2")?.Execute();

interface and classes for complete code

namespace ConsoleApp1;

public interface ITest
{
    void Execute();
}

public class Test1 : ITest
{
    public void Execute() => Console.WriteLine("Test1.Execute");
}

public class Test2 : ITest
{
    public void Execute() => Console.WriteLine("Test2.Execute");
}
dadhi commented 1 year ago

@lassevk Let's keep this issue open until the IKeyedServiceProvider and its friends are implemented.

dadhi commented 11 months ago

Let's address a challenge of the not-uniqueness of the MS.DI keyed services comparing to the uniqueness of DryIoc keyed services.

Solution 1

Create the composite key for the second, third, etc. MS key adding its index into the pair, e.g. "foo", ("foo", 1), ("foo", 2).

Pros:

Cons:

Solution 2

Represent the MS keys as DryIoc metadata

Pros:

Cons:

dadhi commented 10 months ago

v8 vs v7 API diff https://www.fuget.org/packages/Microsoft.Extensions.DependencyInjection.Abstractions/8.0.0/lib/netstandard2.1/diff/7.0.0/

dadhi commented 10 months ago

Problems to solve

The multiple same key registrations

DryIoc treats the service key as unique ID for the same service type. So it prevents you with registering the same service type with the same service key. The main question, should I use the DryIoc metadata then or can

ServiceDecriptor.KeyedImplementationFactory has the key parameter

The type of KeyedImplementationFactory is Func<IServiceProvider, object?, object> where the object? parameter is the key. Specifically, it is not the registration service key but the resolution/injection key, which may differ (e.g. it may be the AnyKey described below).

Problem is that DryIoc does not supply a service key into the FactoryDelegate which type is Func<IResolverContext, object>.

KeyedService.AnyKey

We need to support the KeyedService.AnyKey to resolve the service with any (not null) service key specified. Therefore, I will be adding Registrator.AnyServiceKey on the DryIoc side.

So far, so good. But what is this thing? See the next section for the reveal :-P

        [Fact]
        public void ResolveKeyedServiceSingletonFactoryWithAnyKeyIgnoreWrongType()
        {
            var serviceCollection = new ServiceCollection();
            serviceCollection.AddKeyedTransient<IService, ServiceWithIntKey>(KeyedService.AnyKey);

            var provider = CreateServiceProvider(serviceCollection);

            Assert.Null(provider.GetService<IService>());
            Assert.NotNull(provider.GetKeyedService<IService>(87));
            Assert.ThrowsAny<InvalidOperationException>(() => provider.GetKeyedService<IService>(new object()));
        }

ServiceKeyAttribute

Marks the parameter to be injected with the resolution service key as in the following case used by the test above:

        internal class ServiceWithIntKey : IService
        {
            private readonly int _id;

            public ServiceWithIntKey([ServiceKey] int id) => _id = id;
        }

Moreover, we need to select the appropriate constructor in presence of attribute

In the example Service we need to select the Constructor with key string id for the keyed resolution and the default one for the non-keyed one.

        internal class Service : IService
        {
            private readonly string _id;

            public Service() => _id = Guid.NewGuid().ToString();

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

            public override string? ToString() => _id;
        }

FromKeyedServicesAttribute

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

            public IService Service1 { get; }

            public IService Service2 { get; }
        }
dadhi commented 9 months ago

Seems like we are doing the same Key for the same service Type in DryIoc.MefAttributedModel. For this purpose, MEF has ServiceKeyStore stores the map from the Key to the all types registered (exported) with this key. And if Type is the same, it increases the number of such registrations and uses this number to augment the service Key and make it unique.

Question, can we use the MEF for MS.DI?... Probably no, but we may steal the ServiceKeyStore or better the whole AttributedModel.WithMultipleSameContractNamesSupport method.

Btw, how to get all non-keyed services in the collection?

//cc @yallie

dadhi commented 9 months ago

Ok, for the last 2 failing tests we need the #618

yallie commented 9 months ago

@dadhi, sorry, I'm on vacation, have no PC to look into it. Will return next Monday. Moving the required code from MEF to DtryIoc core seems like a good idea 👍

dadhi commented 9 months ago

@yallie Enjoy vacation, I have moved the thing already and optimized it along the way.

dadhi commented 8 months ago

done