
Domain-Driven Design With Entity Framework Core 8
Domain-Driven Design (DDD) is a collection of principles and patterns that helps developers build software by modelling business domains. In other words, it's all about understanding your business and transforming it into the code. Writing code is, dare I say it, the easiest part of DDD. Yet, storing your sophisticated domain models in a database can sometimes get tricky. In this article, I will show you how to use various DDD patterns together with Entity Framework (EF) Core 8. I will be using Microsoft SQL Server as a database. However, the code examples (with minor adjustments) should also work with other relational databases such as MySQL or PostgreSQL.
Creating Entities
Entities in Domain-Driven Design are domain objects defined by their identities and lifecycles. In simpler terms, they represent unique domain objects that can change over time. Let's define two of them—a Product and a Seller that sells products:
Notice the empty private constructor in the Product entity. EF can use either a constructor with properties matching arguments (i.e. Seller) or an empty constructor and then resolve all entity properties using .NET reflection. It doesn't matter whether constructors are public, private, or internal. That means we can create many different constructors with all kinds of arguments. I usually go with an empty private constructor for EF and a public one for the rest of the application. It might not be super clean, as I'm leaking infrastructure concerns into my domain model. However, it's a pragmatic approach as it reduces boilerplate code while not impacting the domain model in a significant way.
Let's add Microsoft.EntityFrameworkCore.SqlServer library and define EF database context together with our entity configurations:
After generating migrations and updating our database, we should now be able to work with Product and Seller entities:
Easy, right? Not so fast! Let's go one step further and introduce value objects.
Mapping Value Objects
Value objects in Domain-Driven Design are immutable stateless domain objects defined only by their property values. Let's define a couple of them:
Since we validate data when creating value objects, we can be sure that our database will have valid data after value objects are persisted. There's no need to repeat validations when restoring persisted value objects. It can even be problematic if we do so, especially after introducing new validation rules that have yet to be applied to the whole database. To mitigate this, we could introduce a static factory method:
We could then use .NET reflection to configure EF to use private value object constructors when restoring persisted value objects. However, to ensure the code examples are not overly complicated, we'll stick with one public constructor for the rest of this article.
We should also override equality operators, as two or more value objects are equal if they have the same property values (entities, on the other hand, are equal if they have identical IDs). We could use either the standard .NET method or one of my go-to open-source libraries, CSharpFunctionalExtensions. This library provides base classes for value objects and entities, significantly reducing boilerplate code.
After implementing the changes mentioned above, our value objects should look like this:
Let's also change our entities accordingly:
Let's update our database context:
Alternatively, for value objects, we could use complex properties introduced in EF Core 8:
In theory, it's better to use EF complex properties—configuring value objects as owned entities will prevent us from reusing those value objects in different entities. In practice, however, EF complex properties still have many limitations, one of which is not supporting null values. I guess most of the missing features will be implemented in EF Core 9, as many limitations were not deliberate design decisions.

Storing Value Object Collections
Let's extend our domain model further by adding a collection of SocialMediaLink value objects to the Seller entity:
Before EF Core 8, we could use custom EF value converters serializing value object collections to a text value (i.e. JSON) or map them to separate database tables. Fortunately, EF Core 8 supports mapping value object collections to JSON without having custom value converters. Moreover, it allows data to be filtered by serialized value object properties. Let's update our database context:
Introducing Aggregates
An aggregate in Domain-Driven Design is a cluster of entities treated as a single unit. One of those entities is called an aggregate root. An aggregate root ensures the integrity of the whole aggregate, as all of the modifications to the aggregate entities should only go through the aggregate root. Let's create a Store entity and add it to the Seller aggregate. Entity Seller will become an aggregate root:
We could use EF OwnsMany configuration, which would automatically load Stores when querying the Seller:
Notice the HourConverter—instead of repeating the same conversion expression twice, we could define a separate reusable value converter:
As an alternative to OwnsMany, we could use EF Core HasMany configuration. The downside of this approach is that we would need to include stores explicitly when querying sellers. However, this approach is more flexible, as we can query stores individually without querying sellers or query sellers without querying stores. This flexibility is valuable when working with large aggregates that take significant time to query. I usually prefer this approach, especially when using the repository pattern, as I can put all of the extra included statements inside of the repository:
To map one-to-one relations instead of one-to-many (i.e. Seller with only one Store), you could use almost identical HasOne and OwnsOne EF configurations.
Summary
Domain-driven design patterns and Entity Framework Core 8 are a very powerful combination. It only takes a couple of lines of code to map entities, value objects, and even aggregate roots to database tables. What is more, it gets better and better every release. I can only wonder what Entity Framework Core 9 will bring!
Thank you for reading. Is there something else that this post needs to include? I'd love to hear about it in the comments.