Мне нужна помощь, чтобы найти хорошее решение для внедрения DBContext в Repository Services

У меня есть новый проект Blazor, который использует общий модуль репозитория с другим проектом API. Для сервисов конструкторов репозитория требуется экземпляр DBContext и экземпляр AutoMapper. Я увидел, что лучшие практики говорят, что я должен использовать IDbContextFactory<MyDBContext>. Проблема в том, что я не могу использовать его во время внедрения в свои сервисы, поэтому я придумал этот подход, который работает на данный момент, но я уверен, что есть некоторые недостатки, о которых я еще не знаю. Поэтому любая помощь или совет будут оценены.

        var connectionString = configuration.GetConnectionString("PAADB") ?? throw new InvalidOperationException("Connection string 'PAADB' not found.");
        services.AddDbContextFactory<MyDBContext>(options => options.UseSqlServer(connectionString), ServiceLifetime.Scoped);
        services.AddAutoMapper(Assembly.GetExecutingAssembly());

        services.AddScoped(ConfigurePersistenceService<IUserPersistenceService, UserPersistenceService>);
        services.AddScoped(ConfigurePersistenceService<ICrmFirmsPersistenceService, CrmFirmsPersistenceService>);
        services.AddScoped(ConfigurePersistenceService<ICrmAdvisorsPersistenceService, CrmAdvisorsPersistenceService>);


        .......


        static I ConfigurePersistenceService<I, T>(IServiceProvider provider) where T : class
        {
            var db = provider.GetRequiredService<IDbContextFactory<MyDBContext>>();
            var autoMapper = provider.GetRequiredService<IMapper>();
            return (I)Activator.CreateInstance(typeof(T), db.CreateDbContext(), autoMapper);
        }//ConfigurePersistenceService - special way to pass db instance

Пример службы репозитория

public class CrmFirmsPersistenceService : PersistenceService<CrmFirm>, ICrmFirmsPersistenceService
{
    private readonly IMapper _mapper;

    public CrmFirmsPersistenceService(MyDBContext context, IMapper mapper) : base(context)
    {
        _mapper = mapper;
    }

    public async Task<(IEnumerable<CrmFirmDTO>, int)> GetFirmsWithPagination(int pageNumber, int pageSize)
    {
        var baseQuery = _context.CrmFirms;

        var lstItems = await baseQuery.OrderByDescending(x => x.Id).Skip((pageNumber) * pageSize).Take(pageSize).ToListAsync();
        var total = baseQuery.Count();            
        return (_mapper.Map<IEnumerable<CrmFirmDTO>>(lstItems), total);
    }
}

}

Исключения, которые я получаю время от времени:

Вторая операция была запущена в этом экземпляре контекста до завершения предыдущей операции. Обычно это вызвано тем, что разные потоки одновременно используют один и тот же экземпляр DbContext. Дополнительные сведения о том, как избежать проблем с многопоточностью в DbContext, см. на странице https://go.microsoft.com/fwlink/?linkid=2097913.

DbContext уже является репозиторием с несколькими сущностями и единицей работы. Какие услуги репозитория вы имеете в виду? Где их определения и регистрации? Что бы это ни было, им не нужны ConfigurePersistenceService или Activator.CreateInstance

Panagiotis Kanavos 08.02.2023 11:26

@Panagiotis Kanavos Эти службы репозитория являются еще одним уровнем абстракции над DbContext. Так что это не проблема, потому что они отлично работают с API. Blazor - это проблема, потому что выдает исключения, если я использую его как API с services.AddDbContext вместо services.AddDbContextFactory

D A 08.02.2023 11:30

Я знаю, что они должны быть, и они обычно не являются. Эти однообъектные классы, не относящиеся к репозиторию, почти всегда являются реализацией CRUD более низкого уровня, называемой объектом доступа к данным. Многие плохие статьи пытаются использовать DbContext, как если бы это было соединение ADO.NET вместо репозитория. В результате происходит утечка кода доступа к данным по всему бизнес-коду и нарушение транзакций.

Panagiotis Kanavos 08.02.2023 11:33

Что касается I saw that the best practices are saying that I should use IDbContextFactory<MyDBContext>., это очень плохая практика, если вы не понимаете, о чем они говорят. Не существует «лучших практик», которым нужно следовать слепо. В этом случае во всех веб-приложениях, кроме Blazor Server, для каждого запроса создается новый экземпляр DbContext. Это также объем услуг, и это то, что дает вам транзакционное поведение бесплатно. Однако в Blazor Server областью является весь пользовательский сеанс, что означает, что вам нужно создавать свои собственные области для служб с заданной областью, а не только DbContext.

Panagiotis Kanavos 08.02.2023 11:36

@Panagiotis Kanavos Да, вы правы .... но у меня все еще есть проблема с тем, как передать MyDBContext во время инъекции в те .... зная, что изменение служб репозитория невозможно.

