Перейти к содержанию

3. EF Core и база данных

Entity Framework Core позволяет работать с таблицами через C#-классы. В практике для простого запуска используется SQLite. В промышленном варианте можно заменить провайдер на PostgreSQL, MS SQL или MySQL.

Главная идея EF Core: разработчик описывает данные как классы C#, а EF Core берёт на себя преобразование этих классов в таблицы и SQL-запросы. Это не отменяет понимания базы данных, но снижает количество ручного SQL-кода.

В практике база нужна не просто “для галочки”. Она хранит историю событий. Если приложение перезапустить, события не должны пропасть. Поэтому данные сохраняются в SQLite-файл, а структура базы создаётся через миграции.

Установка пакетов

Эти пакеты подключают EF Core к проекту. Базовые библиотеки ставятся в Infrastructure, потому что именно там будет код работы с базой, а пакет Design добавляется в API, чтобы он мог быть startup-проектом для миграций.

dotnet add ActivityMonitoring.Infrastructure package Microsoft.EntityFrameworkCore
dotnet add ActivityMonitoring.Infrastructure package Microsoft.EntityFrameworkCore.Sqlite
dotnet add ActivityMonitoring.Infrastructure package Microsoft.EntityFrameworkCore.Design
dotnet add ActivityMonitoring.Api package Microsoft.EntityFrameworkCore.Design

Для PostgreSQL:

dotnet add ActivityMonitoring.Infrastructure package Npgsql.EntityFrameworkCore.PostgreSQL

Для MS SQL:

dotnet add ActivityMonitoring.Infrastructure package Microsoft.EntityFrameworkCore.SqlServer

Для MySQL:

dotnet add ActivityMonitoring.Infrastructure package Pomelo.EntityFrameworkCore.MySql

Модели пользователей и ролей

Пользователь в системе нужен не только для входа. Через пользователя события получают автора: можно понять, кто чаще работает с системой, кто создаёт ошибки, какие роли активнее используют функциональность.

Роль нужна для разграничения доступа. Например, оператор может создавать события, аналитик может смотреть отчёты, администратор может удалять данные.

namespace ActivityMonitoring.Domain.Entities;

public class Role
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public List<AppUser> Users { get; set; } = new();
}

public class AppUser
{
    public int Id { get; set; }
    public string UserName { get; set; } = string.Empty;
    public string PasswordHash { get; set; } = string.Empty;
    public bool IsActive { get; set; } = true;
    public int RoleId { get; set; }
    public Role Role { get; set; } = null!;
    public List<ActivityEvent> Events { get; set; } = new();
}

DbContext

AppDbContext — центральная точка EF Core. Через него приложение получает доступ к таблицам. Свойства DbSet<T> можно воспринимать как коллекции записей, которые EF Core умеет загружать из базы и сохранять обратно.

Метод OnModelCreating уточняет правила: уникальность логина, максимальная длина описания, начальные роли и связи между таблицами.

namespace ActivityMonitoring.Infrastructure.Persistence;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    public DbSet<Role> Roles => Set<Role>();
    public DbSet<AppUser> Users => Set<AppUser>();
    public DbSet<ActivityEvent> ActivityEvents => Set<ActivityEvent>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Role>()
            .HasIndex(x => x.Name)
            .IsUnique();

        modelBuilder.Entity<AppUser>()
            .HasIndex(x => x.UserName)
            .IsUnique();

        modelBuilder.Entity<ActivityEvent>()
            .Property(x => x.Description)
            .HasMaxLength(500);

        modelBuilder.Entity<ActivityEvent>()
            .HasOne(x => x.User)
            .WithMany(x => x.Events)
            .HasForeignKey(x => x.UserId);
    }
}

Подключение в Program.cs

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

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));

Сама строка подключения хранится в конфигурации API-проекта. Так код не привязывается к конкретному пути базы данных и может использовать разные настройки для разработки и запуска.

appsettings.json:

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=activity-monitoring.db"
  }
}

Миграции

Миграция — это история изменения базы данных. Сегодня вы добавили таблицу событий, завтра добавили поле Severity, послезавтра изменили индекс. EF Core сохраняет эти изменения как C#-файлы миграций, чтобы базу можно было воспроизвести на другом компьютере.

Команды ниже сначала устанавливают инструмент миграций, затем создают миграцию по текущим моделям и применяют её к SQLite-базе.

dotnet tool install --global dotnet-ef
dotnet ef migrations add InitialCreate --project ActivityMonitoring.Infrastructure --startup-project ActivityMonitoring.Api
dotnet ef database update --project ActivityMonitoring.Infrastructure --startup-project ActivityMonitoring.Api

Пример запроса EF Core

Этот запрос выбирает события за период, подгружает пользователя, сортирует новые события выше старых и ограничивает результат первыми 100 строками. Такой подход похож на SQL-запрос с JOIN, WHERE, ORDER BY и LIMIT, но записан на C# через LINQ.

var events = await db.ActivityEvents
    .AsNoTracking()
    .Include(x => x.User)
    .Where(x => x.CreatedAt >= from && x.CreatedAt <= to)
    .OrderByDescending(x => x.CreatedAt)
    .Take(100)
    .ToListAsync(ct);

AsNoTracking() используется для чтения, когда записи не планируется изменять. Это снижает накладные расходы EF Core.

Что проверить после настройки БД

  1. Проект собирается без ошибок.
  2. Команда dotnet ef migrations add InitialCreate создаёт папку Migrations.
  3. Команда dotnet ef database update создаёт файл SQLite.
  4. В базе есть таблицы Roles, Users, ActivityEvents.
  5. Начальные роли и пользователи добавлены через HasData.