Chapter 06 · Complex Types, Owned Types, JSON Mapping, Conversions, Comparers, and Encapsulation

Owned Entity Types and Aggregate Boundaries: OwnsOne, OwnsMany, and Table Mapping

Use owned entity types only when lifecycle-bound entity semantics are real, and prove hidden identity, OwnsOne/OwnsMany keys, table mapping, tracking, and ownership behavior.

Intermediate100–125 minutesowned identity + OwnsMany mapping labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

Learning outcomes

Complex types model value semantics, but some lifecycle-bound structures still need entity-style tracking and ownership. A work-order checklist is a useful example: each checklist item belongs to exactly one work order, may need stable identity within that owner, and can be stored in its own table without becoming an independently managed aggregate. EF Core models this with owned entity types.

01

Explain why an owned type is still an entity type with identity even when the CLR type has no explicit key property.

02

Configure OwnsOne and OwnsMany, including implicit/shadow keys and composite ownership keys.

03

Compare table splitting with separate-table ownership and inspect the resulting migration/foreign keys.

04

Show nested ownership and automatic inclusion with the owner.

05

Contrast owned tracking/reference semantics with complex value semantics.

06

Recognize cases where an owned type is being stretched beyond its lifecycle boundary and should become a normal entity or complex type instead.

1. Ownership says “this entity cannot live outside this owner”

OwnsOne and OwnsMany are not shorthand for “put some properties in a class.” EF creates entity metadata for the owned type. That metadata participates in state management, ownership relationships, keys, migrations, cascade behavior, and materialization.

Domain rule

If ServiceHub must reference, transfer, secure, version, or query the object independently of a work order, ownership may be the wrong boundary. A normal entity with its own key can be clearer.

2. OwnsOne: lifecycle-bound dispatch metadata

csharp · owned reference
public sealed class DispatchMetadata{    public string Channel { get; private set; } = "manual";    public DateTime? DispatchedUtc { get; private set; }    public string? ExternalDispatchId { get; private set; }}public sealed partial class WorkOrder{    public DispatchMetadata Dispatch { get; private set; } = new();}
csharp · OwnsOne table-split mapping
builder.OwnsOne(x => x.Dispatch, owned =>{    owned.Property(x => x.Channel)        .HasColumnName("dispatch_channel")        .HasMaxLength(30);    owned.Property(x => x.DispatchedUtc)        .HasColumnName("dispatched_utc");    owned.Property(x => x.ExternalDispatchId)        .HasColumnName("external_dispatch_id")        .HasMaxLength(100);});

By convention, an OwnsOne owned entity shares the owner's table and receives a shadow primary key whose value follows the owner key. It still has entity identity in EF metadata even though that identity is hidden from the CLR type.

3. Inspect the hidden owned identity

csharp · owned metadata evidence
var orderType = db.Model.FindEntityType(typeof(WorkOrder))!;var ownership = orderType.FindNavigation(nameof(WorkOrder.Dispatch))!.ForeignKey;var ownedType = ownership.DeclaringEntityType;Console.WriteLine($"Owned type: {ownedType.Name}");Console.WriteLine($"Is ownership: {ownership.IsOwnership}");Console.WriteLine($"Primary key: {string.Join(", ", ownedType.FindPrimaryKey()!.Properties.Select(p => p.Name))}");

This is the mechanism that complex types deliberately avoid. If value semantics are what the domain needs, complex types are generally the cleaner model in EF Core 10.

4. OwnsMany needs an identity per child

ServiceHub checklist items need ordering and stable identity inside the work order. The owner's key alone cannot distinguish several items, so configure an additional key component.

csharp · owned checklist collection
public sealed class ChecklistItem{    public int Id { get; private set; }    public string Text { get; private set; } = string.Empty;    public int DisplayOrder { get; private set; }    public bool IsCompleted { get; private set; }}public sealed partial class WorkOrder{    private readonly List<ChecklistItem> _checklist = new();    public IReadOnlyCollection<ChecklistItem> Checklist => _checklist;}
csharp · OwnsMany separate-table mapping
builder.OwnsMany(x => x.Checklist, owned =>{    owned.ToTable("work_order_checklist");    owned.WithOwner().HasForeignKey("work_order_id");    owned.Property<int>("id");    owned.HasKey("work_order_id", "id");    owned.Property(x => x.Text)        .HasColumnName("text")        .HasMaxLength(300);    owned.Property(x => x.DisplayOrder)        .HasColumnName("display_order");    owned.Property(x => x.IsCompleted)        .HasColumnName("is_completed");});

The composite key makes the child identity immutable within its owner. If items must move between work orders while keeping identity, model them as normal entities instead.

