Skip to main content

Command Palette

Search for a command to run...

EF Core Owned Entities vs Complex Types for Value Objects

Updated
โ€ข11 min read
EF Core Owned Entities vs Complex Types for Value Objects

When you adopt Domain-Driven Design in a .NET codebase, one of the first decisions your team faces is how to map value objects to the database. For years, OwnsOne and OwnsMany were the only first-class options in EF Core. Then .NET 8 introduced ComplexProperty โ€” a cleaner, tighter mapping that changes the calculus for many teams.

Both approaches solve the same fundamental problem: persisting an object that has no identity of its own, only the values it holds. But they diverge sharply in semantics, performance characteristics, and DDD alignment. The wrong choice doesn't break anything immediately, but it accumulates friction in the form of unexpected JOIN queries, null-handling edge cases, and configuration complexity. If you want to see how these patterns integrate into a complete production codebase with full domain modelling, Chapter 3 of the Zero to Production course covers owned types, complex properties, and the broader entity design strategy inside a working API โ€” with everything wired together from day one.

ASP.NET Core Web API: Zero to Production

For developers who want the full domain model with annotated source code, edge cases included, the complete reference implementation is available on Patreon โ€” the kind you'd actually ship in an enterprise context.

What Are We Actually Comparing?

Before the trade-off analysis, it's worth grounding both options in a concrete framing.

Owned entities (OwnsOne / OwnsMany) have existed since EF Core 2.0. They allow you to embed one entity inside another, with EF Core treating the embedded object as part of the owner's lifecycle. Despite the word "entity", owned types lack a standalone identity โ€” they exist only in relation to their owner. They can be stored in the same table as the owner (default) or split into a separate table.

Complex types (ComplexProperty) arrived in EF Core 8. They represent a more precise mapping for true value objects: no key, no identity, no independent existence. A complex type is always mapped inline with its owner, always in the same table, and EF Core makes no assumptions about its navigability or identity. What you lose in flexibility you gain in correctness โ€” the mapping more faithfully reflects DDD semantics.

The question is: when does that precision matter enough to prefer one over the other?

The Core Semantic Difference: Identity

This is where the decision starts. Owned entities, despite being designed for value-object-like scenarios, are still entity types under the hood. EF Core assigns them shadow primary keys when they live in a separate table, and even in table-splitting configurations, the internal tracking mechanics treat them as entities with identity.

This matters most when:

  • You use OwnsMany to represent a collection โ€” EF Core needs a key to track individual items

  • You rely on FindAsync or change tracking, where EF's entity identity assumptions can surface unexpectedly

  • You nest owned types inside other owned types, which can trigger surprising SQL generation

Complex types carry no such baggage. There is no shadow key, no identity tracking at the individual value level, and no concept of "updating a specific instance" in the database. You modify the owning entity, and EF Core propagates the changes to the inlined columns. This is exactly how value objects are supposed to work: immutable, identity-free, owned entirely by their parent.

When to Use Owned Entities

Owned entities remain the right tool in several scenarios where complex types cannot reach.

Collections of embedded objects. Complex types cannot (currently) be used with collections. If you have a domain model where an order contains a list of MoneyAmount value objects or an address history, OwnsMany is still the only built-in option. EF Core stores these in a related table with a generated key, but ownership semantics are preserved at the application level.

Separate table storage. If architectural reasons require a value object to live in its own table โ€” for column-count management, regulatory data separation, or legacy schema compatibility โ€” owned entities with ToTable() provide that without breaking DDD intent.

Hierarchical nesting with polymorphism. Complex types do not support inheritance hierarchies. If your value object participates in a type hierarchy where different subclasses carry different fields, owned entities give you the mapping hooks (TPH, TPT) you need.

EF Core versions below 8. Any project still on .NET 6 or .NET 7 โ€” or EF Core 7 and earlier โ€” has no ComplexProperty option at all. Owned entities are the only path.

When to Use Complex Types

Complex types shine in the most common DDD scenarios, and the cleaner semantics are a genuine advantage.

Single value objects embedded in an entity. An Address, a Money type, a DateRange, a Coordinates struct โ€” anything that is a single, non-nullable value attached to an entity and mapped inline. ComplexProperty maps these directly to the parent table's columns with zero ceremony.

Better null semantics. With owned entities, EF Core can produce nullable shadow properties that leak into your queries. Complex types enforce that a value is always present: you can't have a "null Address" in the database when Address is a complex type โ€” the owning entity must always carry a value. This aligns with DDD's recommendation that value objects not be nullable; instead, use a null-object pattern or option type at the domain layer.

Reuse across multiple entities. Complex types can be shared across multiple entity types without the ownership constraint that ties an owned type to one specific owner hierarchy. If Money is used in Order, Invoice, and ProductPrice, complex type registration is cleaner.

No unintended tracking side effects. Because EF Core doesn't track complex type instances independently, you avoid the edge cases where change tracking behaves unexpectedly on owned types โ€” particularly in detached or disconnected scenarios common in web APIs.

Side-By-Side Comparison

Dimension Owned Entities (OwnsOne) Complex Types (ComplexProperty)
EF Core version required EF Core 2.0+ EF Core 8+
Has identity / primary key Shadow key (implicit) No key โ€” pure value semantics
Can store in separate table Yes (via ToTable) No โ€” always inline
Supports collections Yes (OwnsMany) No (EF Core 8/9/10 limitation)
Supports inheritance Yes (TPH/TPT) No
Null handling Nullable shadow properties (risky) Non-nullable by design
Change tracking behaviour Entity-level tracking Column-level diff only
DDD value object semantics Approximate Precise
Configuration complexity Moderate Low
Reuse across entity types Limited (ownership is exclusive) Unrestricted

