c# - 使用级联后备解决键控子图

标签 c# dependency-injection autofac

这将有些疯狂,但是我相信,如果可能的话,它将成为当前任务中最可维护的解决方案。

我们的应用程序使用Autofac进行依赖项注入。

我们使用自定义数据文件格式,出于技术(性能/存储空间优化)或域方面的原因,我们需要能够对其进行发展。该应用程序将始终只写入该格式的最新版本,但也需要能够读取所有以前的版本。通常,版本之间的演变是相当渐进的,仅在几个地方进行了更改,因此,许多用于阅读它的代码将保持不变。

文件格式版本号在文件开头存储为整数。读取任何版本的文件格式将始终导致相同的数据结构,在此称为Scenario

可以从文件读取数据的类依赖于IReadDataFile



public interface IReadDataFile
{
    Scenario From(string fileName);
}


其后是一个非平凡的对象图,用于读取场景的各个部分。但是,每种文件格式版本所需的图看起来都有些不同(说明性示例,不是实际的类型;实际的图要复杂得多):

版本1:

ReadDataFileContents : IReadDataFileContents
└> ReadCoreData : IReadCoreData
└> ReadAdditionalData : IReadAdditionalData
   └> NormalizeName : INormalizeName


版本2:

ReadDataFileContentsV2 : IReadDataFileContents
└> ReadCoreData : IReadCoreData
└> ReadAdditionalDataV2 : IReadAdditionalData
   └> NormalizeNameV2 : INormalizeName
      └> AdditionalNameRegex : IAdditionalNameRegex


版本3:

ReadDataFileContentsV2 : IReadDataFileContents
└> ReadCoreData : IReadCoreData
└> ReadAdditionalDataV3 : IReadAdditionalData
   └> NormalizeNameV2 : INormalizeName
      └> AdditionalNameRegexV3 : IAdditionalNameRegex


(我只考虑像这样的完全独立的图;在单个图中处理此图,并在每次与版本相关的差异时进行切换,显然很快就会变得非常混乱。)

现在,每当调用IReadDataFile.From()方法加载文件时,它都需要获取文件格式版本的适当子图。一个简单的方法是通过注入工厂:

public class ReadDataFile : IReadDataFile
{
    private readonly IGetDataFileVersion getDataFileVersion;
    private readonly Func<int, IReadDataFileContents> createReadDataFileContents;

    public ReadDataFile(
        IGetDataFileVersion getDataFileVersion,
        Func<int, IReadDataFileContents> createReadDataFileContents)
    {
        this.getDataFileVersion = getDataFileVersion;
        this.createReadDataFileContents = createReadDataFileContents;
    }

    public Scenario From(string fileName)
    {
        var version = this.getDataFileVersion.From(fileName);
        var readDataFileContents = this.createReadDataFileContents(version);
        return readDataFileContents.From(fileName);
    }
}


问题是这些子图的注册和解析将如何工作。

手动将完整的子图注册为Keyed<T>非常麻烦,并且容易出错,并且无法很好地扩展其他文件格式版本(尤其是图比示例复杂得多)。

相反,我希望如上所述的整个注册过程如下所示:

builder.RegisterAssemblyTypes(typeof(IReadDataFile).Assembly).AsImplementedInterfaces();

builder.RegisterType<ReadDataFileContents>().As<IReadDataFileContents>();
builder.RegisterType<ReadDataFileContentsV2>().Keyed<IReadDataFileContents>(2);

builder.RegisterType<ReadAdditionalData>().As<IReadAdditionalData>();
builder.RegisterType<ReadAdditionalDataV2>().Keyed<IReadAdditionalData>(2);
builder.RegisterType<ReadAdditionalDataV3>().Keyed<IReadAdditionalData>(3);

builder.RegisterType<NormalizeName>().As<INormalizeName>();
builder.RegisterType<NormalizeNameV2>().Keyed<INormalizeName>(2);

builder.RegisterType<AdditionalNameRegex>().As<IAdditionalNameRegex>();
builder.RegisterType<AdditionalNameRegexV3>().Keyed<IAdditionalNameRegex>(3);

builder.Register<Func<int, IReadDataFileContents>>(c =>
{
    var context = c.Resolve<IComponentContext>();

    return version => // magic happens here
});


这意味着只有在图形之间变化的组件的显式注册。所谓“这里发生了神奇的事情”,我的意思是,要摆脱注册的这一最低限度,决议将不得不承担繁重的工作。

