On my article “Creating Domain-Driven Design entity classes with Entity Framework Core” @ardalis commented that “your entities all are tightly coupled to EF Core. That’s not good…”. Then I did a podcast with Bryan Hogan where we discussed Domain-Driven Design (DDD) and he goes further than my CRUD-only (Create, Read, Update, and Delete) approach – he says that the entity classes is the perfect place for business logic too.
NOTE: My discussion with Bryan Hogan is now out. You can find the PodCast here.
With such diverging views on the best way to implement DDD in Entity Framework Core (EF Core) I decided to write an article that a) compares normal approach with a DDD approach, and b) compare three different ways to implement DDD in EF Core. This is a detailed look at the issues, hence it is very long! But hopefully useful to those looking to use DDD, or developers that want to consider all the options available to them.
TL;DR; – summary
NOTE: DDD is a massive topic, with many facets. In this article I only look at the entity class issues, which is a tiny part of what DDD is about. I really recommend Eric Evan’s book Domain-Driven Design for a full coverage.
The DDD approach to writing entity classes in EF Core makes every property read-only. The only way to create or update entity data is constructors (ctors), factories or methods in the entity class. This article introduces the DDD entity style by comparing the standard, non-DDD approach, against a basic DDD-styled entity class. This gives us a clear starting point from which I can go on to compare and contrast three difference DDD approaches:
- A DDD-styled entity, but not including any references to EF Core commands.
- A DDD-styled entity which has access to EF Core’s DbContext and implements CRUD methods.
- A DDD-styled entity which has access to EF Core’s DbContext and contains all code that interacts with the entity, i.e. CRUD and more complex business logic.
Comparing a standard entity class with a DDD approach
Let’s start by comparing an implementing something using a standard entity class and a DDD-styled entity class. The aim of this section is to describe the major differences and sets the scene for a look at the subtler differences between the three DDD approaches.
The diagram below shows a standard entity class (i.e. a class that EF Core maps to the database) with read-write access.
And this diagram shows the same entity class but using a DDD approach.
The big difference is that the standard entity class can be created/changed by any external code, but in the DDD-styled entity class you can only create/change data via specific constructors/methods. But why is this a good idea? – here is a list of benefits:
- External code now has clearly named methods/static factories to call. This makes it much clearer to developers what the entity class supports in terms of creating/changing that class.
- The code to create/change the class is contained in the class itself, which keeps the code co-located with the data. This makes writing/refactoring of the class much simpler.
- It stops duplication of code, and in multi-person projects it stops different developers (or even the same developer!) applying different business rules to the same feature.
DDD is about making the domain (i.e. business) rules the focus of the code, so having methods like ChangeDeliveryDate or MarkOrderAsDispatched in your entity class encapsulates business rules with a meaningful name. There is much less possibility of getting things wrong this way.
Pros and Cons of Standard & DDD approaches
Here is summary of the Pros/Cons (advantages/disadvantages) of the two approaches
Approach | Pros | Cons |
Standard | Simple.
Minimum code.
|
Big systems can become quite hard to understand.
Possibly of duplicate code happening. |
DDD-styled | More obvious, i.e. meaningful named methods to call.
Better control of access to data. Puts the code next to the data. |
Slightly more code to write. |
Different views on building DDD-styled entity classes
Having introduced the DDD approach I now want to look at three different approaches to implementing DDD entity classes. Here is a diagram to show you the three DDD approaches, showing what code the entity classes contain. I use two terms that I need to define:
- Repository: A repository pattern provides a set of methods to access the database. These method ‘hide’ the code needed to implement the various database features you need.
- C(r)UD: This is Create, (Read), Update, and Delete – I use the term C(r)UD to separate the Read from the functions that change the database (Create, Update are Delete).
- Query Objects: This is a design pattern for building efficient database queries for EF. See this article from Jimmy Bogard on this topic.
- Business logic (shortened to Biz Logic): This is more complex processes that go beyond simple validation. They may have complex calculations (e.g. pricing engine) or require access to external systems (e.g. sending an email to a customer).
As you will see in the diagram below, the further to the right you go the more external code is moved inside the entity classes.
NOTE: I will use the terms POCO-only class, C(r)UD only and C(r)UD & Business Logic when referring to these three approaches.
1. The different ways of reading data
The first big difference is how the data is read from the database. When using a repository pattern, you tend to read and as well as write via the repository pattern. I personally have found (some) repository patterns can lead to poor performing code, mainly because it’s hard to build a fully featured repository with the correct adapters for the front-end. I therefore use Query Objects, normally defined in the services layer so that they can adapt the data to the needs of the front end.
I’m not going to cover this here as I have an article called “Is the repository pattern useful with Entity Framework Core?” which goes into it in detail, with this section on query objects.
2. The different ways of updating relationships
The big difference between the POCO-only class approach and the other DDD versions is that the POCO-only version doesn’t have any access to the DbContext, so it can’t load relationships. To show why this matter, let’s introduce a Book entity class a collection of Reviews, e.g. (5 stars – “It’s a great book” says Jill). Now we want to implement a method to allow a user to add a new Review entity class to an existing Book entity instance. The steps are:
- Get the new review information, with the Book’s primary key.
- Load the Book entity instance using the primary key.
- Call a method in the loaded Book instance to create a new Review and add it to the Book’s Reviews collection.
The problem comes if the Reviews collection hasn’t been loaded when the Book entity instance was loaded. If we assume the Reviews collection was loaded and its wasn’t, then adding a new review will either a) fail with a null collection or b) silently delete all the other reviews! Therefore, something has to make sure the collection is loaded. Here are some possible implementations for the POCO-only version, and the other two DDD versions:
1.a Handling adding a Review to POCO-only version
In a POCO-only entity class there is no references to EF Core. This means you need something else to handle the correct loading of the Book with its Review collection. The standard way to do this is via a repository pattern. The repository would contain an AddReview method that would load the Book entity instance, along with loading the Reviews collection before it calls the Book’s AddReview method to update the Books _reviews backing field. Here is some example code:
1.b Handling adding a Review within the DDD-styled entity class
The limitation of the POCO-only entity class is you need to rely on the caller to pre-load the Reviews collection. Because the POCO-only entity class can’t access any of EF Core’s classes or methods. In the other two types you can access EF Core, so you can move loading of the Reviews collection inside the entity class. Here is the code:
NOTE: When using the C(r)UD only approach the code in the ASP.NET controller has a repetitive pattern: load the entity and call a named method, which makes it a candidate for building library to handle this. This is exactly what I have done with the EfCore.GenericServices library, which can work with normal and DDD-styled entity classes. This the article “GenericServices: A library to provide CRUD front-end services from a EF Core database” for more on this.
Comparing the two approaches to adding a review
The differences are subtle, but important. In the POCO-only class the update relies on an external piece of code, in this case a repository pattern, to correctly update the reviews. In the C(r)UD only case the DDD access method, AddReview, handles the whole process.
Now, if you are using a repository pattern everywhere then this difference is small. However, it does allow a small loophole in which someone could bypass the repository and calls AddReview directly, which with the code I have written would cause a null reference exception (other designs I have seen silently delete existing reviews if you don’t preload the collection).
In this case the C(r)UD only approach is better because it encapsulates the whole of the process so that calling methods don’t have to know anything about the process. Therefore, the C(r)UD only (and the C(r)UD & Business Logic) approach follows the Single Responsibility Principle, i.e. the AddReview is responsible for adding a new review to a Book, and entirely encapsulates the code to add that review.
However, the C(r)UD only approach has one big down side – it refers to EF Core. Why is this a problem? The entity classes, like Book and Review, are core parts of your domain design and Eric Evans says in chapter 6 of his book that repositories should be used to “decouple application and domain design from persistence technology”. There are many people who like having entity classes that contain no database access logic so that they the class is focused on domain (i.e. business) logic.
You have to remember that the repository pattern was the recommended approach when Eric Evan’s book was published in 2004. Personally I, and others, don’t think the repository pattern appropriate for EF Core (see my article “Is the repository pattern useful with Entity Framework Core?”).
In the next section we look more carefully at the business logic and how that is handled, but before I leave this comparison, I should talk about performance. In the two examples the POCO-only class approach is quicker, as it loads the reviews at the same time as the book, while the C(r)UD only approach needs two trips to the database. I used similar code two examples to make it easier for you to compare the two approaches, but in my actual code (shown below) I use a much more efficient approach if the _reviews collection is not loaded/initialised: It just creates a new review with the correct foreign key, which is much more efficient.
public void AddReview(int numStars, string comment, string voterName, DbContext context) { if (_reviews != null) { //Create a new Book: the _reviews HashTable is set to empty _reviews.Add(new Review(numStars, comment, voterName)); } else if (context.Entry(this).IsKeySet) { //Existing Book: create a new review directly context.Add(new Review(numStars, comment, voterName, BookId)); } else { throw new InvalidOperationException( "Could not add a new review."); } }
3. Different ways of handling business logic
The first example was a Create, i.e. adding a new Review (known as an aggregate entity in DDD) to the Book entity (known as the root entity in DDD). These CRUD operations might include some form of validation, such as making sure the numStars value in the Review is between 1 and five. But when the rules get more complicated than validating properties then we tend to call the code business logic.
When someone wants an application written there are rules on how things work. For instance, placing an order might require certain checks on stock, payment etc. and kick off other tasks such as processing the order and delivery. The steps in the example order are called business rules, or in DDD-speak, domain problems.
In Eric Evans book he says “When the domain is complex, this is a difficult task, calling for the concentrated effort of talented and skilled people”. In fact, most of Eric Evan’s book is about domain problems: how to describe them (ubiquitous language), how to group domain problem (bounded context), etc. But in this section, I’m just going to look at the three DDD approaches and how they handle business logic.
For this I am going to use the example of processing a customer’s order for books. To make it easier to understand I am only showing 5 stages (e.g. I left out payment processing, shipment, etc.):
- Load the Books ordered using the primary keys provided by the higher layers
- Get the address that the order should be sent to
- Create the Order
- Save the Order to the database
- Send an email to the user saying the Order was successfully
I am also going to show the layering of the assemblies, i.e. in what order the assemblies reference each other. This matters because in .NET you can’t have circular references to assemblies (that causes problems at compile time), which constraints how you put the assemblies together. In most cases the entity classes are the lowest assembly, because everything else needs to access these key classes. (NOTE: I also add a red oval on the assembly that references EF Core).
2.a. POCO-only class approach
The POCO-only class approach uses a repository for database accesses so when the business logic needs data it relies on the repository. Because you can’t have circular references to assemblies this means part of the business logic moves into the repository. Typically, this results in repository taking on a coordination of the building of the order (Command Pattern). The figure to the right shows the assembly references.
And below is a diagram of one possible implementation of handling a customer order:
2.b The C(r)UD only approach
Over the years I have developed a series of rules to help me in implement business logic. And one of them is “Business logic should think it’s working on in-memory data”. Having decided that generic repository pattern doesn’t work well with EF Core (see my article “Is the repository pattern useful with Entity Framework Core?” on why that is) I had to come up with another way to do this. My solution is to have two new assemblies, which are:
- BizLayer: This contains classes that are pure business logic, i.e. they don’t access EF Core directly, but rely on their own mini-repository for all database actions
- BizDBAccess: This is handles all database accesses for a single business logic class, i.e. it’s a dedicated repository for the business logic.
It may seem odd to say I don’t like the repository pattern, and then build a dedicated repository for each business logic. But it’s because writing generic repository is actually very difficult – for instance EF Core is a generic repository/unit of work pattern and think how much effort it’s taken to write that! But writing a dedicated repository for a specific piece of business logic is easy. (I agree with Neil Ford’s comment from his book Building evolutionary architectures – “The more reusable the code is, the less usable it is.”).
Now, I’m not going to show the code here, as I have a much longer article called “Architecture of Business Layer working with Entity Framework (Core and v6) – revisited” where I covers the same example of placing an Order (NOTE: this article doesn’t include a “SendMail” part, but I think you can see that goes in the business logic).
2.c. C(r)UD and business logic
The final version has methods/factories for ALL manipulation of the data in the entity class. This means any there is no repository and no business layer because it’s all handled by the entity class itself.
In this case the business logic code that was in the repository (see 2.a) is moved completely into the entity class. The technical issue with this is the SendMail method is in an assembly that is linked to the assembly containing the entity classes, which stops you referencing the SendMail directly. It’s pretty simple to fix this by defining an interface (e.g. ISendMail) in the entity class and using dependency injection to provide the SendMail instance at run time.
At this point every operation other that read (and maybe some deletes) will all be entity class, and those method will use EF Core to access the database directly from the entity itself.
Comparing approaches to business logic
In the end the differences are around the ‘scope’ of the code inside the entity class. The POCO-only has the simplest code, with no references to EF Core. The C(r)UD only approach uses EF Core to be a one-stop-shop for doing database changes but has the business logic in another assembly. While the C(r)UD + Biz Logic approach has everything that changes the database inside the entity class.
Here is a table where I try to list the advantages/disadvantages of each approach.
Approach | Pros | Cons |
1. POCO-only | Follows the original DDD description of handling entities. | Hard to write generic repositories.
Repository pattern in data layer can cause performance problems. The repository can be bypassed. |
2. C(r)UD only | Access methods follow the single responsibility principle.
Business logic is separated from database accesses. |
More work writing the business logic and its dedicated repository. |
3. C(r)UD + Biz Logic | Everything in one place. | Can suffer with the “God Object” anti-pattern.
Problems calling methods in “higher” assemblies. |
While I think a DDD approach is useful, it’s a trade-off of features and limitations whichever way you go. In the end it depends on a) your application and b) what you feel comfortable with. For instance, if your application has some really complex business logic, with lots of calls to other parts of the system then the C(r)UD + Biz Logic approach might not work that well or you.
Conclusion
I do believe using a DDD approach with EF Core has many benefits – it can make the code clearer and more robust (e.g. no way to get it wrong). But coming up with a good DDD approach can take a while – I built my first DDD business logic approach in 2015, and it’s been through two iterations in 2016 and 2017 as I have improved it. This article is just another part of my continuing journey to learn and improve my skills around using DDD.
I am also want to be an efficient developer, so whatever approach must allow me to build quickly. But with a bit of help from some libraries, like the EfCore.GenericServices, I can build both robust, well-performing systems quickly. And when new features, like JSON Patch, become useful I try to adapt my approach to see if it can be added to my DDD style (see my article “Pragmatic Domain-Driven Design: supporting JSON Patch in Entity Framework Core” to see the outcome).
I hope this article helps you decide for yourself whether it’s worth using the DDD approach with EF Core and which approach suits your needs.
Happy coding!
Apologies for commenting on an older article but I’m exploring DDD and have used EF (both core and “regular”) pretty extensively so I’m trying to reconcile what I’ve read in Evans book with these technologies. It seems you follow a similar approach to Julie Lerman but I’m wondering how you would say this relates to the onion architecture or perhaps I’m wrong in considering EF to reside in the infrastructure layer.
Hi Ben,
In the 2nd edition of my book “Entity Framework Core in Action” I created a complex application that used both DDD and modular monolith using a onion-type layers and it was rather nice (see the code in branch Part3-Net6 of the repo, i.e. https://github.com/JonPSmith/EfCoreinAction-SecondEdition/tree/Part3-Net6). The Part3 application was quite complex (it has five different ways to access the database to show different performance approaches) and I used a very extreme layering system to keep each part modular.
I covered using DDD + modular monolith in chapter 13 of the book, but also wrote some extra thoughts on the DDD + MM combinations – try these articles.
PS. I also recommend the book “Building evolutionary architectures” by Neal Ford et al.
regarding “The different ways of updating relationships”: you don’t need to load the full collection in order to modify it! EF is weirdly magic. to add a new review, you can:
and to remove elements, you can:
I don’t know how EF tracks this stuff – feels like black magic – but it does; I routinely add elements without loading the set, and delete elements by loading partial sets.
Hi Ben,
Yes, that feature was added in EF Core 5. My experience is I don’t generally use the Where in an Include, but there are a few specific situations where it will improve performance.
I have rather small commercial experience (year+) and I found Your articles very inspiring. Unfortunately I didn’t have anything in common with DDD before (we are using only ‘standard’ entities) but I’m trying to refactor one of my own projects this way just for fun.
I really like the idea of not having repository (becase db context is a repository) and EF using the same entities in domain (in ddd-style). I love the idea of making everything as simple as possible, but not simpler;-) That’s why I’m a blazor lover – I can use the same models on front-end and in the backend – just keeping them in common layer.
However I dislike having context in a domain object. What do You think about that:
Not sure why this may be problematic (I hope I’m gonna try that this weekend) 🙂
Hi Pawel,
I can’t quite visualise the architecture you are suggesting but having validation in your DDD entities is a good idea. But my experience is that the DDD entities aren’t always the the best model as the DDD entities contain the basic parts of the business needs (e.g. Book, Author, Review) but when the frontend displays a book it has information from all of those DDD entities. Therefore, when I am building an application I use a number of layers, one of which job is to create a queries that visualise the data in the way that the user wants to see – have a look at this article which describes the layers I use. That might help you think about the layers.
Thank You for reply:-) I’ve just finished Your suggested article. It seems that in general I had similar vision (after reading Your other articles):-) I also saw Your repo on github. Your way of thinking is really refreshing:) I’ll definitively buy Your book:)
Thanks for this article. In my experience, the POCO-only approach is well suited for DDD and I don’t think that there is a major downside to not being able to use ORM specific features from within the domain classes. Sure, loading individual parts of an aggregate on demand is no longer possible, but do you really need that ability? When handling business use cases, a complete aggregate is what you normally want anyway, whereas for reporting you can always query directly against the database without going through a repository. By the way, the repository’s job is to presist and rehydrate aggregates. Methods such as AddReview and PlaceOrder don’t really belong there. As for the circular dependencies issue, the solution is to access the repository via an interface, with the interface being part of the domain layer.
Hi Andreas,
Thanks for adding your comments to the discussion. There are lots of different ways to approach DDD – I’m glad the POCO-only approach works for you.
Could you explain yoru comment “Methods such as AddReview and PlaceOrder don’t really belong there”. Where would you place these methods?
Hi Jon,
What I meant was that these are domain methods and should consequently be placed in the appropriate entity classes or in separate domain services. A repository shouldn’t really contain any business logic but should be concerned only with loading and saving aggregates.
By the way, when I talk about repositories, I am not limiting myself to the classic approach where you have a class such as CustomerRepository (and usually an accompanying ICustomerRepository) containing a number of methods for storing and retrieving objects. An alternative design involving query objects and handlers (as described here) is also possible and IMO often preferable. But the basic idea – keeping the domain model and the data model separate – remains the same.
Hi Jon, great post! My two cents a year later.
I would totally agree that using domain services with Interfaces to abstract the repostitory would fit perfectly in DDD. The domain logic would be encapsulated in the domain.
On the other hand the repostitory should always has to deal with a complete aggregate, so the reviews should always has to be part of the Book aggregate root.
With this approach you would have best of two worlds. The Domian bussiness encapsulated and one round trip to the DB.
The other thing I like about keeping separated Domain from EF Entities through repositories is that if you want to move to NoSQL the domain shouldn’t be affected and you should only have to work on new respositories
Thanks for the post and sorry if my english it’s not perfect
Thank you Francisco for adding your thoughts on using DDD with EF Core. There do seem to be many ways to implement DDD, which makes sense as Eric Evans was talking about an approach, not an implementation.
Any implementation that uses DDD in a way that fits Eric Evans’ approach will gain the benefits of DDD.
I am quite surprised why someone would ever want to use EF for DDD when there is an obvious better tool (NHibernate) for the job: https://enterprisecraftsmanship.com/2018/06/13/ef-core-vs-nhibernate-ddd-perspective/