Chapter 03 · Model Building: Conventions, Data Annotations, Fluent API, and Metadata

Organize Large Models with IEntityTypeConfiguration, Assembly Scanning, and Validation

Scale model configuration with IEntityTypeConfiguration, explicit application, assembly scanning, deterministic ownership, metadata validation, and migration-drift checks.

Intermediate95–120 minutesconfiguration modularization + validation labEF Core 10.0.11 · .NET 10SQLite provider 10.0.11 baselineLast reviewed: August 2026

Learning outcomes

By this point, ServiceHub mapping contains table/column facets, ignored members, and encapsulation rules. Leaving all of that in one OnModelCreating method makes ownership, review, merge conflicts, and testing progressively worse. EF Core provides IEntityTypeConfiguration<TEntity> and assembly scanning to modularize model construction—but modularization introduces its own ordering and discovery rules that must be explicit.

01

Split entity mapping into focused IEntityTypeConfiguration classes and apply them explicitly.

02

Use ApplyConfigurationsFromAssembly with an accurate understanding of discovery, constructor, ordering, and trimming constraints.

03

Explain why assembly-scan configuration order is undefined and why conflicting configurations are therefore a design defect.

04

Validate the composed model with metadata assertions and migration-diff checks.

05

Establish a scalable ServiceHub configuration folder/namespace convention without turning model construction into hidden reflection magic.

1. Move WorkOrder mapping out of the context

csharp · Data/Configurations/WorkOrderConfiguration.cs
using Microsoft.EntityFrameworkCore;using Microsoft.EntityFrameworkCore.Metadata.Builders;using ServiceHub.EfLab.Domain;namespace ServiceHub.EfLab.Data.Configurations;public sealed class WorkOrderConfiguration    : IEntityTypeConfiguration<WorkOrder>{    public void Configure(EntityTypeBuilder<WorkOrder> builder)    {        builder.ToTable("work_orders");        builder.HasKey(x => x.Id);        builder.Property(x => x.Id)            .HasColumnName("work_order_id");        builder.Property(x => x.CustomerName)            .HasColumnName("customer_name")            .HasMaxLength(120)            .IsRequired();        builder.Property(x => x.Summary)            .HasField("_summary")            .UsePropertyAccessMode(PropertyAccessMode.Field)            .HasColumnName("summary")            .HasMaxLength(400)            .IsRequired();        builder.Property(x => x.Priority)            .HasColumnName("priority");        builder.Property(x => x.OpenedUtc)            .HasColumnName("opened_utc");        builder.Ignore(x => x.DisplayLabel);    }}

The context can now focus on model composition rather than every entity detail.

csharp · explicit configuration application
protected override void OnModelCreating(ModelBuilder modelBuilder){    modelBuilder.ApplyConfiguration(new WorkOrderConfiguration());}

Explicit application gives deterministic, reviewable order. It is an excellent default when the model is modest or when some configurations intentionally layer over others.

2. Assembly scanning removes repetitive ApplyConfiguration calls

csharp · scan the assembly containing configuration classes
protected override void OnModelCreating(ModelBuilder modelBuilder){    modelBuilder.ApplyConfigurationsFromAssembly(        typeof(WorkOrderConfiguration).Assembly);}

EF locates IEntityTypeConfiguration<T> implementations in the assembly. A predicate can restrict which types are applied.

csharp · optional namespace filter
modelBuilder.ApplyConfigurationsFromAssembly(    typeof(ServiceHubContext).Assembly,    type => type.Namespace == "ServiceHub.EfLab.Data.Configurations");

3. Assembly-scanned configuration order is undefined

Microsoft's model-building documentation explicitly warns that the order in which ApplyConfigurationsFromAssembly applies configurations is undefined. Therefore scanned configuration classes must not rely on “A runs before B” to resolve conflicting values.

csharp · deliberately conflicting configurations — do not ship
public sealed class WorkOrderLengthA : IEntityTypeConfiguration<WorkOrder>{    public void Configure(EntityTypeBuilder<WorkOrder> b)        => b.Property(x => x.CustomerName).HasMaxLength(80);}public sealed class WorkOrderLengthB : IEntityTypeConfiguration<WorkOrder>{    public void Configure(EntityTypeBuilder<WorkOrder> b)        => b.Property(x => x.CustomerName).HasMaxLength(120);}

If both are discovered by scanning, relying on one to “win” is invalid design because order is undefined. Even if a particular build appears stable, reflection/discovery details are not your configuration-order contract.

Repair

Have one authoritative configuration for each setting. If deliberate layering is required, apply those configuration objects explicitly in a documented order rather than relying on assembly scanning. Add metadata assertions to catch accidental duplicates.

4. Configuration classes discovered by scanning need usable constructors

The current EF Core model-building documentation notes that assembly scanning instantiates configuration types with parameterless constructors; types whose constructors require parameters are skipped and a warning is logged. That is a strong signal that configuration classes should normally be deterministic metadata definitions, not service-locator objects that pull request/environment state into model construction.

