Ronny John
Ronny John

Reputation: 3

Custom lifetime scope per request in ASP.NET Core without DependencyResolver

I am in the process of migrating a ASP.NET MVC (net48) application to ASP.NET Core (net6+). In this application we make heavy use of tagged autofac scopes. In net48 we used the AutofacDependencyResolver with a custom LifetimeScopeProvider to create a tagged lifetime scope for each request, depending for example on the area of the request. This allowed us to have different service implementations registered for each area among other things.

This is how we registered the custom lifetime scope provider for ASP.NET MVC and WebApi:

var customLifetimeScopeProvider = new CustomLifetimeScopeProvider(container);
DependencyResolver.SetResolver(new AutofacDependencyResolver(container, customLifetimeScopeProvider));
GlobalConfiguration.Configuration.DependencyResolver = new CustomWebApiDependencyResolver(container, customLifetimeScopeProvider);

Our custom lifetime scope provider would then dynamically create tagged autofac scopes based on the request:

public class CustomLifetimeScopeProvider : ILifetimeScopeProvider {
    ...

    public ILifetimeScope GetLifetimeScope(Action<ContainerBuilder> configurationAction) {
        var request = HttpContext.Current.Request;
        var scope = CreateTaggedScopeBasedOnRequest(request, configurationAction);
        return scope;
    }

    ...
}

I haven't found a way to replicate this functionality in ASP.NET Core. There is no DependencyResolver anymore because the framework handles dependency injection on its own.

Is there a way to create a custom lifetime scope for each request somewhere, that will be used to resolve all dependencies in controllers, views, view components, action filters, middlewares and so on?

Edit

I am still using autofac in the migrated app and have no problem with getting autofac to work. But i haven't found a way to manually create the scope that will be used by the framework to resolve controllers, views etc. I know that generally this is not necessary anymore and hence there is no DependencyResolver. BUT in our application we need to create dynamically tagged Scopes because this is how we build our architecture in .net framework where this was possible.

Example

We have two ScopeTags: "Area1" and "Area2" and two services that are registered for the same interface depending on the tag:

builder.Register<Area1Service>.As<IService>.InstancePerMatchingLifetimeScope("Area1");
builder.Register<Area2Service>.As<IService>.InstancePerMatchingLifetimeScope("Area2");

We have a controller in each area that has the service as a dependency:

public class MyController: Controller {
  public MyController(IService service) {
    ...
  }
}

Request 1: example.org/area1/controller/action => A scope with tag "Area1" is created and the controller gets a instance of "Area1SService".

Request 2: example.org/area2/controller/action => A scope with tag "Area2" is created and the controller gets a instance of "Area2Service"

In our real application the logic is not quite as simple as in this example, but it shows how we made use of the tagged scope feature in combination with the DependencyResolver/ LifetimeScopeResolver-Mechanism in the old .net framework.

Upvotes: 0

Views: 2012

Answers (1)

Travis Illig
Travis Illig

Reputation: 23924

You probably noticed, both through experience and documentation, that Autofac is no longer what actually spawns the request lifetime in ASP.NET Core - it's the framework itself, using the Microsoft.Extensions.DependencyInjection package:

Use InstancePerLifetimeScope instead of InstancePerRequest. In previous ASP.NET integration you could register a dependency as InstancePerRequest which would ensure only one instance of the dependency would be created per HTTP request. This worked because Autofac was in charge of setting up the per-request lifetime scope. With the introduction of Microsoft.Extensions.DependencyInjection, the creation of per-request and other child lifetime scopes is now part of the conforming container provided by the framework, so all child lifetime scopes are treated equally - there’s no special “request level scope” anymore. Instead of registering your dependencies InstancePerRequest, use InstancePerLifetimeScope and you should get the same behavior. Note if you are creating your own lifetime scopes during web requests, you will get a new instance in these child scopes.

I'm not trying to avoid the question, but it's important that you understand what you're getting into, for a couple of reasons:

  • Your search parameters when you're looking for answers should change. This isn't really an Autofac question anymore, it's an ASP.NET Core question in combination with Microsoft.Extensions.DependencyInjection now. You should be searching for more general things about how to control lifetime scope creation in ASP.NET Core and not limiting the search to Autofac.
  • It's reasonably supported, it's just not obvious how to make it happen. You may have to tinker with it a bit.

So, knowing all that, I'll provide you with the moving pieces and a link to a repo where we have a sort of example, but I'm not going to just write all the code for you.

The moving pieces:

  • HttpContext.Features - The pipeline relies on several registered "features" which is sort of like a dependency resolver for pipeline components. The one you care about is...
  • IServiceProvidersFeature - This is the feature that manages the request lifetime scope. The default implementation creates the scope and, on dispose, removes the scope. You'll use this in your solution.
  • IServiceProviderFactory - The RequestServicesFeature needs one of these to create the scope. You'll implement a custom version of this to create your Autofac scope.
  • Middleware - you'll need some middleware that runs first thing in the pipeline to make sure you get there in time to replace the default IServiceProvidersFeature before anything has a chance to resolve anything from the request lifetime scope.

That's the stuff you'll need to work with.

  1. Your middleware runs, grabs HttpContext.Features, replaces the IServiceProvidersFeature with your custom version.
  2. The first time something in the pipeline (e.g., controller resolution) needs something from the request, it'll ask HttpContext.RequestServices.
  3. HttpContext.RequestServices will get your custom IServiceProvidersFeature and ask for the lifetime scope.
  4. Your IServiceProvidersFeature will create the scope if needed and return it.
  5. At the end of the request, your IServiceProvidersFeature will get disposed and that's where you dispose the lifetime scope.

As for the working example, check out the Autofac.AspNetCore.Multitenant repo. We had to do this for multitenant-aware request lifetimes. It has a little more in it than you'll need, but you should get the idea.

  • When you register multitenant services, it registers an IStartupFilter that will run first thing before other middleware gets registered. The startup filter is where the middleware gets registered.
  • The middleware
    • Grabs any existing IServiceProvidersFeature.
    • Replaces it with a new IServiceProvidersFeature that uses the custom IServiceProviderFactory.
    • Registers the custom IServiceProvidersFeature for disposal at the end of the request.
    • After the request pipeline executes, it puts back the original IServiceProvidersFeature just in case something downstream needs that at the end of the request.
  • The custom service provider factory is what is responsible for creating the lifetime scope.

Now I've given you that, let me give you a warnings because this may not actually do what you think it's going to do. This is the part where you're going to have to test and see if things are working as expected.

ASP.NET Core assumes scopes are flat. That is, it assumes that if it calls IServiceProvider.CreateScope() to create a scope off the container, and then you call IServiceScope.CreateScope() to create another scope... that every scope is actually right from the container. For folks used to Autofac, this is kind of weird and a change to how one might think about scopes. You can dig into some discussion on it here.

For implementing the IServiceProvider interface along with IServiceProviderFactory in the default case (that's Autofac.Extensions.DependencyInjection) there was a lot of work to make sure everything stayed flat. You may be able to use the code in that library as a bootstrap for figuring out how to do your own factory.

The problem you may have is if someone inside a controller calls HttpContext.RequestServices.CreateScope() - from an Autofac perspective, you'd want that to be a child of your named scope, and that the created scope will not be named. (That may be tricky to achieve.) From a Microsoft perspective, they think it's going to be a child of the container, not a child of the request scope so you may encounter some weirdness in the case of background/scheduled tasks that make the assumption scopes are flat.

Long story too long - this is probably possible, but it's not "I'll just override this one method and magic will happen." It's going to be a lot of work and testing to figure out the right incantation to make it happen.

Upvotes: 2

Related Questions