D A 08.02.2023 11:36

У вас действительно есть приложение Blazor Server? Если нет, вам не нужен DbContextFactory. Независимо от того, что у вас есть, невозможно ответить на этот вопрос, не зная, в чем проблема, чего ожидают классы репозитория и как они создаются.

Panagiotis Kanavos 08.02.2023 11:37

Да, это серверное приложение Blazor.

D A 08.02.2023 11:37
I still have the problem on how to feed MyDBContext at the injection time into those. какие those? Опубликуйте их код.
Panagiotis Kanavos 08.02.2023 11:37

Давайте продолжим обсуждение в чате.

D A 08.02.2023 11:38

Если вам нужны репозитории с ограниченной областью действия, вам необходимо создать области видимости явно. Когда вы это сделаете, репозитории, экземпляры DbContext и AutoMapper будут созданы по мере необходимости самой DI. Никогда не нужно Activator.CreateInstance

Panagiotis Kanavos 08.02.2023 11:38

Проблема в неполном вопросе, а не в чате. Добавьте код и исключения в сам вопрос

Panagiotis Kanavos 08.02.2023 11:39

@Panagiotis Kanavos - Готово. Добавил то, что ты просил.

D A 08.02.2023 11:46

Я добавил ответ, объясняющий проблему с использованием DbContextFactory в этом случае и некоторые альтернативы

Panagiotis Kanavos 08.02.2023 12:28
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
13
58
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я видел, что лучшие практики говорят, что я должен использовать IDbContextFactory

Это не «лучшая практика», на самом деле в данном случае это проблема.

Фактическая проблема, которую необходимо решить, заключается в том, что областью внедрения зависимостей в приложении Blazor Server является весь сеанс пользователя. Доступно множество вариантов если нам нужна семантика области видимости, и специализированная фабрика — только один из них. На самом деле DbContextFactory предназначен только для создания управляемых пользователем экземпляров DbContext. Он не предназначен для управления временем жизни оболочек или зависимых объектов.

DbContextFactory — управляемая пользователем область

DbContextFactory предназначен для использования, когда мы хотим явно управлять областью действия только DbContext, например, на функциональном уровне. Он только создает DbContext, но не удаляет его. Это видно из примеров документации:

@inject IDbContextFactory<ContactContext> DbFactory

private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();
    ...
}

Это делает его плохо подходящим для CrmFirmsPersistenceService. По крайней мере, вызывающие службы должны явно удалять службу, чтобы она, в свою очередь, могла удалять экземпляр DbContext.

Объем компонента

Один из способов решить эту проблему — ограничить область действия службы временем жизни компонента. Это можно сделать, наследуя от OwningComponentBase.

В этом примере документации AppDbContext относится к сроку службы компонента. Он доступен через свойство Service:

@page "/users"
@attribute [Authorize]
@inherits OwningComponentBase<AppDbContext>

<h1>Users (@Service.Users.Count())</h1>

<ul>
    @foreach (var user in Service.Users)
    {
        <li>@user.UserName</li>
    }
</ul>

В случае вопроса это будет работать, не требуя фабрики контекста или активатора, просто зарегистрировав DbContexts и CrmFirmsPersistenceService и др. в качестве служб с ограниченной областью действия.

Конфигурация DI может быть:

services.AddDbContext<MyDBContext>(options => options.UseSqlServer(connectionString));
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddScoped<ICrmFirmsPersistenceService, CrmFirmsPersistenceService>();

Сам компонент должен был бы наследоваться от OwningComponentBase<T>

@page "/firms"
@inherits OwningComponentBase<ICrmFirmsPersistenceService>

<h1>Firms (@page.count)</h1>

<ul>
    @foreach (var firm in page.firms)
    {
        ...
    }
</ul>

@code {

    (IEnumerable<CrmFirmDTO> firms, int count) page = null!;
    protected override async Task OnInitializedAsync()
    {
        page = await Service.GetFirmsWithPagination();
    }

Явная область

Когда мы хотим определить более точную область, нам нужно создать ее явно с помощью IServiceProvider.CreateScope и создать службы, используя эту область. Вот как singleton Hosted Services может потреблять услуги с заданной областью.

Для этого нам нужно добавить IServiceProvider в качестве зависимости к компоненту:

@page "/firms"
@inject IServiceProvider Services

Затем, когда нам нужно создать экземпляр с областью действия ICrmFirmsPersistenceService, нам нужно сделать это через явную область:

using var scope=Services.CreateScope();
var firmsService=scope.GetRequiredService<ICrmFirmsPersistenceService>();
...

Когда эта область удаляется, любая обслуживаемая область, созданная с ее помощью, также будет удалена, включая CrmFirmsPersistenceService и его DbContext.

Спасибо, теперь все становится более понятным.

D A 08.02.2023 12:32

Другие вопросы по теме