ASP.NET Core Web API - проблемы с Autofac и EF Core при использовании InstancePerRequest () - PullRequest
0 голосов
/ 02 июня 2018

Я пытаюсь настроить мой ASP.NET Core Web API проект с Autofac , EF Core и AutoMapper .

В настоящее время я могу заставить мой проект работать со следующими двумя настройками:

  • Зарегистрировать все как сингелтон => SingleInstance() (проблемы с параллелизмом в контексте EF какне является потокобезопасным)
  • Зарегистрируйте все, чтобы он каждый раз получал новый экземпляр Resolve() вызывается => InstancePerDependency() (дополнительные издержки)

К сожалению, я не могу заставить его работать с областью действия InstancePerRequest().

По некоторым причинам я получаю следующее сообщение об ошибке:

enter image description here

Звучит так, как будто я вставляю эти InstancePerRequest() зависимости где-то в singelton сервисе ...

Как только я изменяю все на 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
}

Ответы [ 2 ]

0 голосов
/ 06 июня 2018

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

Итак, что происходит, когда WebAPI получает запрос?Хук Autofac в конвейере WebAPI создает новую область видимости, помеченную константой Autofac.Core.Lifetime.MatchingScopeLifetimeTags.RequestLifetimeScopeTag (это строка «AutofacWebRequest»).Эта область становится «корнем» для этого конкретного запроса.Контроллер и все его компоненты также разрешаются из этой области, и все попытки разрешить зависимости, зарегистрированные с InstancePerRequest(), то есть с InstancePerMatchingLifetimeScope("AutofacWebRequest"), доходят до этой области.

Теперь давайте вернемся к.NET Core.Microsoft внедрила собственное управление DI, и теперь оно также встроено в WebAPI.Он работает почти так же, но с одним исключением: в DI Microsoft нет концепции «помеченных» областей, поэтому у него просто нет средств для обеспечения функциональности, аналогичной InstancePerRequest().Вместо этого внутренние компоненты WebAPI просто создают новую обычную область видимости, то есть делают вызов BeginLifetimeScope() в терминах autofac.Это позволяет контроллерам разрешать свои собственные экземпляры DbContext s и другие вещи, уникальные для запроса.Таким образом, как уже указывал Трэвис, самый простой и простой способ обработки таких зависимостей в .NET Core - это использование InstancePerLifetimeScope() регистраций (или «областей действия» в терминах DI-контейнера Microsoft), поскольку именно этим и занимается WebAPI.внутренне все равно.Это первый вариант, который вы должны рассмотреть в .NET Core. И именно здесь поведение отличается от областей действия autofac по запросу.

Однако, по моему мнению, концепция InstancePerRequest() регистраций все еще действует и имеет право на существование, даже если онадолжно быть очень редко нужно.К счастью, это не очень сложно реализовать самостоятельно.Вам нужно несколько важных вещей:

  1. Autofac;
  2. Регистрация всех контроллеров как сервисов с использованием расширения AddControllersAsServices();
  3. Промежуточное программное обеспечение, которое получает запрос, создает теговое время жизниscope и помещает эту область в качестве контейнера DI в запросе, чтобы все нижестоящие компоненты - промежуточное ПО, контроллеры и т. д. - использовали этот контейнер для разрешения своих зависимостей; расширение
  4. для регистрации этого промежуточного ПО;
  5. нестандартное InstancePerRequest() расширение для регистрации зависимостей с помощью собственного тега времени жизни.

Итак, в целом возможная реализация может выглядеть следующим образом.

Testсервис для игры:

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);
    }
}
0 голосов
/ 02 июня 2018

Прекратите использовать InstancePerRequest и переключитесь на InstancePerLifetimeScope. Из документации :

Использовать InstancePerLifetimeScope вместо InstancePerRequest. В предыдущей интеграции ASP.NET вы могли зарегистрировать зависимость как InstancePerRequestчто обеспечит создание только одного экземпляра зависимости для каждого HTTP-запроса.Это сработало, потому что Autofac отвечал за настройку времени жизни каждого запроса.С введением Microsoft.Extensions.DependencyInjection создание индивидуальных запросов и других дочерних областей действия теперь стало частью соответствующего контейнера, предоставляемого платформой, поэтому все дочерние области действия обрабатываются одинаково - больше нет особой «области действия уровня запроса».Вместо регистрации ваших зависимостей InstancePerRequest используйте InstancePerLifetimeScope, и вы должны получить то же поведение.Обратите внимание: если вы создаете свои собственные области действия во время веб-запросов, вы получите новый экземпляр в этих дочерних областях.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...