简体   繁体   中英

Resolving several instances of the same class with different dependency implementations each time

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.

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM