# Owned Types in EF Core for DDD Value Objects

> Value objects don't have identity. EF Core owned types let you persist them as part of the parent entity - no separate table, no separate ID.

Published: 2026-08-25. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/owned-types-ef-core-ddd

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**](https://milanjovanovic.tech/blog/value-objects-in-dotnet-ddd-fundamentals) 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**](https://milanjovanovic.tech/blog/complex-types-ef-core) - similar but with stricter restrictions (EF Core 8+)
3. [**Value conversions**](https://milanjovanovic.tech/blog/value-conversions-ef-core) - 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](https://milanjovanovic.tech/blogs/articles/owned-types-ef-core-ddd/owned-types-storage.png)

## Basic Owned Type

Define the value object:

```csharp
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:

```csharp
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:

```sql
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:

```csharp
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:

```csharp
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:

```csharp
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:

```csharp
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:

```sql
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`:

```csharp
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; }
}
```

```csharp
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:

```csharp
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:

```csharp
// 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.
