Owned Types in EF Core for DDD Value Objects

Owned Types in EF Core for DDD Value Objects

6 min read··

ddddotnetef-core

Owned types are entity types that belong to a parent entity and carry no identity of their own in your domain model. Configure one with OwnsOne and EF Core stores its properties as extra columns in the parent table, while OwnsMany stores a collection of them in a separate table. That is how a DDD value object like Money or Address persists without an artificial ID.

A value object belongs to an aggregate because of what it represents, not because it has its own database identity. Flattening it into the entity weakens the domain model, while giving it an artificial ID changes its semantics.

Why Value Objects Need Special Mapping

Value objects are a core DDD building block. They have no identity - two Money objects with the same amount and currency are equal. But EF Core needs to store them in a relational database.

You have three options:

  1. Owned types - stored as columns in the parent table (EF Core 2.0+)
  2. Complex types - similar but with stricter restrictions (EF Core 8+)
  3. Value conversions - single-property value objects stored as one column

Owned types are the most flexible approach.

An Order aggregate owning a Money value object stored as columns in the Orders table via OwnsOne, and a LineItems collection stored in a separate OrderLineItems table via OwnsMany

Basic Owned Type

Define the value object:

public sealed record Money
{
    public decimal Amount { get; init; }
    public string Currency { get; init; }

    private Money() { }

    public Money(decimal amount, string currency)
    {
        if (amount < 0)
            throw new ArgumentException("Amount cannot be negative.", nameof(amount));
        if (string.IsNullOrWhiteSpace(currency))
            throw new ArgumentException("Currency is required.", nameof(currency));

        Amount = amount;
        Currency = currency;
    }

    public static Money Zero(string currency) => new(0, currency);
}

Configure as owned:

public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.HasKey(x => x.Id);

        builder.OwnsOne(x => x.TotalAmount, money =>
        {
            money.Property(m => m.Amount)
                .HasColumnName("TotalAmount")
                .HasPrecision(18, 2);

            money.Property(m => m.Currency)
                .HasColumnName("TotalCurrency")
                .HasMaxLength(3);
        });
    }
}

This stores Money as two columns in the Orders table:

CREATE TABLE "Orders" (
    "Id" uuid NOT NULL,
    "TotalAmount" numeric(18,2) NOT NULL,
    "TotalCurrency" varchar(3) NOT NULL
);

No separate table. No separate ID. The value object lives inside its parent.

Address Value Object

A common example with multiple properties:

public sealed record Address
{
    public string Street { get; init; }
    public string City { get; init; }
    public string State { get; init; }
    public string ZipCode { get; init; }
    public string Country { get; init; }

    private Address() { }

    public Address(
        string street, string city, string state,
        string zipCode, string country)
    {
        Street = street;
        City = city;
        State = state;
        ZipCode = zipCode;
        Country = country;
    }
}

Configure:

builder.OwnsOne(x => x.ShippingAddress, address =>
{
    address.Property(a => a.Street)
        .HasColumnName("ShippingStreet")
        .HasMaxLength(200);

    address.Property(a => a.City)
        .HasColumnName("ShippingCity")
        .HasMaxLength(100);

    address.Property(a => a.State)
        .HasColumnName("ShippingState")
        .HasMaxLength(50);

    address.Property(a => a.ZipCode)
        .HasColumnName("ShippingZipCode")
        .HasMaxLength(20);

    address.Property(a => a.Country)
        .HasColumnName("ShippingCountry")
        .HasMaxLength(100);
});

Multiple Owned Types of the Same Type

An order might have both a shipping and billing address:

public class Order
{
    public Guid Id { get; private set; }
    public Address ShippingAddress { get; private set; }
    public Address BillingAddress { get; private set; }
}

Configure each one separately:

builder.OwnsOne(x => x.ShippingAddress, address =>
{
    address.Property(a => a.Street).HasColumnName("ShippingStreet");
    address.Property(a => a.City).HasColumnName("ShippingCity");
    // ...
});

builder.OwnsOne(x => x.BillingAddress, address =>
{
    address.Property(a => a.Street).HasColumnName("BillingStreet");
    address.Property(a => a.City).HasColumnName("BillingCity");
    // ...
});

Result:

CREATE TABLE "Orders" (
    "Id" uuid NOT NULL,
    "ShippingStreet" varchar(200),
    "ShippingCity" varchar(100),
    "BillingStreet" varchar(200),
    "BillingCity" varchar(100),
    -- ...
);

Owned Collections (Separate Table)

For collections of value objects, use OwnsMany:

public class Order
{
    public Guid Id { get; private set; }
    private readonly List<LineItem> _lineItems = [];
    public IReadOnlyCollection<LineItem> LineItems => _lineItems;
}

