Owned Entities vs Value Objects: Modelling DDD Concepts in EF Core
In domain-driven design, value objects are types defined by their attributes rather than an identity. An Address or Money type doesn't need its own primary key — it's meaningful only as part of an entity. EF Core's owned entity types provide the mechanism to persist these concepts without giving them their own table or identity.
Defining a Value Object
Start with a clean value object in your domain:
public class Address
{
public string Street { get; init; } = string.Empty;
public string City { get; init; } = string.Empty;
public string PostCode { get; init; } = string.Empty;
public string Country { get; init; } = string.Empty;
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public Address ShippingAddress { get; set; } = null!;
public Address BillingAddress { get; set; } = null!;
}
Configuring Owned Types
Mark Address as owned in OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Customer>(builder =>
{
builder.OwnsOne(c => c.ShippingAddress, address =>
{
address.Property(a => a.Street).HasColumnName("Shipping_Street");
address.Property(a => a.City).HasColumnName("Shipping_City");
address.Property(a => a.PostCode).HasColumnName("Shipping_PostCode");
address.Property(a => a.Country).HasColumnName("Shipping_Country");
});
builder.OwnsOne(c => c.BillingAddress, address =>
{
address.Property(a => a.Street).HasColumnName("Billing_Street");
address.Property(a => a.City).HasColumnName("Billing_City");
address.Property(a => a.PostCode).HasColumnName("Billing_PostCode");
address.Property(a => a.Country).HasColumnName("Billing_Country");
});
});
}
This stores both addresses directly in the Customers table as additional columns. No separate Addresses table is created.
The Resulting Table Structure
The Customers table now looks like:
| Id | Name | Shipping_Street | Shipping_City | Shipping_PostCode | Shipping_Country | Billing_Street | ... |
|---|
This is table splitting — multiple types mapped to the same table.
Owned Type with Its Own Table
If you prefer a separate table, use ToTable:
builder.OwnsOne(c => c.ShippingAddress, address =>
{
address.ToTable("ShippingAddresses");
});
The owned type still has no independent identity — it's always loaded and saved with its owner.
Collections of Owned Types
You can also have collections of value objects. Consider an order with multiple line items modelled as owned types:
public class Order
{
public int Id { get; set; }
public List<LineItem> LineItems { get; set; } = [];
}
public class LineItem
{
public string ProductName { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
modelBuilder.Entity<Order>()
.OwnsMany(o => o.LineItems, item =>
{
item.ToTable("OrderLineItems");
item.WithOwner().HasForeignKey("OrderId");
item.Property<int>("Id");
item.HasKey("Id");
});
Collections of owned types always require their own table.
Value Object Equality
True DDD value objects should use structural equality. While EF Core doesn't enforce this, you should implement it on your types:
public class Money : IEquatable<Money>
{
public decimal Amount { get; init; }
public string Currency { get; init; } = "GBP";
public bool Equals(Money? other)
=> other is not null && Amount == other.Amount && Currency == other.Currency;
public override bool Equals(object? obj) => Equals(obj as Money);
public override int GetHashCode() => HashCode.Combine(Amount, Currency);
}
Configure it as owned:
modelBuilder.Entity<Product>()
.OwnsOne(p => p.Price, money =>
{
money.Property(m => m.Amount).HasColumnName("Price");
money.Property(m => m.Currency).HasColumnName("PriceCurrency");
});
Complex Types (EF Core 8+)
EF Core 8 introduced complex types, which are a closer match to DDD value objects. Unlike owned types, complex types cannot be null and always exist as part of their owner:
modelBuilder.Entity<Customer>()
.ComplexProperty(c => c.ShippingAddress);
Complex types are always stored in the owner's table (no separate table option) and cannot contain navigations to other entities. They're simpler and more constrained than owned types — which is often exactly what you want for value objects.
Owned Types vs Complex Types
| Feature | Owned Types | Complex Types (EF Core 8+) |
|---|---|---|
| Nullable | Yes | No |
| Separate table | Optional | No |
| Navigation properties | Yes | No |
| Collections | Yes | No |
| Identity resolution | Has hidden key | No key |
When to Use Which
- Complex types for simple value objects that are always present (addresses, money, coordinates)
- Owned types for nullable value objects, value objects that need their own table, or collections of value objects
- Regular entities when the type genuinely has its own identity and lifecycle
Owned entities and complex types let you model rich domain concepts without polluting your database design with unnecessary tables and foreign keys. Choose the option that best matches your domain's constraints.