Autofac、OWIN 和临时 per-request 注册

Autofac, OWIN, and temporary per-request registrations

我对在将 Web API OWIN 管道与 Autofac 结合使用时创建临时请求范围有疑问。

我们需要根据需要禁用一些外部依赖项,以便我们的 QA 团队可以测试他们的负面测试用例。我不想更改正常应用程序流程中的任何代码,所以我所做的是创建一个自定义中间件来检查对某些 QA headers 的请求,并且当它们存在时使用临时新范围扩展普通容器, 仅为该调用注册替换 object,覆盖 autofac:OwinLifetimeScope,然后在该调用结束时处理该临时范围。

这让我可以仅覆盖该请求的正常容器行为,但允许所有其他请求照常继续。

这是我的中间件的修改示例。此代码完全按预期工作。

public override async Task Invoke(IOwinContext context)
{
    var headerKey = ConfigurationManager.AppSettings["QaTest.OfflineVendors.HeaderKey"];

    if (headerKey != null && context.Request.Headers.ContainsKey(headerKey))
    {
        var offlineVendorString = context.Request.Headers[headerKey].ToUpper(); //list of stuff to blow up

        Action<ContainerBuilder> qaRegistration = builder =>
        {
            if (offlineVendorString.Contains("OTHERAPI"))
            {
                var otherClient = new Mock<IOtherClient>();
                otherClient.Setup(x => x.GetValue()).Throws<APIServiceUnavailableException>();
                builder.Register(c => otherClient.Object).As<IOtherClient>();
            }
        };

        using (
            var scope =
                context.GetAutofacLifetimeScope()
                    .BeginLifetimeScope(MatchingScopeLifetimeTags.RequestLifetimeScopeTag, qaRegistration))
        {
            var key = context.Environment.Keys.First(s => s.StartsWith("autofac:OwinLifetimeScope"));
            context.Set(key, scope);

            await this.Next.Invoke(context).ConfigureAwait(false);
        }
    }
    else
    {
        await this.Next.Invoke(context).ConfigureAwait(false);
    }
}

但是,行

var key = context.Environment.Keys.First(s => s.StartsWith("autofac:OwinLifetimeScope"));
context.Set(key, scope);

看起来很老套,我不喜欢它们。我四处搜索,但我没有找到一种方法来完全覆盖上下文 object,也没有找到更好的方法来实现此功能。

我正在寻找更好的处理方法的建议。

我可以看到两种实现您想要的方法。

1。动态注册

第一种可能性是模仿 Autofac 本身在与 ASP.NET Web API.

集成时注入当前 HttpRequestMessage 的行为

你可以看看它是如何完成的here。它所做的是创建另一个 ContainerBuilder,注册所需的类型,并在生命周期范围的 ComponentRegistry.

上调用 Update 方法

应用于您的场景,它可能类似于:

public override Task Invoke(IOwinContext context)
{
    var headerKey = ConfigurationManager.AppSettings["QaTest.OfflineVendors.HeaderKey"];
    if (headerKey != null && context.Request.Headers.ContainsKey(headerKey))
    {
        // Not sure how you use this, I assume you took it out of the logic
        var offlineVendorString = context.Request.Headers[headerKey].ToUpper(); //list of stuff to blow up

        // Get Autofac's lifetime scope from the OWIN context and its associated component registry
        // GetAutofacLifetimeScope is an extension method in the Autofac.Integration.Owin namespace
        var lifetimeScope = context.GetAutofacLifetimeScope();
        var componentRegistry = lifetimeScope.ComponentRegistry;

        // Create a new ContainerBuilder and register your mock
        var builder = new ContainerBuilder();
        var otherClient = new Mock<IOtherClient>();
        otherClient.Setup(x => x.GetValue()).Throws<APIServiceUnavailableException>();
        builder.Register(c => otherClient.Object).As<IOtherClient>();

        // Update the component registry with the ContainerBuilder
        builder.Update(componentRegistry);
    }

    // Also no need to await here, you can just return the Task and it'll
    // be awaited somewhere up the call stack
    return this.Next.Invoke(context);
}

警告:虽然上例中容器已经构建完成后Autofac本身使用了动态注册,但是ContainerBuilder上的Update方法被标记了与以下消息一样已过时 - 跨越多行以提高可读性:

Containers should generally be considered immutable.
Register all of your dependencies before building/resolving.
If you need to change the contents of a container, you technically should rebuild the container.
This method may be removed in a future major release.

2。有条件注册

第一个解决方案有两个缺点:

  • 它使用了可以删除的过时方法
  • 它涉及到OWIN中间件的条件注册,因此它只适用于QA环境

另一种方法是注册IOtherClient 每个请求。由于 Autofac OWIN 集成在生命周期范围内注册了 OWIN 上下文 - 如您所见 here,您可以为每个请求确定要注册 IOtherClient 的哪个实例。

它可能看起来像:

var headerKey = ConfigurationManager.AppSettings["QaTest.OfflineVendors.HeaderKey"];
if (CurrentEnvironment == Env.QA && !string.IsNullOrEmpty(headerKey))
{
    builder
        .Register(x =>
        {
            var context = x.Resolve<IComponentContext>();
            var owinContext = context.Resolve<IOwinContext>();

            // Not sure how you use this, I assume you took it out of the logic
            var offlineVendorString = context.Request.Headers[headerKey].ToUpper();  //list of stuff to blow up

            var otherClient = new Mock<IOtherClient>();
            otherClient.Setup(x => x.GetValue()).Throws<APIServiceUnavailableException>();

            return otherClient.Object;
        })
        .As<IOtherClient>()
        .InstancePerLifetimeScope();
}
else
{
    // normally register the "real" instance of IOtherClient
}

InstancePerLifetimeScope 注册伪造的 IOtherClient 非常重要,因为这意味着将为每个请求执行逻辑。


3。笔记

我认为在测试项目之外使用 Moq 不是一个好主意。我建议创建一个 IOtherClient 的存根实现,它会在需要时抛出异常。这样您就可以摆脱与生产代码无关的依赖关系。