这将有些疯狂,但是我相信,如果可能的话,它将成为当前任务中最可维护的解决方案。
我们的应用程序使用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
的解析失败时,将解析默认注册。一个完整的例子:
用
createReadDataFileContents
值version
调用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/