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 Web App.

Это руководство относится к Server проекту размещенного Blazor WebAssembly решения или Blazor Server приложения.

Безопасный поток проверки подлинности, необходимый для рабочих приложений

В этой статье используется локальная база данных, которая не требует проверки подлинности пользователя. Рабочие приложения должны использовать самый безопасный поток проверки подлинности. Дополнительные сведения о проверке подлинности для развернутых тестовых и рабочих Blazor приложений см. в статьях о Blazorбезопасности и Identity узле.

Для служб Microsoft Azure рекомендуется использовать управляемые удостоверения. Управляемые удостоверения безопасно проходят проверку подлинности в службах Azure без хранения учетных данных в коде приложения. Дополнительные сведения см. на следующих ресурсах:

Пример приложения

Пример приложения был создан в качестве ссылки на серверные Blazor приложения, которые используются EF Core. Пример приложения включает сетку с операциями сортировки и фильтрации, удаления, добавления и обновления.

В примере показано использование EF Core для обработки оптимистического параллелизма. Однако маркеры параллелизма, созданные собственной базой данных, не поддерживаются для баз данных SQLite, которые являются поставщиком базы данных для примера приложения. Чтобы продемонстрировать параллелизм с примером приложения, выполните другой поставщик базы данных, поддерживающий маркеры параллелизма, созданные базой данных (например, поставщик SQL Server).

Просмотр или скачивание примера кода (как скачать): выберите папку, соответствующую используемой версии .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.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, см. пример приложения.

Руководство по созданию Blazor приложения базы данных фильма

Руководство по созданию приложения, которое используется EF Core для работы с базой данных, см. в разделе "Создание Blazor приложения базы данных фильма" (обзор). В этом руководстве показано, как создать Blazor Web App фильмы и управлять ими в базе данных фильмов.

Доступ к базе данных

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

Предупреждение

Не сохраняйте секреты приложений, строка подключения, учетные данные, пароли, персональные идентификационные номера (ПИН-коды), частный код C#/.NET или закрытые ключи и токены в клиентском коде, который всегда небезопасн. В средах тестирования и промежуточной и рабочей среды код на стороне Blazor сервера и веб-API должны использовать безопасные потоки проверки подлинности, которые не поддерживают учетные данные в файлах кода проекта или конфигурации. Вне локального тестирования разработки рекомендуется избегать использования переменных среды для хранения конфиденциальных данных, так как переменные среды не являются наиболее безопасным подходом. Для локального тестирования разработки средство Secret Manager рекомендуется для защиты конфиденциальных данных. Дополнительные сведения см. в разделе "Безопасное обслуживание конфиденциальных данных и учетных данных".

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

home На странице примера приложения 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;

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

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