ASP.NET Core Web API - 使用 InstancePerRequest() 时 Autofac 和 EF Core 的问题

Han*_*lse 1 .net entity-framework dependency-injection autofac asp.net-web-api

我正在尝试使用AutofacEF CoreAutoMapper设置我的ASP.NET Core Web API项目。

目前,我可以通过以下两种设置使我的项目工作:

  • 将所有内容注册为单例 => SingleInstance() (EF 上下文的并发问题,因为它不是线程安全的)
  • 注册所有内容,以便每次Resolve()调用时都会获得一个新实例=> InstancePerDependency() (额外开销)

不幸的是,我无法使其与InstancePerRequest()范围一起使用。

出于某种原因,我收到以下错误消息:

在此处输入图片说明

这听起来有点像我会InstancePerRequest()singelton服务的某个地方注入这些依赖项......

一旦我将所有内容更改为SingleInstance()InstancePerDependency()一切正常。与InstancePerRequest().

启动文件

    /// <summary>
    /// Is called by the ASP.NET Core application for all environments.
    /// </summary>
    /// <param name="builder">Builder container.</param>
    private static void ConfigureContainer(ContainerBuilder builder, ApplicationName applicationName)
    {
        // For all environments and for only user service specific registrations
        if (applicationName == ApplicationName.XY)
        {

            // Auto mapper
            builder.RegisterAutoMapper();

            // Register db context
            builder.RegisterType<MyDbContext>().InstancePerRequest();

            // Repositories
            builder.RegisterType<RepositoryA>().As<IRepositoryA>().InstancePerRequest();

            // Services
            builder.RegisterType<ServiceA>().As<IServiceA>().InstancePerRequest();
        }
    }

     /// <summary>
    /// Register AutoMapper to the DI container.
    /// </summary>
    public static void RegisterAutoMapper(this ContainerBuilder builder)
    {
        builder.Register(c => new MapperConfiguration(cfg =>
        {
            // Mapping
            cfg.CreateMap<EntityA, EntityB>();


        })).AsSelf().SingleInstance();

        // Register mapper
        builder.Register(c => c.Resolve<MapperConfiguration>().CreateMapper(c.Resolve)).As<IMapper>().InstancePerLifetimeScope();
    }
Run Code Online (Sandbox Code Playgroud)

服务A

public class ServiceA : IServiceA
{
    private readonly IMapper _mapper;
    private readonly IRepositoryA _repository;

    public ServiceA (IRepositoryA _repository, IMapper mapper)
    {
        _repository = _repository;
        _mapper = mapper;
    }

    /// <summary>
    /// Return all categories.
    /// </summary>
    public async Task<List<XY>> GetAllAsync()
    {
        return await _repository.XY.ToListAsync();
    }
}
Run Code Online (Sandbox Code Playgroud)

存储库 A

 public class RepositoryA : RepositoryBase<XY, int, RepositoryA>, IRepositoryA
    {
        public RepositoryA(MyDbContext myDbContext, ILogger<RepositoryA> logger) : base(myDbContext, logger)
        {

        }
    }
}
Run Code Online (Sandbox Code Playgroud)

存储库库

 public abstract class RepositoryBase<TEntityType, TIdType, TLoggingType>
    where TEntityType : class
    where TLoggingType : class
{
    private readonly MyDbContext _myDbContext;

    protected readonly ILogger<TLoggingType> _logger;
    protected DbSet<TEntityType> _dbSet;

    private bool _disposed = false;

    public RepositoryBase(MyDbContext myDbContext, ILogger<TLoggingType> logger)
    {
        _myDbContext = myDbContext;
        _dbSet = _myDbContext.Set<TEntityType>();
        _logger = logger;
    }


    // All the boilerplate code has been left out for the sake of brevity
}
Run Code Online (Sandbox Code Playgroud)

Ale*_*nov 5

TL;DR - 这是可能的,但InstancePerLifetimeScope()仍然应该是第一个考虑的选项。此外,autofac 的文档并不完全正确 - 行为不会完全相同。

首先,请阅读InstancePerRequest() 如何在完整的 .NET Framework WebAPI 应用程序中工作。正如链接下所述,它实际上InstancePerMatchingLifetimeScope()在幕后使用来完成工作。

下一个问题是 - 如何InstancePerMatchingLifetimeScope()运作?:) 反过来,这里描述。下面的例子展示了它背后的想法。

public interface IMyService { }

public class MyService: IMyService
{ 
    public MyService()
    { 
    }
}

internal class Program
{
    public static void Main(string[] args)
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<MyService>().As<IMyService>().InstancePerMatchingLifetimeScope("ScopeName");
        var container = builder.Build();

