Modern applications need richer domain models than primitives like string and decimal.
We deal with addresses, money, date ranges and similar concepts every day. These values have meaning, structure and rules. Treating them as primitives leads to weaker models and more bugs over time.
This is where complex types in Entity Framework Core shine, especially in applications following DDD (Domain-Driven Design).
With EF Core 10, complex types take a major step forward and become far more practical for real-world systems.
Complex Types
If you’re new to the topic, complex types are essentially EF Core’s first-class support for value objects.
Value objects are a core DDD concept:
- They are not entities
- They don’t have identity
- Equality is based on values
- They represent a single concept, such as Money or Address
They are foundational for expressive domain models, however, for a long time EF Core didn’t support them cleanly.
For years, we used owned types to model value objects. While they solved mapping, they introduced trade-offs because owned types were still entity types.
Basically owned types solved mapping but not modeling.
With EF Core 8, complex types were introduced as true value objects.
Here’s a simple complex type example:
public class Order
{
public Guid Id { get; set; }
public Address ShippingAddress { get; set; }
}
public record Address(string Street, string City, string Country);
To configure it you can use ComplexProperty method:
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable(TableNames.Orders);
builder.HasKey(order => order.Id);
builder.ComplexProperty(i => i.ShippingAddress, address =>
{
address.Property(a => a.Street).HasMaxLength(200).IsRequired();
address.Property(a => a.City).HasMaxLength(100).IsRequired();
address.Property(a => a.Country).HasMaxLength(100).IsRequired();
});
}
}
If you prefer attributes, this also works:
[ComplexType]
public record Address(string Street, string City, string Country);
However, using complex types had a few limitations as well:
- No optional complex types
- No struct support
- Limited JSON support
- No collections
EF Core 10 addresses most of these gaps.
Optional Complex Types
In EF Core 10, complex types can be optional.
The entire complex object can be null. EF Core determines presence based on column values.
For example, you may want to have billing address optional if it matches shipping address:
public class Order
{
public Guid Id { get; set; }
public Address? BillingAddress { get; set; }
public Address ShippingAddress { get; set; }
public Money TotalAmount { get; set; }
}
This enables realistic domain models without workarounds.
Struct Support
EF Core 10 also adds struct and record struct support for complex types.
This is ideal for small, immutable value objects like Money:
public class Order
{
public Guid Id { get; set; }
public Address? BillingAddress { get; set; }
public Address ShippingAddress { get; set; }
public Money TotalAmount { get; set; }
}
public struct Money
{
public required Currency Currency { get; set; }
public required decimal Amount { get; set; }
}
This could reduce allocations, bring better performance and using records is a big plus for me since I prefer avoiding mutation of value objects.
JSON Mapping
Probably the most interesting update lies in new EF Core JSON mapping capabilities that are a great application for complex types.
Configuration is straightforward:
builder.ComplexProperty(i => i.BillingAddress, address => address.ToJson());
What makes this especially useful in EF Core 10 is that JSON columns are now fully queryable:
var ordersInParis = dbContext.Orders
.Where(o => o.ShippingAddress.City == "Paris")
.Select(o => new { o.Id, o.ShippingAddress.City })
.ToListAsync(cancellationToken);
This is incredibly useful when:
- Structures are deeply nested
- Schema flexibility matters more than strict normalization
If you want to dive deeper, check out my dedicated post on EF Core 10 JSON mapping: JSON Type Support in EF Core 10
Breaking Changes
EF Core 10 introduces an important breaking change for complex types.
Previously, properties with the same name across different complex types could silently map to the same column.
Now column names are automatically uniquified, EF Core uses the full property path and suffixes are added when collisions occur.
This improves clarity, avoids bugs and makes large schemas much easier to reason about.
Conclusion
Complex types in EF Core 10 are no longer a niche feature.
They are now safer, more expressive and finally compatible with proper value object modeling.
But keep in mind, there are still limitations such as collections of complex types.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!
