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 это будет исключение, когда это необходимо. Таким образом, вы можете освободить себя от зависимости, которая не имеет никакого отношения к производственному коду.

Другие вопросы по тегам