Search code examples
c#inversion-of-controlioc-containersimple-injector

Inject specific values to specific constructor parameters with Simple Injector


I have some dependencies that can be modified in the field via a config file, and I am trying to specify some construtor parameters.

I have a module (which will be registered with the Simple Injector container), the constructor should always contain a Guid for systemId and a dictionary for the settings. It may contain constructor for other items such as a ILogger etc:

public interface IMonitor
{
    void Start();
    void Stop();
}

public class DevLinkMonitor : IMonitor
{
    public DevLinkMonitor(Guid systemId, Dictionary<string,string> settings)
    {
        // store the injected parameters
    }

    public void Start() {}

    public void Stop() {}
}

I then have a class which manages the construction of the monitor objects:

_monitorManager.Add(
    Guid.NewGuid(),
    "System A London",
    new Dictionary<string, string>()
    {
        { "IpAddress", "192.168.1.2" },
        { "Password", "password" }
    },
    typeof(DevLinkMonitor));

_monitorManager.Add(
    Guid.NewGuid(),
    "System B Mars",
    new Dictionary<string, string>()
    {
        { "IpAddress", "192.168.100.10" },
        { "Password", "password" }
    },
    typeof(DevLinkMonitor));

On construction of the monitor object I would like to inject the specific Guid ID and specific settings dictionary (done in CreateAndRun()):

public class MonitorManager
{
    internal class Item
    {
        public readonly string Description;
        public readonly Dictionary<string, string> Settings;
        public readonly Type TypeToCreate;
        public IMonitor Instance { get; set; }

        public Item(string description, Dictionary<string, string> settings, Type typeToCreate)
        {
            Description = description;
            Settings = settings;
            TypeToCreate = typeToCreate;
        }
    }

    private readonly Container _container;
    readonly Dictionary<Guid, Item> _list = new Dictionary<Guid, Item>();

    public MonitorManager(Container container)
    {
        _container = container;
    }

    public void Add(Guid id, string description, Dictionary<string, string> settings, Type typeToCreate)
    {
        if(typeToCreate.GetInterfaces().Contains(typeof(IMonitor)))
           throw new ArgumentException($"{typeToCreate} does not implement {typeof(IMonitor)}", nameof(typeToCreate));

        _list.Add(id, new Item(description, settings, typeToCreate));
    }

    public void CreateAndRun()
    {
        foreach (var item in _list)
        {                
            var id = item.Key; // this is the guid we want to inject on construction
            var value = item.Value;
            var settings = value.Settings; // use this settings dictionary value on injection

            // for each below call, somehow use the values id, and settings
            value.Instance = _container.GetInstance(value.TypeToCreate) as IMonitor;
            if (value.Instance == null)
                throw new InvalidCastException($"Does not implement {typeof(IMonitor)} ");

            value.Instance.Start();
        }
    }

    public void Stop()
    {
        foreach (var value in _list.Select(item => item.Value))
        {
            value.Instance.Stop();
        }
    }
}

Is this possible with Simple Injector? Or does anyone smell a code smell?


Solution

  • I wanted the flexibility of SimpleInjector to create types, without the code smell of defining contracts in my constructor - as Steven rightly said this should form part of IMonitor.

    The thing I didnt like about having things being created in Start() is if Stop() was called before Start()... the plugin framework should of course ensure this is not possible but nonetheless I consider it good practice to put null reference checks on things I am going to use within the class and this bloats code to one extent or the other.

    public void Stop()
    {
        // although unlikely, we would fail big time if Stop() called before Start()
        if(_someService != null && _fileService != null)
        {
            _someService .NotifyShutdown();
            _fileService.WriteLogPosition();
        }   
    }
    

    So instead I went for a plugin architecture with a factory approach, in to the factory I pass a SimpleInjector container which it can use to construct any type it likes.

    public interface IMonitorFactory
    {
        void RegisterContainerAndTypes(Container container);
        IMonitor Create(Guid systemId, Dictionary<string,string> settings);
    }
    
    public class DevLinkMonitorFactory : IMonitorFactory
    {
        private Container _container;
    
        public void RegisterContainerAndTypes(Container container)
        {
            _container = container;
    
            // register all container stuff this plugin uses
            container.Register<IDevLinkTransport, WcfTransport>();
    
            // we could do other stuff such as register simple injector decorators
            // if we want to shift cross-cutting loggin concerns to a wrapper etc etc
        }
    
        public IMonitor Create(Guid systemId, Dictionary<string,string> settings)
        {           
            // logger has already been registered by the main framework
            var logger = _container.GetInstance<ILogger>();
    
            // transport was registered previously
            var transport = _container.GetInstance<IDevLinkTransport>();
    
            var username = settings["Username"];
            var password = settings["Password"];
    
            // proxy creation and wire-up dependencies manually for object being created
            return new DevLinkMonitor(systemId, logger, transport, username, password);
        }
    }
    

    The MonitorManager now has a couple of extra tasks to wireup the initial ContainerRegistration and then call the Create for each. Of course the factory doesnt need to use SimpleInjector but makes life a lot easier should the main framework provide services that the plugin will use.

    Also since the object created via a factory with all required parameters in the constructor there is no need for lots of null checks.

    public interface IMonitor
    {
        void Start();
        void Stop();
    }
    
    public class MonitorManager
    {
        internal class Item
        {
            public readonly string Description;
            public readonly Dictionary<string, string> Settings;
            public IMonitorFactory Factory { get; set; }
            public IMonitor Instance { get; set; }
    
            public Item(string description, Dictionary<string, string> settings, IMonitorFactory factory)
            {
                Description = description;
                Settings = settings;
                Factory = factory;
            }
        }
    
        private readonly Container _container;
        readonly Dictionary<Guid, Item> _list = new Dictionary<Guid, Item>();
    
        public MonitorManager(Container container)
        {
            _container = container;
        }
    
        // some external code would call this for each plugin that is found and
        // either loaded dynamically at runtime or a static list at compile time
        public void Add(Guid id, string description, Dictionary<string, string> settings, IMonitorFactory factory)
        {
            _list.Add(id, new Item(description, settings, factory));
            factory.RegisterContainerAndTypes(_container);
        }
    
        public void CreateAndRun()
        {
            foreach (var item in _list)
            {                
                var id = item.Key; // this is the guid we want to inject on construction
                var value = item.Value;
                var settings = value.Settings; // use this settings dictionary value on injection
    
                var factory = value.Factory;
    
                // for each below call, somehow use the values id, and settings
                value.Instance = factory.Create(id, settings);
    
                value.Instance.Start();
            }
        }
    
        public void Stop()
        {
            foreach (var value in _list.Select(item => item.Value))
            {
                value.Instance?.Stop();
            }
        }
    }