ASP.NET Core с Entity Framework Core Blazor (EF 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. Однако существуют сценарии, в которых может потребоваться разрешение дополнительных зависимостей.

Рекомендуемым подходом для создания нового экземпляра 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);
    }
}

В предыдущей фабрике:

В следующем примере настраивается 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

Дополнительные ресурсы