具有运行时数据的条件装饰器

时间:2018-03-06 04:38:04

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

我是不是总是自己实例化这些装饰器?或者我错过了一些Simple Injector功能?

修改

根据Steven的回答,我现在尝试使用范围和IRuntimeConfigurationProvider来构造装饰器,其中包含启用或禁用所需的信息。我提供了更多背景信息。

我的目标是为内部工作流程系统编写插件。合同如下:

public interface IWorkflowAction<T>
{
    async Task<Markdown> Execute(T marshalledData)
}

这份合同是公司范围的,我不能改变它。 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();
    }
}

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

每个请求都使用SendAsync排队,并根据app.config值配置MaxDOP的方式并行执行。

MyActionEngine内的代码是手动构建一个IDataReader并根据配置对象中的值应用所需的装饰器。 一旦所有工作排队,操作块就会被告知不再需要更多数据。然后我们等待完成并退出。

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

1 个答案:

答案 0 :(得分:1)

根据Simple Injector中的运行时信息,不能根据运行时信息有条件地应用装饰器并非如此,但这是一种不鼓励的做法。

不鼓励根据运行时信息进行注册,因为它使您的配置难以验证,因为验证取决于能否构建对象图,这通常是不可能的,因为这些所需的运行时信息通常不可用于验证时间(可能是在应用程序启动时或运行测试套件时)。

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

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

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

这可能如下所示:

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,允许在运行时检索运行时配置,反对将其注入构造函数。 另一种方法是分割运行时选择行为和装饰器的实际行为。当装饰器包含大量逻辑时,这可能很重要。将它们分成两个装饰器会使每个装饰者都有一个责任。这会将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一起注入此选择器类。

注册这个新的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并告诉它注入LineageIdDecorator或注入参数名称的任何构造函数参数(类型IDataReader)使用原始。这将是RuntimeDecoratorSelector

的参数之一

LineageIdDecorator是有条件注册的,但是它被指示注入任何构造函数参数(类型IDataReader),其中参数名称以 decorated 开头。这显然是RuntimeDecoratorSelector的第二个参数。

最后但并非最不重要的是,我们正在注册RuntimeDecoratorSelector。也许令人惊讶的是,它必须有条件地注册。这是因为Simple Injector非常严格,并且会检测多个注册重叠的时间。它迫使你非常清楚你想要什么。不使此注册成为条件,将使其适用于其自己的构造函数参数,这将导致循环依赖。通过声明应将其注入任何消费者,当尚未处理的注册时,我们将此注册作为后备并防止对象图变为循环或模糊。

如此长的故事,如果您希望根据运行时条件阻止构建对象图,则应该将选择逻辑添加到装饰器或创建单独的选择器“装饰器”。如果您无论在构造对象图时如何应用装饰器,您都可以使用CodeSamples项目的运行时装饰器示例。