Hard📖Теория2 min

Шаг 13: DbContext и Entity Configurations

Создание OrderDbContext с IUnitOfWork, конфигурации маппинга Order и OrderItem: Owned Types для Value Objects, индексы и связи

Шаг 13: DbContext и конфигурации EF Core

DbContext -- главный класс Entity Framework Core, представляющий сессию с базой данных. Через него выполняется чтение, запись и транзакции.

OrderDbContext.cs

В папке Persistence проекта Infrastructure:

using Microsoft.EntityFrameworkCore;
using OrderManagement.Domain.Entities;
using OrderManagement.Domain.Interfaces;

namespace OrderManagement.Infrastructure.Persistence;

public class OrderDbContext : DbContext, IUnitOfWork
{
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<OrderItem> OrderItems => Set<OrderItem>();

    public OrderDbContext(
        DbContextOptions<OrderDbContext> options)
        : base(options) { }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(
            typeof(OrderDbContext).Assembly);
    }
}

DbSet<Order> -- коллекция, привязанная к таблице. ApplyConfigurationsFromAssembly автоматически находит все IEntityTypeConfiguration<T>.

OrderConfiguration.cs

В папке Persistence/Configurations:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using OrderManagement.Domain.Entities;

namespace OrderManagement.Infrastructure.Persistence.Configurations;

public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.ToTable("Orders");
        builder.HasKey(o => o.Id);

        builder.Property(o => o.CustomerId)
            .IsRequired().HasMaxLength(100);
        builder.Property(o => o.Status)
            .IsRequired().HasConversion<string>().HasMaxLength(20);
        builder.Property(o => o.CreatedAt).IsRequired();

        builder.OwnsOne(o => o.ShippingAddress, sa =>
        {
            sa.Property(a => a.Street)
                .HasColumnName("ShippingStreet")
                .HasMaxLength(200).IsRequired();
            sa.Property(a => a.City)
                .HasColumnName("ShippingCity")
                .HasMaxLength(100).IsRequired();
            sa.Property(a => a.PostalCode)
                .HasColumnName("ShippingPostalCode")
                .HasMaxLength(20).IsRequired();
            sa.Property(a => a.Country)
                .HasColumnName("ShippingCountry")
                .HasMaxLength(100).IsRequired();
        });

        builder.OwnsOne(o => o.TotalAmount, m =>
        {
            m.Property(x => x.Amount)
                .HasColumnName("TotalAmount")
                .HasColumnType("decimal(18,2)").IsRequired();
            m.Property(x => x.Currency)
                .HasColumnName("TotalCurrency")
                .HasMaxLength(3).IsRequired();
        });

        builder.HasMany(o => o.Items).WithOne()
            .HasForeignKey("OrderId")
            .OnDelete(DeleteBehavior.Cascade);

        builder.Ignore(o => o.DomainEvents);
        builder.HasIndex(o => o.CustomerId);
        builder.HasIndex(o => o.Status);
    }
}

OwnsOne маппит Value Object как Owned Type -- столбцы хранятся в таблице Orders. HasConversion<string>() сохраняет enum как текст. Ignore исключает DomainEvents из схемы БД.

OrderItemConfiguration.cs

public class OrderItemConfiguration
    : IEntityTypeConfiguration<OrderItem>
{
    public void Configure(EntityTypeBuilder<OrderItem> builder)
    {
        builder.ToTable("OrderItems");
        builder.HasKey(i => i.Id);
        builder.Property(i => i.ProductId)
            .IsRequired().HasMaxLength(100);
        builder.Property(i => i.ProductName)
            .IsRequired().HasMaxLength(200);
        builder.Property(i => i.Quantity).IsRequired();

        builder.OwnsOne(i => i.UnitPrice, m =>
        {
            m.Property(x => x.Amount)
                .HasColumnName("UnitPrice")
                .HasColumnType("decimal(18,2)").IsRequired();
            m.Property(x => x.Currency)
                .HasColumnName("UnitCurrency")
                .HasMaxLength(3).IsRequired();
        });

        builder.Ignore(i => i.DomainEvents);
        builder.Ignore(i => i.TotalPrice);
    }
}

Проверь себя

🧪

Что делает OwnsOne для ShippingAddress?

🧪

Почему TotalPrice в OrderItem игнорируется (builder.Ignore)?

🧪

Зачем HasConversion<string>() для OrderStatus?