Search code examples
asp.net-web-api2owinautofacowin-middleware

Autofac, OWIN, and temporary per-request registrations


I have a question about creating a temporary request scope when using the Web API OWIN pipeline with Autofac.

We have the need to disable some external dependencies on demand so our QA team can test their negative test cases. I did not want to change ANY code in the normal application flow, so what I did was create a custom middleware that inspects a request for certain QA headers, and when they are present extends the normal container with a temporary new scope, registers a replacement object only for that call, overrides the autofac:OwinLifetimeScope, then disposes that temporary scope at the end of that call.

This has allowed me to override the normal container behaviour for that request only, but allow all other requests to continue as normal.

Here is a modified sample of my middleware. This code is working fully as expected.

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);
    }
}

However, the lines

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

seem very hacky and I don't like them. I have searched all around, but I have not found a way to cleanly override the context object, or found a better way to implement this functionality.

I'm looking for any suggestions for a better way to handle this.


Solution

  • I can see two ways of achieving what you want to.

    1. Dynamic registration

    The first possibility is to mimic what Autofac itself does to inject the current HttpRequestMessage when integrated with ASP.NET Web API.

    You can have a look at how it's done here. What it does is create another ContainerBuilder, registers the desired type, and calls the Update method on the lifetime scope's ComponentRegistry.

    Applied to your scenario, it could look something like:

    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);
    }
    

    Warning: Even though Autofac itself uses dynamic registration after the container has been built in the example above, the Update method on ContainerBuilder is marked as obsolete with the following message - spanned across several lines for readability:

    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. Conditional registration

    There's 2 drawbacks to the first solution:

    • it uses an obsolete method that could be removed
    • it involves conditional registration of the OWIN middleware so that it's only applied in the QA environment

    Another way would be to register IOtherClient per-request. Since the Autofac OWIN integration registers the OWIN context in the lifetime scope - as you can see here, you could determine for each request which instance of IOtherClient you want to register.

    It could look something like:

    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
    }
    

    Registering the fake IOtherClient with InstancePerLifetimeScope is really important, as it means the logic will be executed for each request.


    3. Notes

    I think using Moq outside of test projects is not a very good idea. I would suggest creating a stub implementation of IOtherClient that would throw an exception when needed. This way you can free yourself of a dependency that has nothing to do in production code.