我希望这种方法的工作方式是:对于要解析的每个组件(在此子图中),尝试解析键入到请求的文件格式版本的注册。如果该尝试失败,则为下一个较低版本进行另一尝试,依此类推;当键2的解析失败时,将解析默认注册。

一个完整的例子:


createReadDataFileContentsversion调用3工厂,因此所需的图形是上述文件格式版本3的图形。
尝试使用键IReadDataFileContents解析3。这是不成功的。没有这样的注册。
现在尝试使用键IReadDataFileContents解析2。这样成功了。
构造函数需要一个IReadCoreData。尝试使用键3,然后按2来解决此问题。都失败,因此默认注册得以解决,从而成功。
第二个构造函数参数是IReadAdditionalData;尝试使用键3解决此问题,该键成功。
构造函数需要INormalizeName;键3的解析失败,然后2的尝试成功。
该构造函数反过来需要IAdditionalNameRegex;键3的解析尝试成功。


这里棘手的事情(我不知道该怎么做)是,每次要从version的初始值开始解决的每个独立依赖项,都需要进行版本“倒数”回退过程。

仔细研究一下Autofac API和一些Google搜索,可以发现一些看起来很有趣的东西,但是似乎没有一个提供解决方案的明显途径。


Module.AttachToComponentRegistration()-我已经在其他地方使用它来使用registration.Preparing参与解析过程;但是,只有在找到合适的注册后才引发该事件,并且在此之前似乎没有引发过的事件,也没有在解决方法失败的情况下注册回调的方法(这使我感到惊讶)。
IRegistrationSource-这似乎是实现更通用的注册/解决方案原则的方法,但是如果我确实正在寻找这个地方,我无法理解其中需要做的事情对于。
WithKeyAttribute-我们不能在这里使用它,因为我们需要控制从外部注入的依赖项的“版本”(而且,实际的业务代码将变得依赖于Autofac,这永远都不好。)
ILifetimeScope.ResolveOperationBeginning-这看起来非常有希望,但是该事件仅针对已经成功的解决方案引发。
IIndex<TKey, TValue>-乍看起来确实不错的另一件事,但是它包含已经构造的实例,因此无法获得较低级别分辨率的版本密钥。


从侧面解决的一个问题是将整个内容限制为实际上与此相关的类型,但是我想可以根据需要(基于约定(名称空间等))进行操作。

另一种可能会有所帮助的想法是,在完成所有注册(必须以某种方式确定)之后,可以“填充”“空白”-意味着如果存在以3为键但没有2为键的注册,则将被添加等于默认注册。这将允许使用相同的键来解决子图中的所有依赖关系,并且无需使用“级联后备”机制,这可能是整个过程中最困难的部分。

Autofac有什么方法可以实现?

(另外,感谢您首先阅读此史诗!)

最佳答案

开箱即用,Autofac确实没有这种级别的控制。但是,如果您不介意一点间接性,可以在中间添加一个工厂来构建它。

首先,让我发布一个有效的C#文档,然后对其进行解释。您应该可以将其粘贴到例如.csx scriptcs文档中,然后查看它的内容-这就是我编写的地方。

using Autofac;
using System.Linq;

// Simple interface just used to prove out the
// dependency chain that gets resolved.
public interface IDependencyChain
{
  IEnumerable<Type> DependencyChain { get; }
}

// File reading interfaces
public interface IReadDataFileContents : IDependencyChain { }
public interface IReadCoreData : IDependencyChain { }
public interface IReadAdditionalData : IDependencyChain { }
public interface INormalizeName : IDependencyChain { }
public interface IAdditionalNameRegex : IDependencyChain { }

// File reading implementations
public class ReadDataFileContents : IReadDataFileContents
{
  private readonly IReadCoreData _coreReader;
  private readonly IReadAdditionalData _additionalReader;
  public ReadDataFileContents(IReadCoreData coreReader, IReadAdditionalData additionalReader)
  {
    this._coreReader = coreReader;
    this._additionalReader = additionalReader;
  }

  public IEnumerable<Type> DependencyChain
  {
    get
    {
      yield return this.GetType();
      foreach(var t in this._coreReader.DependencyChain)
      {
        yield return t;
      }
      foreach(var t in this._additionalReader.DependencyChain)
      {
        yield return t;
      }
    }
  }
}

public class ReadDataFileContentsV2 : ReadDataFileContents
{
  public ReadDataFileContentsV2(IReadCoreData coreReader, IReadAdditionalData additionalReader)
    : base(coreReader, additionalReader)
  {
  }
}

