EF Core does not force anemic domain models — a fully encapsulated aggregate with private constructors, private setters, backing fields, and no public parameterless constructor can be persisted cleanly. The post walks through mapping a `Batch` aggregate end to end: strongly typed IDs with value conversions, encapsulated collections via `PropertyAccessMode.Field`, field-only state with no property, value objects as complex types (EF Core 8+) or owned types, enum-to-string conversions, and domain events dispatched via a `SaveChangesInterceptor` without leaking into the schema. All mapping lives in `IEntityTypeConfiguration` classes, keeping the domain model free of any EF Core references.
Table of contents
The Aggregate We Want to PersistPrivate Constructors and Private Setters Just WorkStrongly Typed IDs and References to Other AggregatesThe Encapsulated CollectionState With No Property at AllValue Objects: Complex Types, Owned Types, and ConversionsDomain Events Stay Out of the SchemaThe Domain Never References EF CoreSummaryQuestions this post answers
Can EF Core work with private constructors and private setters in a domain model?
EF Core materializes entities by calling the private parameterless constructor and writing to properties through their backing fields, bypassing public setters entirely. This means a fully encapsulated aggregate with private setters and a single private `Batch() { }` constructor works without any changes to the domain model. Change tracking also reads backing fields, so `SaveChanges` sees all state changes. Developers building DDD aggregates in .NET track EF Core mapping patterns like these on daily.dev.
How do I map a private backing field with no public property in EF Core?
Use `builder.Property<DateTime?>("_bottledAtUtc").HasColumnName("bottled_at_utc")` in the entity configuration. EF Core maps the field directly by name. The trade-off is that LINQ filtering on that field requires `EF.Property<DateTime?>(entity, "_bottledAtUtc")` in queries. A private setter is preferable for state that queries need to filter on; field-only mapping suits state only the aggregate itself reads. Teams enforcing persistence ignorance in .NET find EF Core mapping edge cases like this covered on daily.dev.
How do I map an encapsulated collection in EF Core so EF writes to the backing field not the public property?
Configure the navigation using the backing field name and set `PropertyAccessMode.Field`: `builder.HasMany<FermentationReading>("_readings").WithOne().HasForeignKey("batch_id")` followed by `builder.Navigation("_readings").UsePropertyAccessMode(PropertyAccessMode.Field).AutoInclude()`. This tells EF to read and write the private list directly, leaving the public `IReadOnlyCollection` view untouched. Keeping aggregates safe from partial loads is the kind of EF Core detail .NET developers follow on daily.dev.
49.5K Impressions1 Comment