public sealed record LineItem
{
    public Guid ProductId { get; init; }
    public int Quantity { get; init; }
    public Money UnitPrice { get; init; }
}
builder.OwnsMany(x => x.LineItems, lineItem =>
{
    lineItem.ToTable("OrderLineItems");

    lineItem.WithOwner().HasForeignKey("OrderId");

    lineItem.Property(li => li.ProductId);
    lineItem.Property(li => li.Quantity);

    lineItem.OwnsOne(li => li.UnitPrice, money =>
    {
        money.Property(m => m.Amount)
            .HasColumnName("UnitPrice")
            .HasPrecision(18, 2);
        money.Property(m => m.Currency)
            .HasColumnName("Currency")
            .HasMaxLength(3);
    });
});

OwnsMany creates a separate table because you can't flatten a collection into columns.

Nullable Owned Types

With nullable reference types enabled, a non-nullable owned navigation is required by default (EF Core 6+). To make a value object optional, declare the property as nullable (Address?) or configure the navigation explicitly:

builder.OwnsOne(x => x.ShippingAddress, address =>
{
    address.Property(a => a.Street).HasColumnName("ShippingStreet");
    // ...
});

builder.Navigation(x => x.ShippingAddress).IsRequired(false);

When ShippingAddress is null, all its columns will be null in the database.

Querying Owned Types

Access owned types in LINQ queries:

// Filter by owned type property
var expensiveOrders = await _db.Orders
    .Where(o => o.TotalAmount.Amount > 1000)
    .ToListAsync();

// Project owned type properties
var orderSummaries = await _db.Orders
    .Select(o => new
    {
        o.Id,
        Total = o.TotalAmount.Amount,
        Currency = o.TotalAmount.Currency,
        City = o.ShippingAddress.City
    })
    .ToListAsync();

EF Core translates these to SQL column access - no joins needed.

Owned Types vs Complex Types vs Value Conversions

How the three approaches compare (as of EF Core 8):

  • Multiple properties: owned types and complex types support them; value conversions handle a single property only
  • Nullability: owned types and value conversions can be optional; EF Core 8 complex types cannot (EF Core 10 lifted this)
  • Collections: only owned types support them, via OwnsMany
  • Nesting: owned types and complex types can nest other value objects; conversions can't
  • Table placement: owned types can move to a separate table; complex types always share the parent table
  • Hidden key: owned types carry a shadow key under the hood; complex types have none

Use value conversions for single-property value objects like Email or PhoneNumber. Use owned types for multi-property value objects like Money or Address. Use complex types when you want the same behavior without shadow keys.

I walk through a complete aggregate persisted this way in value objects with EF Core.

One Gotcha: Owned Types Are Still Entities

Under the hood, EF Core treats an owned type as an entity with a hidden shadow key tied to its owner. That leaks in a few places:

  • Two owned instances with identical values are not interchangeable to EF Core the way true value objects are - replacing one is an update to the owner's row
  • OwnsMany rows are keyed by the owner plus a synthetic ID, so reordering a collection can generate more SQL than you expect
  • Sharing the same value object instance between two owners throws, because an owned instance can only belong to one owner

None of these are blockers, but they explain the occasional surprising UPDATE statement in your logs.

Summary

Use OwnsOne or OwnsMany when a value cannot exist independently from its aggregate owner. Configure nullability, column names, and table placement explicitly so persistence details do not blur the value object's semantics. On EF Core 10, also consider complex types when you want value semantics without the hidden identity of an owned entity.

Frequently Asked Questions

What are owned types in EF Core?

Owned types are entity types that belong to a parent entity and have no identity of their own in your domain model. EF Core stores them as extra columns in the parent table (OwnsOne) or in a separate table for collections (OwnsMany).

How do I map a DDD value object with EF Core?

Use OwnsOne for multi-property value objects like Address or Money, value conversions for single-property ones like Email, or complex types on EF Core 8+. All three keep the value object free of a database identity.

Can owned types be null in EF Core?

Yes. With nullable reference types enabled, a non-nullable owned navigation is required by default, and declaring the property nullable or calling Navigation(x => x.Property).IsRequired(false) makes it optional. When the value object is null, all of its columns are null.

Do owned types create a separate table?

OwnsOne stores properties in the parent table by default, with an optional ToTable to split them out. OwnsMany always creates a separate table because a collection cannot be flattened into columns.

What is the difference between owned types and complex types?

Owned types are entities internally, with a hidden shadow key and support for collections and separate tables. Complex types (EF Core 8+) are true value types with no key, which makes them cleaner semantically but less flexible on older versions.

Loading comments...

Whenever you're ready, there are 4 ways I can help you:

  1. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  2. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  3. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  4. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

Become a Better .NET Software Engineer

Join 66,000+ engineers who are improving their skills every Saturday morning.