Reputation: 71
I have a service that will receive configuration dynamically in XML in runtime. I have a service class and I need to create several instances of it providing different dependency implementations for each service class instance. Consider the following example:
interface ILogger { }
class FileLogger : ILogger { }
class DebugLogger : ILogger { }
class ConsoleLogger : ILogger { }
interface IStorage { }
class RegistrySrorage : IStorage { }
class FileStorage : IStorage { }
class DatabaseStorage : IStorage { }
class MyService
{
ILogger _logger;
IStorage _storage;
public MyService(ILogger logger, IStorage storage)
{
_logger = logger;
_storage = storage;
}
}
I can do dependency injection by-hand like this:
IEnumerable<MyService> services = new List<MyService>()
{
new MyService(new FileLogger(), new RegistrySrorage()),
new MyService(new FileLogger(), new DatabaseStorage()),
new MyService(new ConsoleLogger(), new FileStorage()),
new MyService(new DebugLogger(), new FileStorage()),
// same implementations as in previous instance are used but with different
// constructor parameter: for example, different destination in FileStorage
new MyService(new DebugLogger(), new FileStorage()),
};
Is there a way to create an XML configuration and have a DI framework to provide collection of configured MyService instances similar to by-hand example above?
UPDATE
I found solution myself for autofac, but I don't think it's best way to do it.
Created list of services:
<component service="System.Collections.IList, mscorlib" type="System.Collections.ArrayList, mscorlib" name="ServicesList">
<parameters>
<parameter name="c">
<list>
<item value="loggerUID,storageUID"/>
</list>
</parameter>
</parameters>
</component>
Then created list of all required components to resolve dependencies and named them uniquely:
<component service="Test.ILogger"
type="Test.FileLogger"
name="loggerUID">
<parameters>
<parameter name="logFile" value="C:\Temp\MyLogForSvc_1.log" />
</parameters>
</component>
Then in code in first pass I retrieve list of all services (component named "ServicesList"). And in second pass, after loading components from XML, I register all services in code using provided component names as keys (no sanity checks here):
foreach (string cfg in servicesList)
{
string[] impl = cfg.Split(',');
builder.Register<MyService>(c => new MyService(
c.ResolveKeyed<ILogger>(impl[0]),
c.ResolveKeyed<IStorage>(impl[1])))
.Named<MyService>(cfg);
}
IContainer container = builder.Build();
List<MyService> services = new List<MyService>();
foreach (string svcName in servicesList)
services.Add(container.ResolveNamed<MyService>(svcName));
Improvement suggestions are welcomed.
Upvotes: 3
Views: 2254
Reputation: 7661
I'm afraid Autofac is not that flexible. It has support for XML configuration but I'd expect it to support only primitive types as constructor parameters.
On the other hand, your example seems to be a bad usage of dependency injection. DI should basically be used when neither the inject component cares about who uses it, nor the component consumer cares which implementation of service it receives. I would add an identification property to both ILogger
and IStorage
, make MyService
receive all available loggers and storages and inside it implement the logic that handles its specific configuration to determine which combinations to use. Something like this:
public interface ILogger
{
string Id { get; }
}
public class FileLogger : ILogger
{
public string Id { get { return "Logger.File"; } }
}
// etc.
public interface IStorage
{
string Id { get; }
}
public class RegistrySrorage : IStorage
{
public string Id { get { return "Storage.Registry"; } }
}
public class MyService
{
IList<Config> _EnabledConfigs;
public MyService(IEnumerable<ILogger> loggers, IEnumerable<IStorage> storages)
{
_EnabledConfigs = ParseXmlConfigAndCreateRequiredCombinations(loggers, storages);
}
class Config
{
public ILogger Logger { get; set; }
public IStorage Storage { get; set; }
}
}
// container config:
public static void ConfigureContainer(IContainerBuilder builder)
{
builder.RegisterType<FileLogger>.AsImplementedInterfaces();
// other loggers next...
builder.RegisterType<RegisterStorage>.AsImplementedInterfaces();
// then other storages
builder.RegisterType<MyService>();
}
And config goes like this:
<MyServiceConfig>
<EnabledCombinations>
<Combination Logger="Logger.File" Storage="Storage.Registry"/>
<!-- Add other enabled combinations -->
</EnabledCombinations>
</MyServiceConfig>
Think about it. I bet it will make things much easier.
As an option you might create a separate class that is responsible for configuring your MyService
so that the MyService
does not contain the configuration-related logic.
UPDATE
If you really need such complex logic for dependency configurations which is best expressed in c# code, your best bet is using Modules. Just extract the code that configures what you need into a separate Autofac module:
public class MyServiceConfigModule : Module
{
protected override void Load(ContainerBuilder builder)
{
// register some compopnent that uses MyService and initialize it with
// the required set of loggers and storages
builder.Register(ctx => new MyServiceConsumer(
new List<MyService>()
{
new MyService(new FileLogger(), new RegistrySrorage()),
new MyService(new FileLogger(), new DatabaseStorage()),
new MyService(new ConsoleLogger(), new FileStorage()),
new MyService(new DebugLogger(), new FileStorage()),
// same implementations as in previous instance are used but with different
// constructor parameter: for example, different destination in FileStorage
new MyService(new DebugLogger(), new FileStorage()),
}));
}
}
, put it into a separate assembly 'MyServiceConfig', and add a couple of config lines to the app.config
:
<autofac>
<modules>
<module type="MyServiceConfigModule, MyServiceConfig" />
</modules>
</autofac>
When you need to change it, you may write the new module source file, compile it in place (csc.exe is always present on a machine with .NET) and swap the old one with it. Of course, this approach fits only for 'startup-time' configuration.
Upvotes: 1
Reputation: 19117
Yes, autofac allows for traditional xml configuration, this example for instance (taken from the autofac docs)
<autofac defaultAssembly="Autofac.Example.Calculator.Api">
<components>
<component
type="Autofac.Example.Calculator.Addition.Add, Autofac.Example.Calculator.Addition"
service="Autofac.Example.Calculator.Api.IOperation" />
<component
type="Autofac.Example.Calculator.Division.Divide, Autofac.Example.Calculator.Division"
service="Autofac.Example.Calculator.Api.IOperation" >
<parameters>
<parameter name="places" value="4" />
</parameters>
</component>
You could also use an autofac module, which gives some additional control, for instance, you could make just a dictionary between loggers and stores, and configure from that
Upvotes: 0