Entity Configuration
The Problem
Section titled “The Problem”EF Core needs to know how to map your entities to the database: which property is the primary key, which properties need indexes, how relationships are configured, what the column types should be. Without configuration, EF Core guesses, and sometimes guesses wrong.
Writing IEntityTypeConfiguration<T> for each entity is repetitive: every entity needs a primary key on PersistenceId, every [LogicKey] property needs a unique index, every [SoftDelete] entity needs a query filter.
The Solution
Section titled “The Solution”The source generator produces a complete IEntityTypeConfiguration<T> for each entity, based on the attributes you’ve declared.
// ═══ Generated: EntityConfig.{Namespace}.Order.g.cs ═══internal sealed class OrderEntityConfig : IEntityTypeConfiguration<Order>{ public void Configure(EntityTypeBuilder<Order> builder) { builder.ToTable("Orders"); // the type name, pluralised builder.HasKey(e => e.PersistenceId);
// A Guid key is assigned by the trait at construction, so the store must not claim it builder.Property(e => e.PersistenceId).ValueGeneratedNever();
// [LogicKey] → unique index builder.HasIndex(e => e.OrderNumber).IsUnique();
// private set → EF writes the field, not the property builder.Property(e => e.OrderNumber).UsePropertyAccessMode(PropertyAccessMode.Field).IsRequired();
// [SoftDelete] → a *named* EF query filter, plus an index on the flag builder.HasQueryFilter("SoftDelete", e => !e.IsDeleted); builder.HasIndex(e => e.IsDeleted);
// [Relation.OneToMany<LineItem>] declared on the parent → Cascade builder.HasMany(e => e.LineItems) .WithOne(e => e.Order) .OnDelete(DeleteBehavior.Cascade); }}Three things that are not in there:
- The concurrency token.
[ConcurrencyAware]produces a shadow property, declared in the generated DbContext and shaped by the provider; there is noe.RowVersionto configure. - The audit fields.
[Auditable]adds the four properties to the entity; nothing configures them here, so they take EF’s conventions. - The filtered unique index. On a
[SoftDelete]entity the logic-key index has to exclude deleted rows, or a re-insert with the same key fails. That filter is raw provider SQL, so it is emitted in the DbContext where the provider is known:modelBuilder.Entity<Order>().HasIndex(e => e.OrderNumber).IsUnique().HasFilter("\"IsDeleted\" = 0").
Property Mapping Rules
Section titled “Property Mapping Rules”The SG applies these rules when generating property configuration:
| C# Type | Generated Configuration | Notes |
|---|---|---|
string |
.IsRequired() if not nullable |
.HasMaxLength(n) if [MaxLength(n)] present |
decimal |
.HasPrecision(18, 2) |
Default precision for monetary values |
byte[] (RowVersion) |
.IsRowVersion() |
Only for [ConcurrencyAware] entities |
enum |
Stored as int |
Default EF Core behavior |
DateTimeOffset |
.IsRequired() if not nullable |
Audit fields: CreatedAt is required, UpdatedAt is optional |
Relationship Configuration
Section titled “Relationship Configuration”For each [Relation.*] attribute on the entity class, the SG generates the corresponding Fluent API calls:
// [Relation.OneToMany<LineItem>] on Order: declared by the parent, so Cascadebuilder.HasMany(e => e.LineItems) .WithOne(e => e.Order) .OnDelete(DeleteBehavior.Cascade);
// [Relation.ManyToOne<Customer>] on Order: declared by the child, so Restrictbuilder.HasOne(e => e.Customer) .WithMany() .HasForeignKey(e => e.CustomerId) .IsRequired() .OnDelete(DeleteBehavior.Restrict);
// [Relation.OneToOne<RelationSide>]: the principal holds the FKbuilder.HasOne(e => e.RelationSide) .WithOne() .HasForeignKey<Order>(e => e.RelationSideId) .OnDelete(DeleteBehavior.Restrict);The navigation names come from the target type, pluralised for collections: LineItems, not any
property you wrote. .WithNavigation("Items") on the relation renames it.
The delete behaviour depends on which side declares the relation: [Relation.OneToMany] on the parent gives Cascade, [Relation.ManyToOne] on the child gives Restrict, and an inverse the generator derives on its own gets NoAction. See Relationships: the default is not the cautious one.
Two Levels of Query Filters
Section titled “Two Levels of Query Filters”The generated configuration includes two complementary filtering mechanisms:
1. EF Core Global Query Filter (in EntityConfiguration)
Section titled “1. EF Core Global Query Filter (in EntityConfiguration)”builder.HasQueryFilter("SoftDelete", e => !e.IsDeleted);EF Core’s built-in mechanism, applied at the model level. It is a named filter, so it can be lifted
on its own with IgnoreQueryFilters(["SoftDelete"]) rather than all-or-nothing.
2. Repository-Level Query Filter (IQueryFilter pipeline)
Section titled “2. Repository-Level Query Filter (IQueryFilter pipeline)”// In the generated repositoryvar filter = _filterProvider?.GetCombinedFilter<Order>(context);query = query.Where(filter);This is Pragmatic’s filter pipeline. It supports runtime toggle (IQueryFilterToggle), filter modes, and navigation-level filtering.
Why both? The EF Core filter provides a safety net: even if code bypasses the repository and queries the DbSet directly, soft-deleted records are still excluded. The Pragmatic filter pipeline adds runtime configurability (disable, modes, navigation filtering).
Inheritance Configuration
Section titled “Inheritance Configuration”For entities with [Inheritance], a separate static configuration class is generated:
// ═══ Generated: {Namespace}.Fee.InheritanceMapping.g.cs ═══public static class FeeInheritanceConfiguration{ public static void Configure(ModelBuilder modelBuilder) { modelBuilder.Entity<Fee>() .HasDiscriminator<string>("FeeType") .HasValue<ServiceFee>("ServiceFee") .HasValue<CancellationFee>("CancellationFee"); }}This is called in OnModelCreating after the ApplyConfiguration calls:
protected override void OnModelCreating(ModelBuilder modelBuilder){ base.OnModelCreating(modelBuilder);
// Entity configurations modelBuilder.ApplyConfiguration(new FeeEntityConfiguration()); modelBuilder.ApplyConfiguration(new ServiceFeeEntityConfiguration());
// Inheritance mappings (after configurations) FeeInheritanceConfiguration.Configure(modelBuilder);}The order matters: entity configurations define properties and relationships, then inheritance configuration defines the discriminator and type mappings.
