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

huangapple go评论61阅读模式
英文:

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

问题

我正在迁移一个 ASP.NET MVC (net48) 应用程序到 ASP.NET Core (net6+)。在这个应用程序中,我们大量使用了带标签的 Autofac 作用域。在 net48 中,我们使用 AutofacDependencyResolver 和自定义的 LifetimeScopeProvider 来创建带标签的生命周期范围,根据请求的区域等情况。这使我们能够为每个区域注册不同的服务实现,等等。

以下是我们为 ASP.NET MVC 和 WebApi 注册自定义生命周期范围提供程序的方式:

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

然后,我们的自定义生命周期范围提供程序会根据请求动态创建带标签的 Autofac 作用域:

public class CustomLifetimeScopeProvider : ILifetimeScopeProvider {
    ...

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

    ...
}

我还没有找到在 ASP.NET Core 中复制这个功能的方法。因为框架自行处理依赖注入,所以不再需要 DependencyResolver。

是否有一种方法可以在某个地方为每个请求创建自定义的生命周期范围,以用于解析控制器、视图、视图组件、操作过滤器、中间件等所有依赖项?

编辑

我仍然在迁移后的应用程序中使用 Autofac,而且没有问题可以让 Autofac 工作。但是我还没有找到一种手动创建框架用于解析控制器、视图等的范围的方法。我知道通常不再需要这样做,因此不再需要 DependencyResolver。但是在我们的应用程序中,我们需要创建动态标记的作用域,因为这是我们在 .net framework 中构建架构的方式,而在那里这是可能的。

示例

我们有两个 ScopeTags: "Area1" 和 "Area2",根据标签注册了两个服务,它们实现了相同的接口:

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

我们每个区域都有一个控制器,该控制器将服务作为依赖项:

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

请求 1: example.org/area1/controller/action => 创建一个标签为 "Area1" 的作用域,并且控制器获取一个 "Area1Service" 的实例。

请求 2: example.org/area2/controller/action => 创建一个标签为 "Area2" 的作用域,并且控制器获取一个 "Area2Service" 的实例。

在我们的实际应用程序中,逻辑并不像这个示例中那么简单,但它展示了我们如何在旧的 .net framework 中利用标签作用域功能与 DependencyResolver/ LifetimeScopeResolver 机制结合使用的方式。

英文:

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.

答案1

得分: 0

你可能已经注意到,通过经验和文档,Autofac在ASP.NET Core中不再是实际生成请求生命周期的工具,而是使用Microsoft.Extensions.DependencyInjection包的框架本身:

使用 InstancePerLifetimeScope 而不是 InstancePerRequest 在以前的ASP.NET集成中,您可以将依赖项注册为 InstancePerRequest,这将确保每个HTTP请求只会创建一个依赖项的实例。这是因为Autofac负责设置每个请求的生命周期范围。随着Microsoft.Extensions.DependencyInjection的引入,创建每个请求和其他子生命周期范围现在是框架提供的符合规范的容器的一部分,因此所有子生命周期范围都被平等对待,不再有特殊的“请求级别范围”。不要再注册您的依赖项为 InstancePerRequest,而是使用 InstancePerLifetimeScope,您应该会得到相同的行为。请注意,如果您在Web请求期间创建自己的生命周期范围,那么在这些子范围中将获得一个新实例。

我不是在回避问题,但重要的是您了解您正在做什么,有一些原因:

  • 您在寻找答案时的搜索参数应该发生变化。这不再是一个Autofac问题,而是与Microsoft.Extensions.DependencyInjection结合使用的ASP.NET Core问题。您应该搜索有关如何控制ASP.NET Core中的生命周期范围创建的更一般的内容,而不要将搜索限制在Autofac上。
  • 它在一定程度上得到了支持,只是如何实现它不太明显。您可能需要稍微尝试一下。

因此,了解了所有这些,我将为您提供移动的部分和一个链接到一个包含示例的存储库,但我不会为您编写所有的代码。

移动的部分

  • HttpContext.Features - 流程依赖于多个已注册的“特性”,这有点像流程组件的依赖解析器。您关心的是...
  • IServiceProvidersFeature - 这是管理请求生命周期范围的特性。默认实现创建范围,并在释放时删除范围。您将在解决方案中使用这个。
  • IServiceProviderFactory - RequestServicesFeature需要其中一个来创建范围。您将实现一个自定义版本以创建Autofac范围。
  • 中间件 - 您需要一些中间件,在管道中的第一步运行,以确保您及时替换默认的 IServiceProvidersFeature,在任何东西有机会解析请求生命周期范围之前。

这是您需要使用的内容。

  1. 您的中间件运行,获取HttpContext.Features,替换IServiceProvidersFeature为您自定义的版本。
  2. 管道中的第一次有东西需要从请求中获取(例如,控制器解析),它将请求 HttpContext.RequestServices
  3. HttpContext.RequestServices将获取您的自定义IServiceProvidersFeature并请求生命周期范围。
  4. 您的IServiceProvidersFeature将在需要时创建范围并返回它。
  5. 在请求结束时,您的IServiceProvidersFeature将被处理,这是您处理生命周期范围的地方。

至于可工作的示例,请查看Autofac.AspNetCore.Multitenant存储库。我们为多租户感知的请求生命周期必须这样做。它包含比您所需的更多内容,但您应该能够理解。

  • 当您注册多租户服务时,它注册了一个 IStartupFilter,会在其他中间件注册之前第一次运行。启动过滤器是注册中间件的地方。
  • 中间件
    • 获取现有的 IServiceProvidersFeature
    • 将其替换为使用自定义 IServiceProviderFactory 的新 IServiceProvidersFeature
    • 注册要在请求结束时处理的自定义 IServiceProvidersFeature
    • 在请求管道执行后,将原始的 IServiceProvidersFeature 恢复,以防下游需要在请求结束时使用它。
  • 自定义服务提供者工厂负责创建生命周期范围。

现在我已经提供了这些信息,让我给您一些警告,因为这可能不会像您想的那样

英文:

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.

huangapple
  • 本文由 发表于 2023年6月26日 22:30:15
  • 转载请务必保留本文链接:https://go.coder-hub.com/76557658.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定