ASP.NET Core с Entity Framework Core Blazor (EF Core)
Примечание.
Это не последняя версия этой статьи. В текущем выпуске см . версию .NET 8 этой статьи.
Предупреждение
Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в статье о политике поддержки .NET и .NET Core. В текущем выпуске см . версию .NET 8 этой статьи.
Внимание
Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
В текущем выпуске см . версию .NET 8 этой статьи.
В этой статье объясняется, как использовать Entity Framework Core (EF Core) в серверных приложениях Blazor .
Серверная часть Blazor — это платформа приложений с отслеживанием состояния. Приложение поддерживает постоянное подключение к серверу, а состояние пользователя хранится в памяти сервера в канале. Примером состояния пользователя являются данные, хранящиеся во внедрениях зависимостей (DI) экземпляров службы, областью действия которых является канал. Для уникальной модели приложения, которую предоставляет Blazor, требуется специальный подход к использованию Entity Framework Core.
Примечание.
В этой статье рассматриваются EF Core серверные Blazor приложения. Приложения Blazor WebAssembly выполняются в песочнице WebAssembly, которая запрещает большинство прямых подключений к базе данных. Выполнение EF Core выходит Blazor WebAssembly за рамки этой статьи.
Это руководство относится к компонентам, которые применяют интерактивную отрисовку на стороне сервера (интерактивная служба SSR) в Blazor веб-приложении.
Это руководство относится к Server
проекту размещенного Blazor WebAssembly решения или Blazor Server приложения.
Пример приложения
Пример приложения был создан в качестве ссылки на серверные Blazor приложения, которые используются EF Core. Пример приложения включает сетку с операциями сортировки и фильтрации, удаления, добавления и обновления. В примере показано использование EF Core для обработки оптимистического параллелизма.
Просмотр или скачивание примера кода (как скачать): выберите папку, соответствующую используемой версии .NET. В папке версии перейдите к примеру с именем BlazorWebAppEFCore
.
Просмотр или скачивание примера кода (как скачать): выберите папку, соответствующую используемой версии .NET. В папке версии перейдите к примеру с именем BlazorServerEFCoreSample
.
В примере используется локальная база данных SQLite, чтобы ее можно было использовать на любой платформе. В этом примере также настраивается ведение журнала базы данных для отображения создаваемых запросов SQL. Это настраивается в appsettings.Development.json
.
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
{
"DetailedErrors": true,
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Information"
}
}
}
Компоненты сетки для добавления и просмотра используют шаблон "контекст на операцию", когда контекст создается для каждой операции. Компонент редактирования использует шаблон "контекст на компонент", когда контекст создается для каждого компонента.
Примечание.
Для некоторых примеров кода в этом разделе требуются пространства имен и службы, которые не показаны. Для просмотра полностью работающего кода, включая обязательные директивы @using
и @inject
для примеров Razor, см. пример приложения.
Доступ к базе данных
EF Core используется в DbContext качестве средства для настройки доступа к базе данных и действия в качестве единицы работы. EF CoreAddDbContext предоставляет расширение для приложений ASP.NET Core, которые регистрируют контекст в качестве службы с областью действия по умолчанию. В серверных Blazor приложениях регистрация служб с областью действия может быть проблематичной, так как экземпляр предоставляется совместно между компонентами в канале пользователя. DbContext не является потокобезопасным и не предназначен для одновременного использования. Существующие времена существования не подходят по следующим причинам.
- Отдельная. Состояние используется всеми пользователями приложения, что приводит к неприемлемому одновременному использованию.
- С заданной областью (по умолчанию). Приводит к той же проблеме для компонентов одного и того же пользователя.
- Временная. В каждом запросе создается новый экземпляр, но, поскольку компоненты могут быть длительного времени существования, это приводит к более долгоживущему контексту, чем предполагалось.
Следующие рекомендации предназначены для обеспечения согласованного подхода к использованию EF Core в серверных приложениях Blazor .
По умолчанию рассмотрите возможность использования одного контекста для каждой операции. Контекст предназначен для быстрого создания экземпляров с низкими накладными расходами.
using var context = new MyContext(); return await context.MyEntities.ToListAsync();
Используйте флаг для предотвращения нескольких одновременных операций.
if (Loading) { return; } try { Loading = true; ... } finally { Loading = false; }
Размещайте операции после строки
Loading = true;
в блокеtry
.Для логики загрузки не требуется блокировка записей базы данных, поскольку потокобезопасность не вызывает опасений. С помощью логики загрузки можно отключить элементы управления пользовательским интерфейсом, чтобы пользователи не могли случайно нажать на кнопки или обновить поля во время получения данных.
Если существует вероятность, что к одному блоку кода обращается несколько потоков, внедрите производство и создайте новый экземпляр для каждой операции. В противном случае обычно достаточно внедрения и использования контекста.
Для более длительных операций, использующих управление EF Coreотслеживанием изменений или параллелизмом, примените контекст к времени существования компонента.
Новые экземпляры DbContext
Самый быстрый способ создать новый экземпляр DbContext — использовать new
. Однако существуют сценарии, в которых может потребоваться разрешение дополнительных зависимостей.
- Использование
DbContextOptions
для настройки контекста. - Использование строки подключения для каждого экземпляра DbContext, как например при использовании модели Identity ASP.NET Core. Дополнительные сведения см. в разделе "Многотенантность" (EF Core документация).
Рекомендуемым подходом для создания нового экземпляра DbContext с зависимостями является использование фабрики. EF Core 5.0 или более поздней версии предоставляет встроенную фабрику для создания новых контекстов.
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace BlazorServerDbContextExample.Data
{
public class DbContextFactory<TContext>
: IDbContextFactory<TContext> where TContext : DbContext
{
private readonly IServiceProvider provider;
public DbContextFactory(IServiceProvider provider)
{
this.provider = provider ?? throw new ArgumentNullException(
$"{nameof(provider)}: You must configure an instance of " +
"IServiceProvider");
}
public TContext CreateDbContext() =>
ActivatorUtilities.CreateInstance<TContext>(provider);
}
}
В предыдущей фабрике:
- ActivatorUtilities.CreateInstance удовлетворяет все зависимости через поставщика услуг;
IDbContextFactory
доступен в EF Core ASP.NET Core 5.0 или более поздней версии, поэтому интерфейс реализован в примере приложения для ASP.NET Core 3.x.
В следующем примере настраивается SQLite и включается ведение журнала данных. Код использует метод расширения (AddDbContextFactory
) для настройки фабрики баз данных для DI и предоставления параметров по умолчанию:
builder.Services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
builder.Services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
builder.Services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
Фабрика внедряется в компоненты и используется для создания новых экземпляров DbContext
.
На домашней странице примера приложения IDbContextFactory<ContactContext>
вставляется в компонент:
@inject IDbContextFactory<ContactContext> DbFactory
Создается экземпляр DbContext
с помощью фабрики (DbFactory
) для удаления контакта в методе DeleteContactAsync
:
private async Task DeleteContactAsync()
{
using var context = DbFactory.CreateDbContext();
Filters.Loading = true;
if (Wrapper is not null && context.Contacts is not null)
{
var contact = await context.Contacts
.FirstAsync(c => c.Id == Wrapper.DeleteRequestId);
if (contact is not null)
{
context.Contacts?.Remove(contact);
await context.SaveChangesAsync();
}
}
Filters.Loading = false;
await ReloadAsync();
}
private async Task DeleteContactAsync()
{
using var context = DbFactory.CreateDbContext();
Filters.Loading = true;
if (Wrapper is not null && context.Contacts is not null)
{
var contact = await context.Contacts
.FirstAsync(c => c.Id == Wrapper.DeleteRequestId);
if (contact is not null)
{
context.Contacts?.Remove(contact);
await context.SaveChangesAsync();
}
}
Filters.Loading = false;
await ReloadAsync();
}
private async Task DeleteContactAsync()
{
using var context = DbFactory.CreateDbContext();
Filters.Loading = true;
if (Wrapper is not null && context.Contacts is not null)
{
var contact = await context.Contacts
.FirstAsync(c => c.Id == Wrapper.DeleteRequestId);
if (contact is not null)
{
context.Contacts?.Remove(contact);
await context.SaveChangesAsync();
}
}
Filters.Loading = false;
await ReloadAsync();
}
private async Task DeleteContactAsync()
{
using var context = DbFactory.CreateDbContext();
Filters.Loading = true;
var contact = await context.Contacts.FirstAsync(
c => c.Id == Wrapper.DeleteRequestId);
if (contact != null)
{
context.Contacts.Remove(contact);
await context.SaveChangesAsync();
}
Filters.Loading = false;
await ReloadAsync();
}
private async Task DeleteContactAsync()
{
using var context = DbFactory.CreateDbContext();
Filters.Loading = true;
var contact = await context.Contacts.FirstAsync(
c => c.Id == Wrapper.DeleteRequestId);
if (contact != null)
{
context.Contacts.Remove(contact);
await context.SaveChangesAsync();
}
Filters.Loading = false;
await ReloadAsync();
}
Примечание.
Filters
является встроенным IContactFilters
, а Wrapper
является ссылкой на компонент для компонента GridWrapper
. См. Home
компонент (Components/Pages/Home.razor
) в примере приложения.
Примечание.
Filters
является встроенным IContactFilters
, а Wrapper
является ссылкой на компонент для компонента GridWrapper
. См. Index
компонент (Pages/Index.razor
) в примере приложения.
Область действия на время существования компонента
Может потребоваться создать экземпляр DbContext, который будет существовать в течение времени существования компонента. Это позволяет использовать его как единицу работы и пользоваться преимуществами встроенных функций, таких как отслеживание изменений и разрешение параллелизма.
Фабрику можно использовать для создания контекста и наблюдения за временем существования компонента. Сначала реализуйте и внедряйте IDisposable фабрику, как показано в компоненте EditContact
(Components/Pages/EditContact.razor
):
Фабрику можно использовать для создания контекста и наблюдения за временем существования компонента. Сначала реализуйте и внедряйте IDisposable фабрику, как показано в компоненте EditContact
(Pages/EditContact.razor
):
@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory
Пример приложения обеспечивает удаление контекста при удалении компонента.
public void Dispose()
{
Context?.Dispose();
}
public void Dispose()
{
Context?.Dispose();
}
public void Dispose()
{
Context?.Dispose();
}
public void Dispose()
{
Context?.Dispose();
}
public void Dispose()
{
Context?.Dispose();
}
Наконец, OnInitializedAsync
переопределяется для создания нового контекста. В примере приложения OnInitializedAsync
загружает контакт в том же методе.
protected override async Task OnInitializedAsync()
{
Busy = true;
try
{
Context = DbFactory.CreateDbContext();
if (Context is not null && Context.Contacts is not null)
{
var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);
if (contact is not null)
{
Contact = contact;
}
}
}
finally
{
Busy = false;
}
await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
Busy = true;
try
{
Context = DbFactory.CreateDbContext();
if (Context is not null && Context.Contacts is not null)
{
var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);
if (contact is not null)
{
Contact = contact;
}
}
}
finally
{
Busy = false;
}
await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
Busy = true;
try
{
Context = DbFactory.CreateDbContext();
if (Context is not null && Context.Contacts is not null)
{
var contact = await Context.Contacts.SingleOrDefaultAsync(c => c.Id == ContactId);
if (contact is not null)
{
Contact = contact;
}
}
}
finally
{
Busy = false;
}
await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
Busy = true;
try
{
Context = DbFactory.CreateDbContext();
Contact = await Context.Contacts
.SingleOrDefaultAsync(c => c.Id == ContactId);
}
finally
{
Busy = false;
}
await base.OnInitializedAsync();
}
protected override async Task OnInitializedAsync()
{
Busy = true;
try
{
Context = DbFactory.CreateDbContext();
Contact = await Context.Contacts
.SingleOrDefaultAsync(c => c.Id == ContactId);
}
finally
{
Busy = false;
}
await base.OnInitializedAsync();
}
В предыдущем примере:
- Если
Busy
имеет значениеtrue
, могут начинаться асинхронные операции. ЕслиBusy
возвращается в значениеfalse
, асинхронные операции должны быть завершены. - Разместите дополнительную логику обработки ошибок в блоке
catch
.
Включение ведения журнала для конфиденциальных данных
EnableSensitiveDataLogging позволяет включить данные приложения в сообщения об исключениях и журналы платформы. Записанные в журнал данные могут содержать значения, присвоенные свойствам экземпляров сущностей, и значения параметров для команд, отправляемых в базу данных. Данные ведения журнала с EnableSensitiveDataLogging помощью риска безопасности, так как они могут предоставлять пароли и другие персональные данные (PII) при журнале инструкций SQL, выполняемых в базе данных.
Мы рекомендуем включать метод EnableSensitiveDataLogging только на этапах разработки и тестирования.
#if DEBUG
services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
.EnableSensitiveDataLogging());
#else
services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db"));
#endif
Дополнительные ресурсы
ASP.NET Core