public class ReadCoreData : IReadCoreData
{
  public IEnumerable<Type> DependencyChain
  {
    get
    {
      yield return this.GetType();
    }
  }
}

public class ReadAdditionalData : IReadAdditionalData
{
  private readonly INormalizeName _normalizer;
  public ReadAdditionalData(INormalizeName normalizer)
  {
    this._normalizer = normalizer;
  }

  public IEnumerable<Type> DependencyChain
  {
    get
    {
      yield return this.GetType();
      foreach(var t in this._normalizer.DependencyChain)
      {
        yield return t;
      }
    }
  }
}

public class ReadAdditionalDataV2 : ReadAdditionalData
{
  public ReadAdditionalDataV2(INormalizeName normalizer)
    : base(normalizer)
  {
  }
}

public class ReadAdditionalDataV3 : ReadAdditionalDataV2
{
  public ReadAdditionalDataV3(INormalizeName normalizer)
    : base(normalizer)
  {
  }
}

public class NormalizeName : INormalizeName
{
  public IEnumerable<Type> DependencyChain
  {
    get
    {
      yield return this.GetType();
    }
  }
}

public class NormalizeNameV2 : INormalizeName
{
  public readonly IAdditionalNameRegex _nameRegex;
  public NormalizeNameV2(IAdditionalNameRegex nameRegex)
  {
    this._nameRegex = nameRegex;
  }

  public IEnumerable<Type> DependencyChain
  {
    get
    {
      yield return this.GetType();
      foreach(var t in this._nameRegex.DependencyChain)
      {
        yield return t;
      }
    }
  }
}

public class AdditionalNameRegex : IAdditionalNameRegex
{
  public IEnumerable<Type> DependencyChain
  {
    get
    {
      yield return this.GetType();
    }
  }
}

public class AdditionalNameRegexV3 : AdditionalNameRegex { }

// File definition modules - each one registers just the overrides needed
// for the upgraded version of the file type. ModuleV1 registers the base
// stuff that will be used if things aren't overridden. If any version
// of a file format needs to "revert back" to an old mechanism, like if
// V2 needs NormalizeNameV2 and V3 needs NormalizeName, you'd have to re-register
// the base NormalizeName in the V3 module - override the override.
public class ModuleV1 : Module
{
  protected override void Load(ContainerBuilder builder)
  {
    builder.RegisterType<ReadDataFileContents>().As<IReadDataFileContents>();
    builder.RegisterType<ReadCoreData>().As<IReadCoreData>();
    builder.RegisterType<ReadAdditionalData>().As<IReadAdditionalData>();
    builder.RegisterType<NormalizeName>().As<INormalizeName>();
  }
}

public class ModuleV2 : Module
{
  protected override void Load(ContainerBuilder builder)
  {
    builder.RegisterType<ReadDataFileContentsV2>().As<IReadDataFileContents>();
    builder.RegisterType<ReadAdditionalDataV2>().As<IReadAdditionalData>();
    builder.RegisterType<NormalizeNameV2>().As<INormalizeName>();
    builder.RegisterType<AdditionalNameRegex>().As<IAdditionalNameRegex>();
  }
}

public class ModuleV3 : Module
{
  protected override void Load(ContainerBuilder builder)
  {
    builder.RegisterType<ReadAdditionalDataV3>().As<IReadAdditionalData>();
    builder.RegisterType<AdditionalNameRegexV3>().As<IAdditionalNameRegex>();
  }
}

// Something has to know about how file formats are put together - a
// factory of some sort. Here's the thing that "knows." You could probably
// drive this from config or something else, too, but the idea holds.
public class FileReaderFactory
{
  private readonly ILifetimeScope _scope;
  public FileReaderFactory(ILifetimeScope scope)
  {
    // You can always resolve the current lifetime scope as a parameter.
    this._scope = scope;
  }

  public IReadDataFileContents CreateReader(int version)
  {
    using(var readerScope = this._scope.BeginLifetimeScope(b => RegisterFileFormat(b, version)))
    {
      return readerScope.Resolve<IReadDataFileContents>();
    }
  }

  private static void RegisterFileFormat(ContainerBuilder builder, int version)
  {
    switch(version)
    {
      case 1:
        builder.RegisterModule<ModuleV1>();
        break;
      case 2:
        builder.RegisterModule<ModuleV1>();
        builder.RegisterModule<ModuleV2>();
        break;
      case 3:
      default:
        builder.RegisterModule<ModuleV1>();
        builder.RegisterModule<ModuleV2>();
        builder.RegisterModule<ModuleV3>();
        break;
    }
  }
}


