使用级联回退解析键控子图

时间:2015-10-01 22:33:58

标签: 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
});

这意味着只有在图表之间有所不同的组件的显式注册。而且#&#34;魔法发生在这里&#34;,我的意思是,为了在注册中获得这个最低限度,解决方案将不得不进行繁重的工作。

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

一个完整的例子:

  • 调用createReadDataFileContents工厂时version值为3,因此所需的图表是上面给出的文件格式版本3的图表。
  • 尝试使用密钥IReadDataFileContents解决3。这是不成功的;没有这样的注册。
  • 现在尝试使用密钥IReadDataFileContents解决2。这成功了。
  • 构造函数需要IReadCoreData。我们尝试使用密钥3解决此问题,然后2;两者都失败,因此默认注册已解决,成功。
  • 第二个构造函数参数是IReadAdditionalData;尝试使用成功的密钥3解决此问题。
  • 构造函数需要INormalizeName;密钥3的解析失败,然后2的尝试成功。
  • 此构造函数又需要IAdditionalNameRegex;密钥3的解析尝试成功。

这里的棘手问题(以及我能解决该怎么做的问题)是版本&#34;倒计时&#34;每次从<{1}}的初始值开始,每个个别依赖关系都需要进行回退过程。

围绕Autofac API和一些谷歌搜索产生了一些看起来很有趣的东西,但它们似乎都没有为解决方案提供明显的途径。

  • version - 我在其他地方用过这个来使用Module.AttachToComponentRegistration()挂钩解决过程;但是,只有在找到合适的注册时才会引发该事件,并且在此之前看起来不是一个事件,也不是在解决方案失败的情况下注册回调的方法(这让我感到惊讶)
  • registration.Preparing - 这似乎是实施更一般的注册/解决原则的方法,但我无法理解我在其中需要做的事情,以防万一这实际上是我正在寻找的地方。
  • IRegistrationSource - 我们无法在此处使用此功能,因为我们需要控制&#34;版本&#34;从外部注入的依赖项(同样,实际的业务代码将依赖于Autofac,这永远不会好。)
  • WithKeyAttribute - 这看起来非常有希望,但只针对已经成功的决议提出此事件。
  • ILifetimeScope.ResolveOperationBeginning - 另一件看起来非常好的东西,但它包含已构造的实例,这样就无法获得较低级别的分辨率的版本密钥。

要解决的问题是将整个事情限制为与此实际相关的类型,但我想如果需要,可以基于约定(命名空间等)完成。

另一个可能有帮助的想法是,在完成所有注册后(必须以某种方式确定),&#34;间隙&#34;可能会被填满#34; - 意味着如果注册的密钥为3但没有密钥为2,则将添加一个等于默认注册的注册。这将允许使用相同的密钥解析子图中的所有依赖关系,并且不需要那个&#34;级联回退&#34;机制可能是整个事情中最困难的部分。

使用Autofac有什么办法可以实现吗?

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

1 个答案:

答案 0 :(得分:3)

开箱即用,Autofac实际上并没有这种控制水平。但是如果你不介意通过在中间添加一个工厂来解决一点间接问题,你就可以构建它。

首先,让我发布一个有效的C#doc,然后我会解释它。你应该可以将它粘贴到一个.csx脚本文档中,然后看看它 - 这就是我编写它的地方。

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需要模块V1,模块V2覆盖,模块V3覆盖。

在现实生活中你可以使这些都是自包含的,但是如果每个版本都是最后一个版本,那么这可能更容易维护 - 每个新版本/模块只需要差异。

然后我有一个中间工厂类,您可以使用它来获取相应的文件版本阅读器。工厂知道如何将文件格式版本与适当的模块集相关联。在一个更复杂的场景中,你可以通过配置或属性或其他东西驱动它,但更容易用这种方式来说明。

当您需要特定的文件格式阅读器时,您可以解析工厂并询问阅读器。工厂获取当前的生命周期范围并生成子范围,仅为该文件格式注册适当的模块,并解析读取器。通过这种方式,您可以更自然地使用Autofac,只需让类型排队,而不是与元数据或其他机制对抗。

小心IDisposable依赖关系。如果你走这条路线并且你的任何文件阅读依赖项是一次性的,你需要将它们注册为Owned或其他东西工厂内的小孩子生命范围没有实例化,然后立即处理你将需要的东西。

启动一个很小的生命周期范围似乎很奇怪,但这也是InstancePerOwned东西的工作原理。幕后有它的先例。

哦,把它全部带回家,如果你真的想注册那个Func<int, IReadDataFileContents>方法,你可以让它解决工厂并在那里调用CreateReader方法。

希望这可以解除阻碍,或者让您知道某个地方可以接受它。我不确定任何标准的开箱即用机制Autofac可以更自然地处理它,但这似乎解决了这个问题。

相关问题