Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

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

Closed
lassevk opened this issue Aug 15, 2023 · 9 comments · Fixed by #608
Closed
Assignees
Labels
enhancement New feature or request
Milestone

Comments

@lassevk
Copy link

lassevk commented Aug 15, 2023

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 dadhi self-assigned this Aug 15, 2023
@dadhi dadhi added the enhancement New feature or request label Aug 15, 2023
@dadhi
Copy link
Owner

dadhi commented Oct 17, 2023

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:

  • For the majority of the cases when user hopefully will use a single key, there will be a parity and backward compatibility between MS and DryIoc.

Cons:

  • What happens when a User is resolving the single key in the presence of many? In this approach, we will resolve the first registered service. Or we need to adjust the SelectLastFactory rule to search for the duplicate keys.
  • When resolving the collection of the keyed services by default, DryIoc will return a single service, because the key is unique. It is not clear how to solve this problem

Solution 2

Represent the MS keys as DryIoc metadata

Pros:

  • DryIoc metadata is not unique, so multiple services with the same metadata are possible
  • SelectLastFactory need to be adjusted to filter based on metadata
  • Collection wrapper will return multiple filtered services based on the metadata

Cons:

  • Discrepancy between two features of MS and DryIoc, there should be a differently named APIs to resolve the services from the MS and from the DryIoc. The documentation should clearly state this fact.
  • DryIoc may have the keys in addition to the metadata, which probably will add confusion.

@dadhi dadhi added this to the v6.0.0 milestone Nov 12, 2023
@dadhi
Copy link
Owner

dadhi commented Nov 15, 2023

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 dadhi changed the title Support for keyed services when used as the container for Microsoft.Extensions.Hosting 8 and beyond Support Keyed Services in DryIoc.MS.DI for the Microsoft.Extensions.DependencyInjection V8 Nov 24, 2023
@dadhi dadhi linked a pull request Nov 24, 2023 that will close this issue
@dadhi
Copy link
Owner

dadhi commented Dec 11, 2023

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
Copy link
Owner

dadhi commented Dec 12, 2023

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

dadhi added a commit that referenced this issue Dec 13, 2023
@yallie
Copy link
Contributor

yallie commented Dec 16, 2023

@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
Copy link
Owner

dadhi commented Dec 16, 2023

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

@dadhi
Copy link
Owner

dadhi commented Jan 22, 2024

done

@dadhi dadhi closed this as completed Jan 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants