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

Ignore Types and Properties, Configure Backing Fields, and Control Property Access Modes

Persist encapsulated domain state with ignored members, backing fields, field-only properties, and explicit property access modes while preserving materialization correctness.

Intermediate90–110 minutesbacking-field + encapsulation labEF Core 10.0.11 · .NET 10SQLite provider 10.0.11 baselineLast reviewed: August 2026

Learning outcomes

ServiceHub's domain model is about to gain behavior. A useful domain object often has computed properties, private setters, backing fields, and invariants that should not all become columns. EF Core can persist encapsulated state without turning every field into a public mutable property—but only if the mapping is explicit and materialization/change-tracking behavior is understood.

01

Exclude types/properties with Ignore and NotMapped without confusing “not persisted” with “not part of business logic.”

02

Use convention-discovered or explicit backing fields with HasField.

03

Configure PropertyAccessMode deliberately and explain when EF reads/writes fields versus property accessors.

04

Create and query a field-only property with EF.Property where appropriate.

05

Diagnose broken backing-field mappings and encapsulation designs that EF cannot materialize safely.

1. Not every public member is persistent state

Suppose dispatch wants a display label composed from existing values. Persisting it would duplicate data and introduce synchronization risk.

csharp · computed member that should not be mapped
public sealed class WorkOrder{    public int Id { get; set; }    public string CustomerName { get; set; } = string.Empty;    public string Summary { get; set; } = string.Empty;    public string DisplayLabel => $"#{Id} - {CustomerName}: {Summary}";}

If EF considers a member mappable and it should not persist, exclude it explicitly.

csharp · Fluent Ignore
builder.Ignore(x => x.DisplayLabel);
csharp · attribute alternative
using System.ComponentModel.DataAnnotations.Schema;[NotMapped]public string DisplayLabel => $"#{Id} - {CustomerName}: {Summary}";

For the course policy established in Lesson 3, prefer Fluent Ignore so persistence configuration remains in the Data layer.

2. Entire CLR types can be ignored

csharp · in-memory helper type
public sealed record DispatchRecommendation(string Team, string Reason);protected override void OnModelCreating(ModelBuilder modelBuilder){    modelBuilder.Ignore<DispatchRecommendation>();}

Ignoring a type is useful when it is reachable from other domain objects but is not an entity/value object intended for EF mapping. Do not use Ignore<T> to hide a relationship or persistence requirement you have not modeled correctly. Exclusion should reflect architecture, not silence model validation.

3. Backing fields preserve encapsulation

Imagine that arbitrary code must not replace a work-order summary with whitespace. The domain can own mutation through a method while EF persists the field.