Does This Decision Affect Query Performance?

In most cases, no โ€” both owned entities (same-table) and complex types map to the same set of columns in the parent table, so the generated SQL is identical for single-value scenarios. The difference shows up in two places.

First, with OwnsMany, EF Core generates a JOIN against the related table. Complex types have no equivalent join cost because collections aren't supported. If you're mapping a high-read-volume entity with multiple owned collections, that query overhead is real and measurable.

Second, change tracking for owned entities involves slightly more overhead because EF Core participates at the entity identity level. For write-heavy APIs with large entity graphs, this can accumulate. Complex types are tracked at the column-diff level โ€” leaner and more predictable.

What Does the Microsoft Guidance Say?

The EF Core documentation now recommends complex types as the preferred approach for mapping value objects that don't require collection semantics or separate table storage. The OwnsOne approach predates complex types and carries the conceptual baggage of the entity model. Microsoft's .NET architecture guide for microservices, updated to reflect EF Core 8+, aligns with this: use complex types for value objects where possible, and fall back to owned types when collection semantics or table separation are required.

Is There a "Which Should You Always Use" Answer?

Yes, with conditions.

Start with ComplexProperty if you are on EF Core 8+ and your value object is a single, non-nullable embedded value on an entity. This is the majority of value object scenarios in typical DDD codebases: Money, Address, DateRange, Coordinates, PhoneNumber, EmailAddress. The stricter semantics, simpler configuration, and absence of identity artifacts make it the better default.

Use OwnsOne / OwnsMany when you need table splitting, a collection of embedded values, or you are on an older EF Core version. These are specific, well-understood requirements โ€” not the default case.

Don't mix them for the same value object type without a clear reason. If Address is a complex type in Customer, it should be a complex type in Branch and Warehouse too. Inconsistency in how the same value object is mapped creates confusion during code reviews and migration authoring.

How to Decide in Your Existing Project

Step 1: Audit your owned type usages. For each OwnsOne / OwnsMany call in your DbContext, ask: does this value object need its own table, participate in a collection, or use inheritance? If the answer to all three is no โ€” and you're on EF Core 8 or later โ€” it's a migration candidate.

Step 2: Check nullable patterns. If your current owned type configuration produces nullable columns for value objects that are conceptually always present, complex types fix that at the schema level.

Step 3: Prioritise new entities. You don't need to migrate everything at once. For new entities added to the system, default to ComplexProperty for value objects. Existing owned types can migrate when the entity is next scheduled for significant change.

Step 4: Don't forget OwnsMany has no equivalent.** If you rely on collections of embedded objects, owned entities remain required. Evaluate whether those collections are genuinely value-object collections or whether they should be child entities with their own identity.

A Note on .NET 9 and .NET 10

EF Core 9 and EF Core 10 haven't changed the fundamental semantics of either feature, but they have continued to close gaps. ComplexProperty in EF Core 9 added support for optional complex types (nullable scenarios), and EF Core 10 extended JSON column support to complex types, which means you can now persist a value object as a JSON column while preserving complex-type semantics. These improvements make ComplexProperty an even stronger default for greenfield projects.

โ˜• Prefer a one-time tip? Buy us a coffee โ€” every bit helps keep the content coming!

FAQ

What is the main difference between EF Core owned entities and complex types? Owned entities are entity types under the hood โ€” they have shadow keys and EF Core tracks them at the identity level. Complex types have no key and no identity, making them a more accurate mapping for DDD value objects. Complex types are always stored inline in the owner's table; owned entities can be in a separate table.

Can I use complex types for collections of value objects in EF Core? No, not in EF Core 8, 9, or 10. Complex types currently only support single-value mappings. For collections of embedded value objects, OwnsMany remains the only built-in EF Core option.

When should I use OwnsMany over ComplexProperty? Use OwnsMany when you need a collection of embedded value objects (e.g., a list of line items without their own table), when you need the embedded objects stored in a separate table, or when you are on EF Core 7 or earlier.

Does switching from owned entities to complex types require a database migration? It depends. If the owned type is already stored in the same table as the owner (the default), the column structure may be identical โ€” but the EF Core model snapshot changes. You typically need a migration to update the snapshot metadata, even if no SQL change is required. Always run dotnet ef migrations add and review the generated migration before applying.

Are complex types available in .NET 6 or .NET 7? No. ComplexProperty was introduced in EF Core 8, which ships with .NET 8. If your project targets .NET 6 or .NET 7, owned entities are your only option for inline value object mapping.

Do complex types work with JSON columns in EF Core? Yes, from EF Core 10. You can map a complex type to a JSON column using .ToJson(), which gives you the clean DDD value-object semantics while persisting the data as a JSON document in a single column.

Is there a performance difference between owned entities and complex types? For single-value scenarios stored in the same table, SQL output is typically identical and performance is indistinguishable. The difference appears with OwnsMany (which generates JOIN queries) versus the absence of complex-type collection support. Change tracking is slightly leaner for complex types in large entity graphs.

Can I reuse the same complex type across multiple entity types? Yes. Unlike owned types, which are bound to their owner hierarchy, complex types can be registered on any number of entities without conflict. This makes them cleaner when a value object like Money or Address appears across multiple aggregate roots.

More from this blog

C

Coding Droplets

207 posts

Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.

From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.