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
的存根实现,它会在需要时抛出异常。这样您就可以摆脱与生产代码无关的依赖关系。
我对在将 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
的存根实现,它会在需要时抛出异常。这样您就可以摆脱与生产代码无关的依赖关系。