        using (var scope = container.BeginLifetimeScope("ScopeName"))
        {
            var instance1 = scope.BeginLifetimeScope().Resolve<IMyService>();
            var instance2 = scope.BeginLifetimeScope().Resolve<IMyService>();
            // This outputs "True" since both instances are actually resolved from the same lifetime scope
            // tagged with string "ScopeName".
            // If none of the parent scopes are tagged with the "ScopeName" 
            // then such an attempt to resolve IMyService will throw an exception.
            Console.WriteLine($"References are the same: {object.ReferenceEquals(instance1, instance2)}");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

那么,当 WebAPI 收到请求时会发生什么?WebAPI 管道中的 Autofac 挂钩创建了标有Autofac.Core.Lifetime.MatchingScopeLifetimeTags.RequestLifetimeScopeTag常量(它是“AutofacWebRequest”字符串)的新范围。此范围成为此特定请求的“根”。Controller 及其所有内容也从该范围解析,并且所有解析注册的依赖项的尝试InstancePerRequest(),即 with InstancePerMatchingLifetimeScope("AutofacWebRequest"),都在该范围内。

现在,让我们回到 .NET Core。微软实现了他们自己的 DI 管理,现在它也嵌入在 WebAPI 中。它的工作方式几乎相同,但有一个例外:Microsoft 的 DI 没有“标记”范围的概念,因此它没有任何方法提供类似于InstancePerRequest(). 相反,WebAPI 内部只是创建新的普通作用域,即BeginLifetimeScope()用 autofac 的术语进行调用。这允许控制器解析他们自己的DbContexts实例和其他对于请求是唯一的东西。因此,正如 Travis 已经指出的那样,在 .NET Core 中处理此类依赖项的最简单、最直接的方法是使用InstancePerLifetimeScope()注册(或 Microsoft 的 DI 容器中的“范围”),因为无论如何这就是 WebAPI 在内部所做的。这是您在 .NET Core 中应该考虑的第一个选项。这就是行为与 autofac 的每个请求范围不同的地方。

然而,在我看来,InstancePerRequest()注册的概念仍然有效并且有权存在,即使它应该很少被需要。幸运的是,自己实现它并不难。你需要一些主要的东西:

  1. Autofac;
  2. 使用AddControllersAsServices()扩展将所有控制器注册为服务;
  3. 接收请求的中间件,创建标记的生命周期作用域,并将该作用域作为 DI 容器放入请求中,以便所有下游事物——中间件、控制器等——使用该容器来解决它们的依赖关系;
  4. 注册该中间件的扩展;
  5. InstancePerRequest()使用您自己的生命周期范围标签注册依赖项的自定义扩展。

因此,所有可能的实现可能如下所示。

测试服务:

public interface ISomeService
{
    Guid Id { get; }
}

public class SomeService : ISomeService
{
    public Guid Id { get; }

    public SomeService()
    {
        Id = Guid.NewGuid();
    }
}
Run Code Online (Sandbox Code Playgroud)

测试控制器.cs

[Route("api/[controller]")]
public class TestController : Controller
{
    private readonly ILifetimeScope _scope;

    public TestController(ILifetimeScope scope)
    {
        _scope = scope;
    }

    [HttpGet]
    public IActionResult Get()
    {
        var service1 = _scope.Resolve<ISomeService>();
        ISomeService service2;
        using (var newScope = _scope.BeginLifetimeScope())
        {
            service2 = newScope.Resolve<ISomeService>();
        }
        return Ok(service1.Id == service2.Id);
    }
}
Run Code Online (Sandbox Code Playgroud)

启动文件

public class Startup
{
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        services
            .AddMvc()
            .AddControllersAsServices();

        var builder = new ContainerBuilder();
        // just to play with another option and see how it works
//        builder.RegisterType<SomeService>().As<ISomeService>().InstancePerLifetimeScope();
        builder.RegisterType<SomeService>().As<ISomeService>().InstancePerRequest();
        builder.Populate(services);

        return new AutofacServiceProvider(builder.Build());
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // !!!!!
        app.UsePerRequestScopes();
        app.UseMvc();
    }
}
Run Code Online (Sandbox Code Playgroud)

现在到了重要的部分。我没有从我的工作项目中提取它,所以它可能不完全正确但它有效并且它很好地展示了这个想法。

请求范围中间件.cs

public class RequestScopeMiddleware
{
    private readonly RequestDelegate _next;
    private readonly string _scopeName;

    public RequestScopeMiddleware(RequestDelegate next, string scopeName)
    {
        _next = next;
        _scopeName = scopeName;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var originalServiceProvider = context.RequestServices;
        var currentLifetimeScope = originalServiceProvider.GetRequiredService<ILifetimeScope>();
        using (var requestScope = currentLifetimeScope.BeginLifetimeScope(_scopeName))
        {
            context.RequestServices = new AutofacServiceProvider(requestScope);
            try
            {
                await _next(context);
            }
            finally
            {
                context.RequestServices = originalServiceProvider;
            }
        } 
    }
}
Run Code Online (Sandbox Code Playgroud)

RequestScopeExtensions.cs

public static class RequestScopeExtensions
{
    private const string ScopeName = "RequestScope";

    public static void InstancePerRequest<T1, T2, T3>(this IRegistrationBuilder<T1, T2, T3> builder)
    {
        builder.InstancePerMatchingLifetimeScope(ScopeName);
    }

    public static void UsePerRequestScopes(this IApplicationBuilder builder)
    {
        builder.UseMiddleware<RequestScopeMiddleware>(ScopeName);
    }
}
Run Code Online (Sandbox Code Playgroud)