c# - 带有运行时数据的条件装饰器

标签 c# simple-injector

使用 Simple Injector 我可以看到我可以根据设计时可用的信息注册装饰器,但是是否可以使用运行时数据获得相同的行为?

这是一个简单的例子(实际上还有更多的装饰器):

public class LineageIdDecorator : IDataReader
{
    public LineageIdDecorator(IDataReader dataReader)
    {
        _dataReader = dataReader;
    }
    // Implementation skipped...
}

public class RuntimeConfig
{
    public bool IncludeLineage { get; set; }
    public string ConnectionString { get; set; }
    public string Query { get; set; }
}

public class DataSource
{
    public IDataReader CreateDataReader(RuntimeConfig config)
    {
        var connection = new SqlConnection(config.ConnectionString);
        var command = new SqlCommand(config.Query, connection);
        connection.Open();
        IDataReader dataReader = command.ExecuteQuery();
        if (config.IncludeLineage)
        {
            dataReader = new LineageIdDecorator(dataReader);
        }
        return dataReader;
    }
}

重要的部分是:
if (config.IncludeLineage)
{
    dataReader = new LineageIdDecorator(dataReader);
}    

我是否总是自己实例化这些装饰器?还是我缺少一些简单的注入(inject)器功能?

编辑

根据史蒂文的回答,我现在正在尝试使用范围和 IRuntimeConfigurationProvider 来构造装饰器,其中包含它们需要启用或禁用的信息。我正在提供更多上下文。

我的目标是为内部工作流系统编写一个插件。契约(Contract)如下所示:
public interface IWorkflowAction<T>
{
    async Task<Markdown> Execute(T marshalledData)
}

这份契约(Contract)是全公司范围的,我无法更改。 Markdown 是在与合约相同的内部 nuget 包中提供的类。 T 代表我在操作中期望的配置数据类型。主机采用用户在网站中配置的 JSON,并自动将其具体化为我指定的类型。
public class MyWorkflowAction : IWorkflowAction<List<RuntimeConfiguration>>
{
    private readonly MyActionEngine _engine;
    public MyWorkflowAction()
    {
        container = new Container();
        // register components
        container.Verify();
        _engine = container.GetInstance<MyActionEngine>
    }

    public async Task<Markdown> Execute(List<RuntimeConfiguration> runtimeConfiguration)
    {
        foreach (var config in runtimeConfiguration)
        {
            await _engine.SendAsync(config);
        }
        _engine.Complete();
        await _engine.Completion;
        return new Markdown();
    }
}

这是我的切入点。我创建 MyWorkflowEngine在构造函数中使用 SimpleInjector。 MyWorkflowEngineIActionBlock<T> 的实现来自 TPL 数据流库。

每个请求都使用 SendAsync 进行排队。并根据 app.config 值如何配置 MaxDOP 并行执行。
MyActionEngine里面的代码正在手动构建一个 IDataReader并根据配置对象中的值应用所需的装饰器。
一旦所有工作都已排队, Action block 被告知不要再有数据了。然后我们等待完成并退出。

我很清楚我需要使用 AsyncScopedLifestyle ,但我仍然不清楚如何在运行时构造装饰器,如果它们依赖于 IRuntimeProviderContext,它本身取决于配置对象的当前实例。

最佳答案

与其说你不能在 Simple Injector 中根据运行时信息有条件地应用装饰器,不如说这是一种不鼓励的做法。

不鼓励基于运行时信息进行注册,因为这会使您的配置难以验证,因为验证依赖于能够构造对象图,而这通常是不可能的,因为所需的运行时信息通常在验证时不可用(可能在应用程序启动或运行测试套件时)。

相反,您不应根据运行时信息更改对象图的结构,而应使用此运行时信息来决定使用哪个调用图来处理已构建的对象图。

由于这种阻碍,内置的装饰器工具不允许根据运行时信息有条件地注册装饰器。有examples关于如何在 Github 存储库中的代码示例项目中进行基于运行时的装饰,但它们只是示例,我再次建议不要使用它们。

不是在运行时有条件地应用装饰器,而是不断地应用装饰器,并根据装饰器在调用时检索到的运行时数据,在装饰器内部实现分支。

这可能如下所示:

public class LineageIdDecorator : IDataReader
{
    public LineageIdDecorator(
        IDataReader decoratee, IRuntimeConfigProvider configProvider) { .. }

