【问题标题】:ASP.NET Core Web API - Issues with Autofac and EF Core when using InstancePerRequest()ASP.NET Core Web API - 使用 InstancePerRequest() 时 Autofac 和 EF Core 的问题
【发布时间】:2018-11-12 06:07:58
【问题描述】:

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

目前我可以让我的项目使用以下两种设置:

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

很遗憾,我无法使其与 InstancePerRequest() 范围一起使用。

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

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

一旦我将所有内容更改为SingleInstance()InstancePerDependency(),一切正常。与InstancePerRequest()相比,至少不计算并发问题和额外开销。

Startup.cs

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

服务 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();
    }
}

存储库 A

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

        }
    }
}

RepositoryBase

 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
}

【问题讨论】:

    标签: .net entity-framework asp.net-web-api dependency-injection autofac


    【解决方案1】:

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

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

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

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

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

    现在,让我们回到 .NET Core。 Microsoft 实现了他们自己的 DI 管理,现在它也嵌入到了 WebAPI 中。它的工作方式几乎相同,但有一个例外:Microsoft 的 DI 没有“标记”作用域的概念,因此它无法提供类似于 InstancePerRequest() 的功能。相反,WebAPI 内部只是创建了新的普通范围,即用 autofac 的术语进行 BeginLifetimeScope() 调用。这允许控制器解析他们自己的 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();
        }
    }
    

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

    Startup.cs

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

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

    RequestScopeMiddleware.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;
                }
            } 
        }
    }
    

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

    【讨论】:

    • InstancePerLifetimeScope() 正是我想要的——谢谢!
    • 请注意,当您的中间件运行时,ASP.NET Core 内部已经创建了它们的请求服务范围,这意味着您基本上在这里为每个请求创建了两个子范围。这并不是完全免费的性能,当涉及到每个生命周期范围内注册实例的实际事物时,可能会产生潜在的副作用。这就是为什么 Autofac 采用 ASP.NET Core 提出的标准,有好有坏,并且在 ASP.NET Core 中没有为请求标记范围。
    • @TravisIllig - 是的,我知道。在我们的情况下,这是完全可以接受的。
    • 您可能知道,提问者和未来的读者可能不知道。如果您向 ASP.NET Core 或 Autofac 提出问题并且您正在这样做,那么您就只能靠自己了。这只是完全定制时需要注意的事情。
    • 同意。感谢您的来信。
    【解决方案2】:

    停止使用InstancePerRequest 并切换到InstancePerLifetimeScopeFrom the documentation:

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

    【讨论】:

    • 确实如此。但是,在 .net 核心中使用 autofac 实现完全相同的每个请求范围并不难。要做的只是小型中间件、一种注册它的扩展方法和.InstancePerRequest() 扩展本身。像魅力一样工作。
    • @Alexander 这可能是可能的,但这不是提问者正在做的事情,这不是默认情况下的工作方式,所以......也许有趣的事实,但在这种情况下没有那么有用。 (除非您可能想添加一个答案来帮助提问者?)
    • 回答是。 :) 这实际上是一个有趣的问题,因为不久前我自己在使用InstancePerRequest() 的旧项目代码迁移期间正在挖掘同样的东西。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-05-15
    • 2019-04-30
    • 2020-11-27
    • 2018-12-17
    • 1970-01-01
    • 2021-06-04
    • 2018-07-20
    相关资源
    最近更新 更多