这会有点疯狂,但我相信如果可能的话,它将成为手头任务中最易维护的解决方案。
我们的应用程序使用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有什么办法可以实现吗?
(另外,感谢您首先阅读这部史诗!)
答案 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可以更自然地处理它,但这似乎解决了这个问题。