simpleinjector / SimpleInjector

An easy, flexible, and fast Dependency Injection library that promotes best practice to steer developers towards the pit of success.
https://simpleinjector.org
MIT License
1.21k stars 153 forks source link

how to Create a collection generic adapter #869

Closed henriblMSFT closed 3 years ago

henriblMSFT commented 3 years ago

I'm trying to support the creation of generic adapter that works with all type of supported collection and I'm looking for advice on how to best implement this. I have a few ideas but none of them feel ideal.

Essentially I want to be able to register a collection of ServiceA and resolve it as ServiceB

given the following service type:

public interface IRootService<T> { }

public interface IAdapterService<T> { }

public class AdapterService<T> : IAdapterService<T>
{
    public AdapterService(IRootService<T> root) { }
}

ideally I'd like to be able to do this:

var container = new Container();
container.Collection.Register(typeof(IRootService<>), typeof(IRootService<>).Assembly);

container.RegisterCollectionAdapter(typeof(IRootService<>, typeof(IAdapterService<>), typeof(AdapterService<>));

container.GetInstance<IEnumerable<IAdapterService<int>>();
container.GetInstance<IReadOnlyList<IAdapterService<int>>();

While a generic way of doing it would be ideal I'm open to other ideas on how to accomplish this.

I've considered creating my own collection and registering that directly with the container, however it then becomes impossible to register decorators through simple injector

public class AdapterServiceCollection<T> : IEnumerable<IAdapterService<T>>, IReadOnlyList<IAdapterService<T>
{
    private IEnumerable<IRootService<T>> _services;
    public AdapterServiceCollection(IEnumerable<IRootService<T>> services)
    {
        _services = services;
    }

    public IEnumerator<IAdapterService<T>> GetEnumerator()
    {
        _services .Select(h => new AdapterService<T>(h)).GetEnumerator();
    }
}

I've also considered using an UnregsiteredTypeResolution to create a container controlled collection registration based on the existing collection but I'm unsure how to register anything else but an IEnumerable<> as container.Collection.CreateRegistration always returns an IEnumerable registration.

// simplified by limiting registration to a single generic argument type to make the code cleaner
public static void RegisterAdapterExample<T>(this Container container)
{
    container.ResolveUnregisteredType += (sender, e) =>
    {
        if (!IsServiceAdapterCollection(e.UnregisteredServiceType))
            return;

        Type adapterServiceType = typeof(IAdapterService<T>);

        Type collectionType = e.UnregisteredServiceType.GetGenericTypeDefinition();
        var serviceTypeCollection = collectionType.MakeGenericType(adapterServiceType);

        InstanceProducer instanceProducer =
            container.GetRegistration(serviceTypeCollection, throwOnFailure: false);

        if (instanceProducer == null)
        {
            return;
        }

        var registrations = new List<Registration>();
        foreach (KnownRelationship m in instanceProducer.GetRelationships())
        {
            Registration registration = instanceProducer.Lifestyle.CreateRegistration(
                typeof(IAdapterService<T>),
                () => new AdapterService<T>((IRootService<T>)instanceProducer.GetInstance()),
                container);

            registrations.Add(registration);
        }

        var collectionRegistration = container.Collection.CreateRegistration(adapterServiceType, registrations);
        e.Register(collectionRegistration);
    };
}

Would there be a simpler method to accomplish this that I'm not thinking of?

dotnetjunkie commented 3 years ago

If I understand correctly, you are trying to wrap each IRootService<T> implementation in a generic AdapterService<T> and inject them as a collection of IAdapterService<T> into a consumer.

This is not something that is very easy to achieve (not impossible, but it's not something I can present from the top of my head, so it will certainly take more code to achieve).

Instead, would it be a possibility for you to, besides applying the Adapter design pattern, apply the Composite design pattern as well? This allows consumers to depend on a single IAdapterService<T> instead of an IEnumerable<IAdapterService<T>>. This would severely simplify your scenario, because that would lead to a situation similar to your AdapterServiceCollection<T>:

public class AdapterRootServiceComposite<T> : IAdapterService<T>
{
    public AdapterService(IEnumerable<IRootService<T>> roots) { }

    public void Adapt()
    {
        foreach (var root in this.root)
        {
            // Do something useful with all roots.
        }
    }
}

// Registration
container.Collection.Register(typeof(IRootService<>), typeof(IRootService<>).Assembly);

container.Register(
    typeof(IAdapterService<>),
    typeof(AdapterRootServiceComposite<>),
    Lifestyle.Singleton);

container.GetInstance<IAdapterService<int>>().Adapt();

Let me know if that would work for you.

henriblMSFT commented 3 years ago

Unfortunately we need to support a decorator for each instance of IAdapterService<T> so the composite pattern didn't work well in this case.

I went ahead and fully implemented option 2 registering events through unregistered type and it seems to work. I'm assuming this code would not work if the underlying collection was uncontrolled collection but I'm ok with this limitation.

I also can't seem to support registering anything else then IEnumerable<IAdapter<T>> as I couldn't find a way to create a IReadOnlyList<> registration for container controller collection.

dotnetjunkie commented 3 years ago

That's an interesting problem you have at hand. It took me some time to find a suitable solution. My initial tries failed because I tried to use features that are internal to Simple Injector. But I found a somewhat elegant solution. This solution, however, disallows decorating IRootService<T> implementations. If you need this as well, let me know, I'll go back to the drawing board :-)

In order for it to work, you have to change your adapter implementation to the following:

public class AdapterService<T, TRoot> : IAdapterService<T> where TRoot : IRootService<T>
{
    public AdapterService(TRoot root) { }
}

I'll explain in a moment why adding this extra generic argument solves the problem. But first, the registrations:

var rootTypes =
    container.GetTypesToRegister(typeof(IRootService<>), this.GetType().Assembly);

container.Collection.Register(typeof(IAdapterService<>),
    from implementation in rootTypes
    from serviceType in implementation.GetClosedTypesOf(typeof(IRootService<>))
    let genericTypeArgument = serviceType.GetGenericArguments()[0]
    select typeof(AdapterService<,>).MakeGenericType(genericTypeArgument, implementation));

foreach (var type in rootTypes) container.Register(type);

// Register some decorators for IAdapterService<T>
container.RegisterDecorator(typeof(IAdapterService<>), typeof(AdapterServiceDecorator<>));

I created the following three test classes to verify its working:

// Notice that each type implements multiple interfaces. Not sure if you need this.
// But to be sure.
public class RIntDouble : IRootService<int>, IRootService<double> { }
public class RIntChar : IRootService<int>, IRootService<char> { }
public class RDoubleChar : IRootService<double>, IRootService<char> { }

And when I inspect the container's RootRegistrations in the debugger, Simple Injector visualizes these collections as follows:

IEnumerable<IAdapterService<int>>( // Singleton
    AdapterServiceDecorator<int>( // Transient
        AdapterService<int, RIntDouble>( // Transient
            RIntDouble())), // Transient
    AdapterServiceDecorator<int>( // Transient
        AdapterService<int, RIntChar>( // Transient
            RIntChar()))) // Transient

Which roughly translates to the following C#:

new IAdapterService<int>[]
{
    new AdapterServiceDecorator<int>(
        new AdapterService<int, RIntDouble>(
            new RIntDouble())),

    new AdapterServiceDecorator<int>(
        new AdapterService<int, RIntChar>(
            new RIntChar())),
}

So what is going on here. Let's disassemble this:

var rootTypes =
    container.GetTypesToRegister(typeof(IRootService<>), this.GetType().Assembly);

Container.GetTypesToRegister is a helper method that allows Simple Injector to go look for all implementations of a given (generic) interface. GetTypesToRegister is internally used when you register a collection by supplying an assembly. This call results in a list of all concrete, non-generic IRootService<T> implementations in the given assembly.

The next call is the meat and potatoes of the example:

container.Collection.Register(typeof(IAdapterService<>),
    from implementation in rootTypes
    from serviceType in implementation.GetClosedTypesOf(typeof(IRootService<>))
    let genericTypeArgument = serviceType.GetGenericArguments()[0]
    select typeof(AdapterService<,>).MakeGenericType(genericTypeArgument, implementation));

Given the three previously shown example classes, the above call is the equivalent of this:

container.Collection.Register(typeof(IAdapterService<>), new Type[]
{
    typeof(AdapterService<int, RIntDouble>),
    typeof(AdapterService<double, RIntDouble>),
    typeof(AdapterService<int, RIntChar>),
    typeof(AdapterService<char, RIntChar>),
    typeof(AdapterService<double, RDoubleChar>),
    typeof(AdapterService<char, RDoubleChar>),
});

Which, again, whould be the equivalent of having three seperate registrations:

container.Collection.Register(typeof(IAdapterService<int>), new Type[]
{
    typeof(AdapterService<int, RIntDouble>),
    typeof(AdapterService<int, RIntChar>),
});

container.Collection.Register(typeof(IAdapterService<double>), new Type[]
{
    typeof(AdapterService<double, RIntDouble>),
    typeof(AdapterService<double, RDoubleChar>),
});

container.Collection.Register(typeof(IAdapterService<char>), new Type[]
{
    typeof(AdapterService<char, RIntChar>),
    typeof(AdapterService<char, RDoubleChar>),
});

The TRoot generic type argument of AdapterService<T, TRoot> is used as constructor argument:

public AdapterService(TRoot root)

By registering a different type per root service implementation, it allows Simple Injector knows what to inject into each implementation. TRoot will become, for instance, the RIntChar implementation. Using the IRootService<T> abstaction as constructor argument wouldn't work, because there will be more IRootService<T> implementations to choose from.

The concequence of this, however, is that because the concrete type is forced into the constructor, no decorators around IRootService<T> can be applied with this solution.

Because the adapter implementations, such as AdapterService<char, RIntChar> depend on a concrete type, e.g. RIntChar, those concrete types must be registered in Simple Injector as well. This is what the following calls achieves:

foreach (var type in rootTypes) container.Register(type);

I hope you find this useful or at least informative.

henriblMSFT commented 3 years ago

Oh that's clever, we don't currently have a need to decorate the root service so this will do nicely. Thanks