5. Migration evidence distinguishes table splitting from a child table

sql · conceptual SQLite ownership DDL
-- OwnsOne Dispatch is table-split into work_orders.ALTER TABLE work_orders ADD COLUMN dispatch_channel TEXT NOT NULL DEFAULT 'manual';-- OwnsMany Checklist gets its own lifecycle-bound table.CREATE TABLE work_order_checklist (    work_order_id INTEGER NOT NULL,    id INTEGER NOT NULL,    text TEXT NOT NULL,    display_order INTEGER NOT NULL,    is_completed INTEGER NOT NULL,    PRIMARY KEY (work_order_id, id),    FOREIGN KEY (work_order_id)        REFERENCES work_orders(work_order_id)        ON DELETE CASCADE);

This is conceptual DDL. Use the generated migration as authoritative for exact quoting/defaults and SQLite rebuild behavior.

6. Owned values are loaded with the owner

Owned entity types are part of the owner's aggregate shape and are normally included when querying the owner; you do not write Include just to retrieve an owned value. Separate-table storage changes SQL joins, not ownership semantics.

csharp · query owner and owned data
var order = await db.WorkOrders    .SingleAsync(x => x.Id == id, ct);Console.WriteLine(order.Dispatch.Channel);foreach (var item in order.Checklist.OrderBy(x => x.DisplayOrder))    Console.WriteLine($"{item.DisplayOrder}: {item.Text}");

7. Deliberately wrong approach: share one owned instance between owners

Owned entity types use reference/identity semantics. Reusing one owned instance as if it were a freely copyable value can produce tracking/ownership conflicts because EF cannot treat one owned entity instance as simultaneously owned by different owners.

csharp · wrong shared-owned-instance idea
var shared = new DispatchMetadata();orderA.ReplaceDispatchForTest(shared);orderB.ReplaceDispatchForTest(shared);// This is not value-object sharing. The owned entity has ownership identity.await db.SaveChangesAsync(ct);

The safe repair depends on meaning: create a distinct owned instance for each owner, or use a complex type if the object is truly a value whose properties should copy structurally.

8. Nested ownership and separate tables are available—but complexity is a signal

An owned type can itself own another type, and an OwnsOne can map to a separate table with ToTable. Those capabilities are useful for legacy schemas and aggregate boundaries, but deeply nested ownership can produce hard-to-review migrations and queries. If the nested object is value-like, complex types may remove hidden identity. If it needs independent lifecycle, a normal entity may be clearer.

9. Hands-on lab: compare complex and owned models

  1. Keep Chapter 06 Lesson 1's ServiceAddress as a complex type.
  2. Add DispatchMetadata with OwnsOne and ChecklistItem with OwnsMany.
  3. Generate/review the migration and identify which columns stay on work_orders and which table/PK/FK is added.
  4. Inspect IModel and print the hidden owned primary key/ownership FK.
  5. Query an order and verify owned data materializes without Include.
  6. Modify one checklist item and inspect ChangeTracker entries; compare with a change inside the complex ServiceAddress.
  7. Run the shared-owned-instance broken example in the disposable database/context and capture the tracking/model failure.
  8. Reset after the experiment.

Verification checklist

  • ServiceAddress remains a complex type with no entity key.
  • DispatchMetadata is owned and carries hidden identity.
  • ChecklistItem has an owner-scoped composite key.
  • Ownership delete behavior is reviewed as lifecycle behavior, not blindly accepted.
  • No independently-lived business object is hidden inside ownership.

Check your understanding

  1. Why does OwnsOne have a key even when the CLR type defines none?
  2. Why is the owner key insufficient for an OwnsMany collection?
  3. Can an owned type be stored in a separate table?
  4. Do owned types and complex types have the same identity semantics?
  5. Why might moving an owned child between owners be a modeling smell?
  6. When is a normal entity preferable to ownership?
Review the answers

Owned types are entity types; EF creates hidden identity, commonly using the owner key for OwnsOne.

Several children need distinct identity inside one owner, so an additional key component is required.

Yes. ToTable can store owned types separately while retaining ownership semantics.

No. Owned types have entity/reference identity; complex types have no independent identity and model values.

Owner identity is commonly part of the child key/lifecycle; transfer semantics may mean the child should have its own entity identity.

When the object needs independent lifecycle, references, permissions, queries, transfer, or identity outside the owner.

10. Production judgment and bridge

Owned types remain useful, especially for aggregate-bound child entities or legacy schemas, but EF Core 10 complex types are generally the better fit for true value objects. Review table width, child-table indexes, cascades, and migration churn. The next lesson moves structural data into JSON columns and makes provider-specific query/update translation observable.

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