Reputation: 2431
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?
Upvotes: 0
Views: 1721
Reputation: 2431
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();
}
}
}
Upvotes: 1