Three approaches to Domain-Driven Design with Entity Framework Core

Last Updated: September 25, 2018 | Created: September 24, 2018

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:

  1. A DDD-styled entity, but not including any references to EF Core commands.
  2. A DDD-styled entity which has access to EF Core’s DbContext and implements CRUD methods.
  3. 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 deal 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:

  1. Get the new review information, with the Book’s primary key.
  2. Load the Book entity instance using the primary key.
  3. 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.):

  1. Load the Books ordered using the primary keys provided by the higher layers
  2. Get the address that the order should be sent to
  3. Create the Order
  4. Save the Order to the database
  5. 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:

  1. 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
  2. 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!