Autofac, OWIN и временные регистрации по запросу
У меня есть вопрос о создании временной области запроса при использовании конвейера OWIN Web API с Autofac.
Нам необходимо отключить некоторые внешние зависимости по требованию, чтобы наша команда QA могла протестировать их отрицательные тестовые случаи. Я не хотел изменять ЛЮБОЙ код в обычном потоке приложений, поэтому я создал собственное промежуточное ПО, которое проверяет запрос на определенные заголовки QA и, когда они присутствуют, расширяет обычный контейнер с новой временной областью, регистрирует замену объект только для этого вызова, переопределяет 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);
кажутся очень хакерскими и мне они не нравятся. Я искал все вокруг, но я не нашел способ чисто переопределить объект контекста, или нашел лучший способ реализовать эту функцию.
Я ищу любые предложения для лучшего способа справиться с этим.
1 ответ
Я вижу два способа достижения того, чего вы хотите.
1. Динамическая регистрация
Первая возможность состоит в том, чтобы подражать тому, что делает сам Autofac, чтобы ввести текущий HttpRequestMessage
при интеграции с ASP.NET Web API.
Вы можете посмотреть, как это делается здесь. Что он делает, это создает другой ContainerBuilder
регистрирует нужный тип и вызывает Update
метод на протяжении всей жизни ComponentRegistry
,
Применительно к вашему сценарию это может выглядеть примерно так:
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 использует динамическую регистрацию после сборки контейнера в приведенном выше примере, Update
метод на ContainerBuilder
помечается как устаревшее со следующим сообщением - для удобства чтения охватывает несколько строк:
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 в области действия времени жизни - как вы можете видеть здесь, вы можете определить для каждого запроса, какой экземпляр 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
}
Регистрация подделки IOtherClient
с InstancePerLifetimeScope
действительно важно, так как это означает, что логика будет выполняться для каждого запроса.
3. Примечания
Я думаю, используя Moq
Вне тестовых проектов это не очень хорошая идея. Я бы предложил создать заглушку реализации IOtherClient
это будет исключение, когда это необходимо. Таким образом, вы можете освободить себя от зависимости, которая не имеет никакого отношения к производственному коду.