    // IDataReader methods
    public object DoSomething()
    {
        if (configProvider.Config.IncludeLineage)
        {
            // run decorated behavior
        }

        return decoratee.DoSomething();
    }
}

这里有一个新的抽象 IRuntimeConfigProvider引入了允许在运行时检索运行时配置,反对将其注入(inject)构造函数。
另一种方法是将运行时选择行为和装饰器的实际行为分开。当装饰器包含大量逻辑时,这可能很重要。将它们拆分为两个装饰器将使每个装饰器都有一个单一的职责。这将减少 LineageIdDecorator回到你原来的实现,第二个实现可能看起来很像这样:
public class RuntimeDecoratorSelector : IDataReader
{
    private readonly IDataReader decoratedDecoratee;
    private readonly IDataReader originalDecoratee;
    private readonly IRuntimeConfigProvider configProvider;

    public RuntimeDecoratorSelector(
        IDataReader decoratedDecoratee, IDataReader originalDecoratee,
        IRuntimeConfigProvider configProvider)
    {
        this.decoratedDecoratee = decoratedDecoratee;
        this.originalDecoratee = originalDecoratee;
        this.configProvider = configProvider;
    }

    private IDataReader Decoratee => 
        configProvider.Config.IncludeLineage ? decoratedDecoratee : originalDecoratee;

    // IDataReader methods
    public object DoSomething()
    {
        return Decoratee.DoSomething();
    }
}

将装饰服务作为原始服务与 IRuntimeConfigProvider 一起注入(inject)到此选择器类中。 .

注册这个新的 RuntimeDecoratorSelector 以及原始的 LineageIdDecorator 和实现现在变得有点复杂,因为与装饰器相比,它涉及进行条件注册。以下是如何进行这些注册:
container.RegisterConditional<IDataReader, DataReaderImpl>(
    c => c.Consumer?.ImplementationType == typeof(LineageIdDecorator) 
        || c.Consumer?.Target.Name.StartsWith("original") == true);

container.RegisterConditional<IDataReader, LineageIdDecorator>(
    c => c.Consumer?.Target.Name.StartsWith("decorated") == true);

container.RegisterConditional<IDataReader, RuntimeDecoratorSelector>(c => !c.Handled);

我们在这里做的是注册 DataReaderImpl有条件地告诉它注入(inject)到 LineageIdDecorator或任何构造函数参数(类型为 IDataReader ),其中参数名称以 original 开头。这将是 RuntimeDecoratorSelector 的参数之一.
LineageIdDecorator有条件地注册,但它被指示注入(inject)任何构造函数参数(类型为 IDataReader ),其中参数名称以装饰的开头。这显然是 RuntimeDecoratorSelector 的第二个参数。 .

最后但同样重要的是,我们正在注册 RuntimeDecoratorSelector .也许令人惊讶的是,它也必须有条件地注册。那是因为 Simple Injector 非常严格,并且会在多个注册重叠时进行检测。它迫使您非常明确地说明您想要什么。不使这个注册有条件,会导致它适用于它自己的构造函数参数,这会导致循环依赖。通过声明它应该被注入(inject)到任何消费者中,当尚未处理注册时,我们将此注册作为后备并防止对象图变得循环或模棱两可。

长话短说,如果您希望阻止基于运行时条件构建对象图,您应该将选择逻辑添加到装饰器或创建单独的选择器“装饰器”。如果您无论如何,在对象图构建期间应用装饰器,您可以使用 CodeSamples 项目的运行时装饰器示例。

关于c# - 带有运行时数据的条件装饰器,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49123476/

相关文章:

c# - 从 Action /模型返回 HTML

c# - 与 C#/NET 中的控制台应用程序相关的问题

C# 序列选择

asp.net-web-api - 为什么在 OWIN 中使用带有 WebApi 自托管的简单注入(inject)器时会出现此 ActivationException?

c# - 在 ASP.NET 页面中选择页眉

asp.net - 简单注入(inject)器 - 解析 MVC Controller 时没有为此对象定义无参数构造函数

c# - 基本的 DI/IoC 问题 - 架构,SimpleInjector

c# - Simple Injector 3 不返回通用实例

c# - 将简单注入(inject)器与 SignalR 结合使用

c# - 标准化相对路径?