У меня есть новый проект 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.
@Panagiotis Kanavos Эти службы репозитория являются еще одним уровнем абстракции над DbContext. Так что это не проблема, потому что они отлично работают с API. Blazor - это проблема, потому что выдает исключения, если я использую его как API с services.AddDbContext вместо services.AddDbContextFactory
Я знаю, что они должны быть, и они обычно не являются. Эти однообъектные классы, не относящиеся к репозиторию, почти всегда являются реализацией CRUD более низкого уровня, называемой объектом доступа к данным. Многие плохие статьи пытаются использовать DbContext, как если бы это было соединение ADO.NET вместо репозитория. В результате происходит утечка кода доступа к данным по всему бизнес-коду и нарушение транзакций.
Что касается I saw that the best practices are saying that I should use IDbContextFactory<MyDBContext>.
, это очень плохая практика, если вы не понимаете, о чем они говорят. Не существует «лучших практик», которым нужно следовать слепо. В этом случае во всех веб-приложениях, кроме Blazor Server, для каждого запроса создается новый экземпляр DbContext. Это также объем услуг, и это то, что дает вам транзакционное поведение бесплатно. Однако в Blazor Server областью является весь пользовательский сеанс, что означает, что вам нужно создавать свои собственные области для служб с заданной областью, а не только DbContext.
@Panagiotis Kanavos Да, вы правы .... но у меня все еще есть проблема с тем, как передать MyDBContext во время инъекции в те .... зная, что изменение служб репозитория невозможно.
У вас действительно есть приложение Blazor Server? Если нет, вам не нужен DbContextFactory. Независимо от того, что у вас есть, невозможно ответить на этот вопрос, не зная, в чем проблема, чего ожидают классы репозитория и как они создаются.
Да, это серверное приложение Blazor.
I still have the problem on how to feed MyDBContext at the injection time into those.
какие those
? Опубликуйте их код.
Давайте продолжим обсуждение в чате.
Если вам нужны репозитории с ограниченной областью действия, вам необходимо создать области видимости явно. Когда вы это сделаете, репозитории, экземпляры DbContext и AutoMapper будут созданы по мере необходимости самой DI. Никогда не нужно Activator.CreateInstance
Проблема в неполном вопросе, а не в чате. Добавьте код и исключения в сам вопрос
@Panagiotis Kanavos - Готово. Добавил то, что ты просил.
Я добавил ответ, объясняющий проблему с использованием DbContextFactory в этом случае и некоторые альтернативы
Я видел, что лучшие практики говорят, что я должен использовать 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.
Спасибо, теперь все становится более понятным.
DbContext уже является репозиторием с несколькими сущностями и единицей работы. Какие услуги репозитория вы имеете в виду? Где их определения и регистрации? Что бы это ни было, им не нужны
ConfigurePersistenceService
илиActivator.CreateInstance