Complex Types in EF Core 8
EF Core 8 introduces a new feature called complex types - which are a way to model value objects in EF Core. Value objects are objects that represent a value, but do not have an identity. For example, an address, a phone number, or a money amount are all value objects.
Complex types are different from entity types in a few ways. First, complex types are not tracked by EF Core. This means that EF Core will not keep track of changes to complex type properties. Second, complex type properties cannot be used as foreign keys in relationships.
Complex types are useful for modeling value objects in EF Core. By modeling value objects as complex types, you can ensure that your data is consistent and that your code is more maintainable. As you read on you may ask yourself: how are complex types different than owned types. The difference really comes down to more flexible modeling with complex types. Or to be frank: complex types allow us to be consistent with reality. An example we dig into later is that of an order having the same billing address and shipping address.
For this post I have created an example you may use as you follow along: efcore-complextypes. As per my usual, the repository is choc full of other goodies, such as running migrations (once) as part of service-level testing, Docker and Compose patterns, Makefile patterns, and a nifty attribute called ServiceFact that excludes service tests when NOT running in a container. Feel free to use any of it.
Complex Type Definition
Here is an example of a complex type for an address:
This complex type can be used to represent the address of a customer, an order, or any other object that needs to have an address.
To use a complex type in EF Core, you need to define it as a property on an entity type. For example, Customer entity type has an Address
property:
Once you have defined a complex type, you can use it in your code like any other type. For example, the following code creates a new customer object with an address:
What if you want to use the same instance of a value type, across multiple entities? For example, use the same address in a customer as well as an order. With owned types we would get compile warnings trying to do that, and the following runtime error:
Owned entities can only be saved as part of an aggregate also including the owner entity.
With complex types we can do that very naturally by simply setting entity properties to the same instance. In the example below, notice that customer.Address
, order.BillingAddress
, and order.ShippingAddress
are all set to the same instance of Address
. Also notice that none of these need to be explicitly loaded since complex types are loaded with their entities - since they are NOT navigation properties (not entities).
Complex Type Configuration
Complex types MUST be explicitly configured in your model. Currently there is no discovery convention for complex types. The cleanest and most flexible way to do this is to specify the ComplexProperty
on the entity in your OnModelCreating.
You can also use the ComplexTypeAttribute
to annotate the value object as a complex type, but in general I recommend using OnModelCreating
to keep model-related configuration in one place. (I plan on writing a post in the near future about a pattern I use to keep OnModelCreating
neat and tidy.)
Complex Type Details
All Complex Type patterns and behavior root in the fact that they are implemented as columns on their containing entity. In other words, what you can do with non-navigation properties is what you can do with complex types.
Complex Type Projections
As non-navigation properties, you can use complex types in projections.
Complex Type Predicates
Since a complex type's properties are columns on their parent entity, you can use their properties in predicates.
That's cool, but what's even cooler is that you can use the complex type itself in predicates - and EF Core will expand that into value-based SQL predicates.
Complex Types More
I highly recommend getting yourself access to EF Core 8. Since it is still in release candidate, I recommend using it somewhere other than your main development machine. The two options I suggest are:
- Use it via .NET Fiddle
- Use it in a VM
This latter approach allows you to be as reckless as you want - because you can always restore a checkpoint. In fact, this is an approach I frequently use. You want to be very protective of your main development setup. It's how you make a living, so treat it with care.
Conclusion
Complex types are a powerful new feature in EF Core 8 that can help you to model your data more accurately and to write more maintainable code.
Here are some of the benefits of using complex types in EF Core 8:
Consistency
Complex types can help you to ensure that your data is consistent. For example, if you have a complex type for an address, you can ensure that all addresses in your database are stored in a consistent format.
Maintainability
Complex types can make your code more maintainable. For example, if you have a complex type for an address, you can avoid having to repeat the same code for validating addresses throughout your codebase.
Performance
Complex types can improve the performance of your application. For example, if you have a complex type for an address, EF Core can generate more efficient queries for filtering and sorting addresses.
Overall, complex types are a valuable new feature in EF Core 8 that can help you to write better code.
Code well, and shape the future.
Further Reading
- efcore-complextypes
- Domain Driven Design, Evans
- Announcing .NET 8 RC1, .NET Blog