csharp · bad fit for assembly scanning
public sealed class WorkOrderConfiguration(IConfiguration configuration)    : IEntityTypeConfiguration<WorkOrder>{    public void Configure(EntityTypeBuilder<WorkOrder> builder)    {        // Runtime/environment state is now coupled to model construction.    }}

If a configuration genuinely requires constructor arguments, instantiate it manually and apply it explicitly. Before doing that, ask whether the argument itself violates the stable-model/cache assumptions from Lesson 1.

5. Scanning has trimming/NativeAOT implications

The EF Core 10 API for ApplyConfigurationsFromAssembly is annotated RequiresUnreferencedCode because it searches arbitrary assembly types. That matters for trimming and NativeAOT scenarios where reflection-discovered types can be removed unless preserved. Chapter 17 returns to compiled models/startup/NativeAOT performance. For now, treat assembly scanning as a runtime discovery mechanism with deployment implications, not a zero-cost syntactic convenience.

Production judgment

If your deployment uses aggressive trimming/NativeAOT, verify the supported EF model-generation path and warnings for that target. Do not suppress trimming warnings without proving configuration types are preserved.

6. Organize by entity/aggregate ownership, not by API method

text · recommended ServiceHub model layout
src/ServiceHub.EfLab/├─ Domain/│  └─ WorkOrder.cs└─ Data/   ├─ ServiceHubContext.cs   ├─ Configurations/   │  └─ WorkOrderConfiguration.cs   ├─ ServiceHubContextFactory.cs   ├─ ServiceHubSeed.cs   ├─ ServiceHubDiagnostics.cs   └─ Migrations/

Do not create separate global files named MaxLengths.cs, ColumnNames.cs, and Requiredness.cs that force reviewers to assemble one entity's mapping mentally from many places. A focused configuration per entity/aggregate is usually easier to reason about.

7. Validate the composed model programmatically

csharp · model validation helper for the lab
public static void ValidateServiceHubModel(ServiceHubContext db){    var entity = db.Model.FindEntityType(typeof(WorkOrder))        ?? throw new InvalidOperationException("WorkOrder missing from model.");    if (entity.GetTableName() != "work_orders")        throw new InvalidOperationException("Unexpected WorkOrder table mapping.");    var customer = entity.FindProperty(nameof(WorkOrder.CustomerName))!;    if (customer.GetMaxLength() != 120 || customer.IsNullable)        throw new InvalidOperationException("CustomerName mapping drifted.");    var summary = entity.FindProperty(nameof(WorkOrder.Summary))!;    if (summary.GetMaxLength() != 400 || summary.IsNullable)        throw new InvalidOperationException("Summary mapping drifted.");}

These assertions validate EF metadata after all conventions/annotations/configurations have composed. They catch accidental mapping drift before a migration reaches deployment. Add relational provider integration tests as the model grows; metadata checks alone do not prove SQLite/SQL Server/PostgreSQL DDL equivalence.

8. Migration stability is part of model-organization quality

Moving equivalent Fluent calls from OnModelCreating into IEntityTypeConfiguration should not create a schema migration if the finalized model is truly unchanged. Use that as a refactoring acceptance test.

text · refactor verification
# Before refactor: working tree clean and current migrations applied.# Move mapping to WorkOrderConfiguration, then:dotnet tool run dotnet-ef -- migrations add VerifyConfigurationRefactor --project src/ServiceHub.EfLab# Inspect generated migration. If there are unexpected operations, the model changed.# If the migration is empty and was only a probe, remove it:dotnet tool run dotnet-ef -- migrations remove --project src/ServiceHub.EfLab

Do not automatically commit an empty probe migration. The point is evidence that a code-organization refactor preserved model semantics.

9. Hands-on lab: refactor to configuration classes and prove no drift

  1. Move all WorkOrder mapping from ServiceHubContext.OnModelCreating into WorkOrderConfiguration.
  2. First apply it explicitly with ApplyConfiguration; run the app and metadata validation.
  3. Generate a probe migration and confirm the refactor did not alter the model/schema.
  4. Switch to ApplyConfigurationsFromAssembly with a namespace predicate.
  5. Run metadata validation again.
  6. Add a second deliberately conflicting configuration class, observe the model result/warning risk, and then remove it; do not rely on scan order.
  7. Optionally add a configuration class with a required constructor parameter and observe the logged skip warning during scan; then remove it.
  8. Record trimming/NativeAOT warning implications in the lab notes even if the current course app is not published with trimming.

Verification checklist

  • ServiceHubContext is small and composes configuration rather than containing every mapping.
  • Exactly one authoritative class configures each WorkOrder facet.
  • Assembly scanning does not rely on configuration order.
  • Metadata validation passes.
  • A refactor-only migration probe has no unexpected schema operations.
  • No configuration constructor pulls per-request state into model building.

Check your understanding

  1. What interface modularizes entity mapping?
  2. Is ApplyConfigurationsFromAssembly application order defined?
  3. What should you do if two configuration fragments intentionally need ordering?
  4. Why can parameterized configuration constructors be a problem for assembly scanning?
  5. What additional deployment concern does ApplyConfigurationsFromAssembly have?
  6. How can you prove a mapping refactor did not change schema semantics?
Review the answers

IEntityTypeConfiguration.

No. Microsoft documents the scan application order as undefined.

Apply them explicitly in the required order and make the layering contract obvious instead of relying on scanning.

Assembly scanning instantiates suitable parameterless configuration classes; parameterized ones can be skipped/log warnings, and runtime dependencies often violate stable-model design.

It uses reflection and is marked RequiresUnreferencedCode, so trimming/NativeAOT scenarios require attention.

Inspect finalized metadata and generate/review a probe migration; an organization-only refactor should not introduce unexpected migration operations.

10. Chapter 03 production judgment and bridge

Chapter 03 converts EF model building from implicit convention behavior into a reviewable metadata pipeline. ServiceHub now has an observable finalized model, deliberate relational facets, a clear configuration-precedence policy, encapsulated persistence where useful, and modular configuration classes with validation.

Before production, keep Microsoft EF packages aligned at the current supported patch, run migration/provider integration tests on the actual production engine, avoid runtime-dependent model shapes unless deliberately engineered, treat assembly-scan order as undefined, and preserve configuration types under any trimming/AOT deployment strategy. Chapter 04 builds on this model foundation with keys, indexes, value generation, shadow state, and concurrency-sensitive property semantics.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n