Resolve instances by key and auto-registration with SimpleInjector

微笑、不失礼 提交于 2019-12-24 07:48:28

问题


I'm trying to resolve instances by key with SimpleInjector.
In my case, the keys are strings which are coming from a configuration file, and I need the factory to return the correct type based on the string.

I used a similar solution like the one described in the link above, but changed it slightly, so the instances can provide their own keys.
(there will be many classes that implement IFoo, so I'd like to auto-register them with their keys)

Here is the complete working example (.NET Core console app):
(I kept it short for readability, so there's only one class that implements IFoo, and I omitted the auto-register code)

using SimpleInjector;
using System;
using System.Collections.Generic;

namespace SimpleInjectorTest1
{
    public interface IFoo
    {
        string Name { get; }
    }

    public class SpecificFoo : IFoo
    {
        public string Name { get { return "foooo"; } }
    }

    public interface IFooFactory
    {
        void Add(IFoo foo);
        IFoo Create(string fooName);
    }

    public class FooFactory : Dictionary<string, IFoo>, IFooFactory
    {
        public void Add(IFoo foo)
        {
            // use the instance's Name property as dictionary key, so I don't  
            // need to hard-code it in the code which does the registration
            this.Add(foo.Name, foo);
        }

        public IFoo Create(string fooName)
        {
            return this[fooName];
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var container = new Container();

            // TODO: loop everything that implements IFoo, create 
            // an instance and add it to the factory
            var factory = new FooFactory();
            factory.Add(new SpecificFoo());
            container.RegisterSingleton<IFooFactory>(factory);
            container.Verify();

            // usage
            var factory2 = container.GetInstance<IFooFactory>();
            IFoo foo = factory2.Create("foooo");
            Console.WriteLine("Success!");
        }
    }
}

This worked perfectly well in the beginning, until I realized that SpecificFoo (and the other IFoos as well) needs a dependency via SimpleInjector.

So when I add SpecificFoo to the factory, I need to create the instance via SimpleInjector instead of new SpecificFoo().

So I changed my code as shown below:

using SimpleInjector;
using System.Collections.Generic;

namespace SimpleInjectorTest2
{
    // dummy dependency
    public interface IBar { }
    public class Bar : IBar { }

    // marker interface
    public interface IFoo
    {
        string Name { get; }
    }

    public interface ISpecificFoo : IFoo
    {
        // empty by purpose
    }

    public class SpecificFoo : ISpecificFoo, IFoo
    {
        private readonly IBar bar;
        public SpecificFoo(IBar bar) { this.bar = bar; }

        public string Name { get { return "foooo"; } }
    }

    public interface IFooFactory
    {
        void Add(IFoo foo);
        IFoo Create(string fooName);
    }

    public class FooFactory : Dictionary<string, IFoo>, IFooFactory
    {
        public void Add(IFoo foo)
        {
            // use the instance's Name property as dictionary key, so I don't
            // need to hard-code it in the code which does the registration
            this.Add(foo.Name, foo);
        }

        public IFoo Create(string fooName)
        {
            return this[fooName];
        }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var container = new Container();
            container.Register<IBar, Bar>();

            var factory = new FooFactory();

            // TODO: loop everything that implements IFoo, create
            // an instance and add it to the factory
            container.Register<ISpecificFoo, SpecificFoo>();
            factory.Add(container.GetInstance<ISpecificFoo>());

            // The next line throws an exception because of this:
            // https://simpleinjector.readthedocs.io/en/latest/decisions.html#the-container-is-locked-after-the-first-call-to-resolve
            container.RegisterSingleton<IFooFactory>(factory);
        }
    }
}

As already said above, the registration of the factory fails because the container is locked after the GetInstance call.

I know that I could change the factory to inherit from Dictionary<string, Func<IFoo>> instead (like shown in Resolve instances by key in the docs), but then I need to provide the string key on registration, like shown in the example in the docs:

container.RegisterSingle<IRequestHandlerFactory>(new RequestHandlerFactory
{
    { "default", () => container.GetInstance<DefaultRequestHandler>() },
    { "orders", () => container.GetInstance<OrdersRequestHandler>() },
    { "customers", () => container.GetInstance<CustomersRequestHandler>() },
});

How can I use a factory to resolve types by key, but still let the types provide their keys themselves?
I don't want to have to add a line like above in the registration code, each time I add a new class that implements IFoo.

I already read Registration of open generic types (and answers like this one as well), but I think it doesn't apply to my situation, because I need to resolve by string keys.


回答1:


Considering your current design, the simplest solution is to simply move the code that populates the Foo's until after the registration in Simple Injector is complete:

container.Verify();
factory.Add(container.GetInstance<ISpecificFoo>());
factory.Add(container.GetInstance<ISomeOtherFoo>());

But do note that since, the factory is holding on to instances indefinately, you should at least register all your Foo instances as singleton in the container; this allows the container to do analysis and diagnostics on those instances, and it will immediately show you that there is a Lifestyle Mismatch in your current registrations.

But instead of letting the factory have a set of instances, a probably more flexible approach is to let the factory resolve again from the container:

public interface IFooFactory {
    IFoo Create(string fooName);
}

public class FooFactory : Dictionary<string, Type>, IFooFactory
{
    private readonly Container container;
    public FooFactory(Container container) { this.container = container; }

    public void Register<T>(string fooName) where T : IFoo {
        this.container.Register(typeof(T));
        this.Add(name, typeof(T));
    }

    public IFoo Create(string fooName) => this.container.GetInstance(this[fooName]);
}

// Registration
var factory = new FooFactory(container);

container.RegisterSingleton<IFooFactory>(factory);

factory.Register<SpecificFoo>("foooo");
factory.Register<AnotherFoo>("another");

Here the dictionary not only caches Type instances, but makes the registrations in the container as well. This allows the container to do analysis on the complete object graph. Later on, the factory forwards the Create request to the container. The container can build up the complete object graph.

Do note that the Add method is removed from the IFooFactory abstraction. Since application code should never add instances to the dictionarty, this method should be removed (this immediately simplifies testing). Further more, since application code probably never calls IFoo.Name (since it is only used by the factory) it should be removed as well. In my example, I supply the name to the Register<T> method, but another option is to place an attribute on the Foo implementations. The factory method can read that attribute from the supplied implementation and this prevents you from having to supply it yourself. Since those values come from a configuration file however, it seems reasonable to not let the implementation to be concerned with that name. Having the attribute on the implementations, hides naming from the abstraction, which is good, since consumers don't need to know about that name (or they already know about that name anyway).

Downside of this solution is that decorators for IFoo can't be applied here, since types are resolved by their concrete type. If this is a requirement, you can solve this by applying the RequestHandlerFactory example from the resolve instances by key that stores InstanceProcucer instances as dictionary value.




回答2:


Here is a complete implementation using generics:

public interface IKeyedFactory<TKey, out TValue> : IDictionary<TKey, Type>
{
    TValue Get(TKey match);
}

public class KeyedFactory<TKey, TValue> : Dictionary<TKey, Type>, IKeyedFactory<TKey, TValue>
{
    private readonly Container _container;
    private readonly bool _autoRegister;
    private readonly Lifestyle _lifestyle;

     public KeyedFactory(Container container) : this(container, false, null)
    {
    }

     public KeyedFactory(Container container, Lifestyle lifestyle) : this(container, true, lifestyle)
    {
    }

     private KeyedFactory(Container container, bool autoRegister, Lifestyle lifestyle)
    {
        _container = container;
        _autoRegister = autoRegister;
        _lifestyle = lifestyle;
    }

     public new void Add(TKey key, Type value)
    {
        if (_autoRegister)
        {
            _container.Register(value, value, _lifestyle);
        }

         base.Add(key, value);
    }

     public TValue Get(TKey source) =>
        (TValue)_container.GetInstance(this[source]);
}

public static class ContainerExtensions
{
    public static TValue GetInstance<TFactory, TKey, TValue>(this Container container, TKey match) where TFactory : class, IKeyedFactory<TKey, TValue>
    {
        var factory = (IKeyedFactory<TKey, TValue>)container.GetInstance<TFactory>();
        return factory.Get(match);
    }
}

Usage

Registration:

    interface IVerificationHandler
    {
    }

     class RemoteVerificationHandler : IVerificationHandler
    {
    }

     class InMemoryVerificationHandler : IVerificationHandler
    {
    }

     enum Source
    {
        Remote,
        InMemory
    }

     void AutoRegistrationUsage()
    {
        var container = new Container();
         //Register keyed factory by specifying key (Source) and value (IVerificationHandler)
        container.RegisterInstance<IKeyedFactory<Source, IVerificationHandler>>(new KeyedFactory<Source, IVerificationHandler>(container, Lifestyle.Transient)
        {
            { Source.InMemory, typeof(InMemoryVerificationHandler) },
            { Source.Remote, typeof(RemoteVerificationHandler) }
        });
    }

     void ManualRegistrationUsage()
    {
        var container = new Container();
         //Register keyed factory by specifying key (Source) and value (IVerificationHandler)
        container.RegisterInstance<IKeyedFactory<Source, IVerificationHandler>>(new KeyedFactory<Source, IVerificationHandler>(container, Lifestyle.Transient)
        {
            { Source.InMemory, typeof(InMemoryVerificationHandler) },
            { Source.Remote, typeof(RemoteVerificationHandler) }
        });
    }

Resolution:

    class DependencyInjectionRoot
    {
        private readonly IKeyedFactory<Source, IVerificationHandler> _factory;

         public DependencyInjectionRoot(IKeyedFactory<Source, IVerificationHandler> factory)
        {
            _factory = factory;
        }

         public void AccessDependency(Source key)
        {
            IVerificationHandler dependency = _factory.Get(key);
        }

    }

     public void ResolutionWithDependencyInjection()
    {
        var container = new Container();
        //...Register factory
        container.Register<DependencyInjectionRoot>();
        var dependencyRoot = container.GetInstance<DependencyInjectionRoot>();
        dependencyRoot.AccessDependency(Source.Remote);
    }

     public void ResolutionWithDirectContainerAccess()
    {
        var container = new Container();
        //...Register factory
        var factory = container.GetInstance<IKeyedFactory<Source, IVerificationHandler>>();
        var resolvedInstance = factory.Get(Source.Remote);
    }

     public void ResolutionWithDirectContainerAccessUsingExtensionMethod()
    {
        var container = new Container();
        //...Register factory
        var resolvedInstance = container.GetInstance<IKeyedFactory<Source, IVerificationHandler>, Source, IVerificationHandler>(Source.Remote);
    } 


来源:https://stackoverflow.com/questions/37911971/resolve-instances-by-key-and-auto-registration-with-simpleinjector

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!