TGViewer
.NET Разработчик .NET Разработчик @netdeveloperdiary · 6.74K subscribers
Post #3302 1.55K
День 2758. #ЗаметкиНаПолях #DDD
Сохраняем Богатую Доменную Модель в EF Core. Продолжение

Начало

Теперь разберём, как EF Core сохраняет и загружает такой богатый агрегат (см. код в предыдущем посте).

Приватные конструкторы и приватные сеттеры просто работают
Когда EF Core материализует сущность, он не использует публичный API. Он вызывает приватный конструктор без параметров и записывает данные в свойства через их базовые поля, приватные сеттеры и т.п.. Поэтому пустой приватный конструктор - единственная уступка, которую доменная модель делает ORM. Загрузка и изменение партии выглядят обычно:
Batch batch = await context.Batches
.SingleAsync(b => b.Id == batchId);

batch.AddReading(new Gravity(1.012m), timeProvider.GetUtcNow().UtcDateTime);

await context.SaveChangesAsync();

Трекер изменений считывает те же самые базовые поля, поэтому приватные сеттеры ничего не скрывают от SaveChanges.

EF может даже использовать привязку через параметризованный конструктор, сопоставляя параметры с отображаемыми свойствами по имени и типу, независимо от того, приватные они или нет. Но это работает только для скалярных типов. Свойства навигации не могут быть привязаны через конструктор, поэтому агрегату с коллекцией по-прежнему нужен конструктор без параметров. EF также пропускает фабричные валидации при загрузке, что правильно: повторное выполнение валидации во время материализации сделало бы старые объекты недоступными для загрузки при изменении правил в будущем.

Типизированные ID и ссылки на другие агрегаты
BatchId и RecipeId — строго типизированные идентификаторы, сопоставляемые с помощью преобразования значений:
public sealed class BatchConfiguration
: IEntityTypeConfiguration<Batch>
{
public void Configure(EntityTypeBuilder<Batch> bld)
{
bld.ToTable("batches");
bld.HasKey(b => b.Id);
bld.Property(b => b.Id)
.HasConversion(id => id.Value,
value => new BatchId(value))
.ValueGeneratedNever();

bld.Property(b => b.RecipeId)
.HasConversion(id => id.Value,
value => new RecipeId(value));
}
}

ValueGeneratedNever важно: мы генерируем Guid.CreateVersion7() в фабрике, поэтому говорим EF не вмешиваться.
RecipeId не является свойством навигации для Recipe. Рецепт — это самостоятельный агрегат, но здесь он нам не нужен.

Инкапсулированная коллекция
По соглашению, EF находит поле _readings для навигации по Readings. Но его лучше настроить явно, чтобы сопоставление сохранялось даже после переименования:
// продолжение BatchConfiguration.Configure()
…
bld.HasMany<FermentationReading>("_readings")
.WithOne()
.HasForeignKey("batch_id");

bld.Navigation("_readings")
.UsePropertyAccessMode(PropertyAccessMode.Field)
.AutoInclude();

PropertyAccessMode.Field указывает EF читать и записывать поле, не обращаясь к публичному представлению Readings. AutoInclude() важно добавлять, т.к. метод Bottle() читает _readings, поэтому использование частично загруженного Batch небезопасно. WithOne() без аргументов означает, что дочерний элемент не имеет навигации обратно к Batch; внешний ключ batch_id существует только как теневое свойство.

Состояние без свойств
_bottledAt не имеет свойств, только приватное поле, и EF всё равно его сопоставляет:
bld.Property<DateTime?>("_bottledAt")
.HasColumnName("bottled_at");

Однако, чтобы отфильтровать по этому полю, придётся добавлять в запрос «костыль»:
var thisWeek = await context.Batches
.Where(b => EF.Property<DateTime?>(b, "_bottledAt") >= weekAgo)
.ToListAsync();

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

Окончание следует…

Источник:
https://milanjovanovic.tech/blog/persisting-a-rich-domain-model-with-ef-core
  • 👎 2
  • 👍 1
More from @netdeveloperdiary
  1. Sep 26, 2026День 2796. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Продолжение Начало Три…
  2. Sep 25, 2026День 2795. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Начало Проблема с позиц…
  3. Sep 24, 2026День 2794. #Оффтоп #Здоровье Сегодня будет необычный пост. Завтра в Москве стартует конфер…
  4. Sep 23, 2026День 2793. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  5. Sep 22, 2026День 2792. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  6. Sep 21, 2026🔍Тестовое собеседование с Senior C# разработчиком уже завтра 22 сентября(уже завтра!) в 1…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →