gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-11-30 16:05:57 24 4
gpt4 key购买 nike

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

我们的应用程序使用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/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com