csharp · encapsulated summary with backing field
public sealed class WorkOrder{    private string _summary = string.Empty;    private WorkOrder() { } // EF materialization    public WorkOrder(        string customerName,        string summary,        WorkOrderPriority priority,        DateTimeOffset openedUtc)    {        CustomerName = string.IsNullOrWhiteSpace(customerName)            ? throw new ArgumentException("Customer name is required.", nameof(customerName))            : customerName.Trim();        Priority = priority;        OpenedUtc = openedUtc;        ReviseSummary(summary);    }    public int Id { get; private set; }    public string CustomerName { get; private set; } = string.Empty;    public string Summary => _summary;    public WorkOrderPriority Priority { get; private set; }    public DateTimeOffset OpenedUtc { get; private set; }    public string DisplayLabel => $"#{Id} - {CustomerName}: {Summary}";    public void ReviseSummary(string value)    {        if (string.IsNullOrWhiteSpace(value))            throw new ArgumentException("Summary cannot be blank.", nameof(value));        _summary = value.Trim();    }}

EF has backing-field naming conventions such as _summary for a Summary property. Explicit mapping is clearer when encapsulation is intentional. Because Chapter 01 seeded WorkOrder with object initializers, this domain refactor also requires the deterministic seed code to use the new public constructor; otherwise the course lab would stop compiling even though the database mapping is valid.

csharp · update deterministic seed construction
new WorkOrder(    "Northwind Workshop",    "Replace vibration sensor on compressor C-14",    WorkOrderPriority.High,    DateTimeOffset.Parse("2026-08-20T08:00:00Z"))
csharp · explicit backing-field mapping
builder.Property(x => x.Summary)    .HasField("_summary")    .HasColumnName("summary")    .HasMaxLength(400)    .IsRequired();

4. PropertyAccessMode controls how EF reaches the value

EF can access a field or property when reading/writing tracked values and materializing objects. The exact choice matters if property setters execute validation, publish events, normalize values, or have other behavior.

csharp · explicit access mode
builder.Property(x => x.Summary)    .HasField("_summary")    .UsePropertyAccessMode(PropertyAccessMode.Field);

Field requires field access. Other modes include Property, PreferField, PreferProperty, and construction-sensitive variants. Do not switch modes as a performance micro-optimization without understanding domain semantics. A property setter that raises a domain event during materialization can be a correctness problem; bypassing a setter that establishes a crucial invariant can also be a problem.

Mode idea Why choose it Risk to inspect
Field Persistence should bypass public setter logic Can bypass validation/event logic that application code relies on
Property Accessor logic must always run Materialization may invoke side effects or reject database values
PreferField Use backing field when available Behavior may differ between members with/without fields
FieldDuringConstruction / PreferFieldDuringConstruction Use fields while materializing, properties later Requires careful understanding of when domain behavior should run

5. Field-only properties are persisted without a CLR property

Sometimes infrastructure state belongs in the database but should not appear in the public domain API. EF can map a field directly.

csharp · field-only infrastructure value
public sealed class WorkOrder{    private DateTimeOffset _lastTouchedUtc;    // Other domain members...}builder.Property<DateTimeOffset>("_lastTouchedUtc")    .HasColumnName("last_touched_utc");

Because there is no CLR property in a lambda, queries access it with EF.Property.

csharp · query a field-only property
var recent = await db.WorkOrders    .Where(w => EF.Property<DateTimeOffset>(w, "_lastTouchedUtc") >= cutoff)    .OrderBy(w => w.Id)    .ToListAsync(ct);

Use this sparingly. If application behavior needs the value, a private/public domain abstraction may be clearer. Field-only state is not a way to make important business data invisible to maintainers.

6. Shadow property and field-only property are different

A field-only property has CLR storage in a field. A shadow property has no CLR property or field; its value lives in EF's change tracker for tracked entities. Both can be accessed in queries through EF.Property, but their runtime storage and detached-entity behavior differ.

Kind CLR storage? Tracked value location Common use
Normal property Yes, property Entity + tracker metadata Domain state
Backing-field property Yes, field + optional property Entity field Encapsulation
Field-only property Yes, field only Entity field Infrastructure/internal persisted state
Shadow property No ChangeTracker entry Foreign keys/audit/infrastructure metadata when CLR member is undesirable

7. Deliberately broken mapping: HasField points at a field that does not exist

csharp · broken field name
builder.Property(x => x.Summary)    .HasField("_summmary"); // typo: three m characters

Model building should fail rather than silently use an imaginary field. The exact exception wording can change, but the diagnosis is straightforward: inspect the CLR type and finalized model configuration, correct the field name, and add a model-contract test so refactoring does not silently break the mapping.

Safer refactoring

String field names are vulnerable to rename drift. Keep configuration close to the entity configuration, cover it with a context-model test, and use code review/refactoring tooling carefully. Do not suppress model-validation errors merely to get startup past the failure.

8. Materialization is not normal application mutation

When EF constructs an entity from database rows, it must populate mapped state. That process is not equivalent to a user invoking domain methods. If setters enforce commands such as “only a dispatcher may revise summary,” those authorization rules should not run while reconstructing already-persisted state. Conversely, if the database contains invalid historical values, bypassing setters does not magically make the domain valid.

Define invariants at appropriate boundaries: database constraints for storage invariants, constructors/factories/domain methods for application state transitions, and materialization-compatible mapping for persistence. Later chapters cover constructors, complex/owned types, converters, and more domain-friendly persistence in depth.

9. Hands-on lab: encapsulate Summary without changing stored data

  1. Convert Summary to a getter backed by _summary and add the constructor shown above.
  2. Update ServiceHubSeed to construct deterministic work orders through that constructor rather than object-initializing private-set members.
  3. Add ReviseSummary with a non-blank invariant.
  4. Map Summary to _summary explicitly and choose PropertyAccessMode.Field.
  5. Reset/apply migrations only if the relational schema changed; a pure access-strategy change should not require destructive DDL.
  6. Load seeded work orders and prove EF materializes the persisted summaries.
  7. Revise one summary through the domain method, call SaveChangesAsync, and inspect generated UPDATE.
  8. Add DisplayLabel and ignore it; prove it does not appear in model properties.
  9. Optionally add _lastTouchedUtc as a field-only property and query it with EF.Property.
  10. Introduce a deliberate HasField typo, capture the model-building failure, then repair it.

Check your understanding

  1. What is the purpose of a backing field mapping?
  2. Does [NotMapped] remove a property from the CLR type?
  3. How is a field-only property different from a shadow property?
  4. Why can using property setters during materialization be dangerous?
  5. What should you do after renaming a private backing field?
Review the answers

It lets EF persist/read field storage while the public property API can remain encapsulated or read-only.

No. It only tells EF not to map the member; application code can still use it.

A field-only property has CLR field storage; a shadow property has no CLR member and its tracked value lives in EF state.

Setters may run validation, authorization, events, or other side effects that are appropriate for commands but not object reconstruction.

Update/verify mapping, run model-contract tests, and exercise materialization/update paths so a string-based HasField mapping cannot drift unnoticed.

10. Production judgment and bridge

Encapsulation is valuable when it expresses domain rules, but persistence tricks should not make a model impossible to understand. Use backing fields deliberately, document access-mode choices, and test both materialization and updates. Avoid hiding ordinary business data in shadow/field-only state solely to make the CLR type look “clean.”

Lesson 5 addresses the next scaling problem: as ServiceHub gains many entity mappings, OnModelCreating must not become an unreviewable thousand-line method. You will split mapping into IEntityTypeConfiguration<T> classes, use assembly scanning carefully, and validate the resulting model.

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