// Only register the factory and other common dependencies - not the file
// format readers. The factory will be responsible for managing the readers.
// Note that since readers do resolve from a child of the current lifetime
// scope, they can use common dependencies that you'd register in the
// container.
var builder = new ContainerBuilder();
builder.RegisterType<FileReaderFactory>();
var container = builder.Build();
using(var scope = container.BeginLifetimeScope())
{
  var factory = scope.Resolve<FileReaderFactory>();

  for(int i = 1; i <=3; i++)
  {
    Console.WriteLine("Version {0}:", i);
    var reader = factory.CreateReader(i);
    foreach(var t in reader.DependencyChain)
    {
      Console.WriteLine("* {0}", t);
    }
  }
}


如果运行此命令,则控制台输出将产生所需结果中概述的正确的文件读取依存关系树:

Version 1:
* Submission#0+ReadDataFileContents
* Submission#0+ReadCoreData
* Submission#0+ReadAdditionalData
* Submission#0+NormalizeName
Version 2:
* Submission#0+ReadDataFileContentsV2
* Submission#0+ReadCoreData
* Submission#0+ReadAdditionalDataV2
* Submission#0+NormalizeNameV2
* Submission#0+AdditionalNameRegex
Version 3:
* Submission#0+ReadDataFileContentsV2
* Submission#0+ReadCoreData
* Submission#0+ReadAdditionalDataV3
* Submission#0+NormalizeNameV2
* Submission#0+AdditionalNameRegexV3


这是想法:

不要使用键控服务或尝试从主容器中解决问题,而应使用子生存期作用域来隔离特定于文件版本的依赖项集。

我的代码中包含一系列Autofac模块,每个文件格式一个。在示例中,这些模块相互构建-文件格式V1需要模块V1;文件格式V2需要模块V1和模块V2覆盖;文件格式V3需要具有模块V2覆盖和模块V3覆盖的模块V1。

在现实生活中,您可以使它们全部独立,但是如果每个版本仅基于最新版本,则维护起来可能会更容易-每个新版本/模块仅需要差异。

然后,我有一个中间工厂类,您将使用该类来获取适当的文件版本阅读器。工厂知道如何将文件格式版本与适当的模块集相关联。在更复杂的情况下,您可能会将其从配置,属性或其他内容中删除,但是用这种方式进行说明比较容易。

当您需要特定的文件格式阅读器时,可以解决工厂问题并要求阅读器。工厂采用当前的生存期范围,并生成一个子范围,为该文件格式注册适当的模块,并解析阅读器。这样,您将以更自然的方式使用Autofac,只是让类型排列而不是与元数据或其他机制作斗争。

当心IDisposable依赖项。如果您采用这种方式,并且任何文件读取依赖项都是可抛弃的,则需要将其注册为Owned或其他名称,这样工厂内部的微小子生存期范围不会实例化,然后立即处理您要处理的东西会需要的。

似乎只启动一个很小的生存期范围似乎很奇怪,但这也是InstancePerOwned东西的工作方式。它在幕后有先例。

哦,要把它带回家,如果您真的想注册该Func<int, IReadDataFileContents>方法,则可以让它解析工厂并在其中调用CreateReader方法。

希望这能解除障碍,或者让您对可以采取的措施有所了解。我不确定Autofac拥有的任何标准的现成机制都可以更自然地处理它,但这似乎可以解决问题。

关于c# - 使用级联后备解决键控子图,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32897868/

相关文章:

c# - 如何从其他类可以使用的外部设置文件创建字典?

c# - 使用 LINQ 将字符串拆分为元组?

c# - 为什么不将所有服务类都集中到一个工厂方法中(而不是注入(inject)接口(interface))?

c# - 使用 Autofac 和 Moq 进行集成测试

caSTLe-windsor - 无需配置即可自动注册所有内容(非通用)的 IoC 容器(程序集到程序集)

c# - C#中的字符串操作优化

c# - 应用安装后媒体资源存放在哪里? C#.net

java - 创建 bean 时出错 - 注入(inject) Autowiring 依赖项失败

java - Spring Autowiring 类与接口(interface)?

asp.net - 如何让 Autofac 在 Orchard CMS 中执行属性注入(inject)