Home .NET DDD-style entities with Entity Framework Core

DDD-style entities with Entity Framework Core

by admin

This article is about how to apply Domain-Driven Design (DDD) principles to Entity Framework Core (EF Core) classes mapped to a database and why it can be useful.

TLDR

There are many advantages to the DDD approach, but the most important one is that DDD moves the creation/modification operations code inside the entity class. This greatly reduces the chances of a developer misunderstanding/interpreting the rules for creating, initializing, and using class instances.

  1. Eric Evans’ book and speeches don’t have much information on this :
  2. Provide the client with a simple model for obtaining persistent objects (classes) and managing their lifecycle.
  3. Your entity classes should explicitly tell you if and how they can be changed and by what rules.
  4. In DDD, there is a concept of an aggregate. An aggregate is a tree of related entities. According to DDD rules, working with aggregates should be done through the "aggregation root" (the root entity of the tree).

Eric mentions repositories in his talks. I don’t recommend implementing a repository with EF Core because EF already implements the "repository" and "work unit" patterns on its own. I discuss this in more detail in a separate article " whether to use the repository along with EF Core ".

DDD-style entities

I’ll start by showing you the code of the entities in DDD-style and then compare it to the way entities are usually created with EF Core. For the example I will use the database of an online bookstore (a very simplified version of Amazon). The database structure is shown in the image below.
DDD-style entities with Entity Framework Core
The first four tables represent everything about the books: the books themselves, their authors, reviews. The two tables below are used in the business logic code. This topic is covered in detail in a separate article.

All the code for this article is posted in the repository GenericBizRunner on GitHub In addition to the GenericBizRunner library code, there is also an example ASP.NET Core application using GenericBizRunner to handle business logic. More on this in the article " Business logic library and Entity Framework Core ".

And here’s the entity code corresponding to the database structure.

public class Book{public const int PromotionalTextLength = 200;public int BookId { get; private set; }//… all other properties have a private set//These are the DDD aggregate propties: Reviews and AuthorLinkspublic IEnumerable<Review> Reviews => _reviews?.ToList();public IEnumerable<BookAuthor> AuthorsLink => _authorsLink?.ToList();//private, parameterless constructor used by EF Coreprivate Book() { }//public constructor available to developer to create a new bookpublic Book(string title, string description, DateTime publishedOn, string publisher, decimal price, string imageUrl, ICollection<Author> authors){//code left out}//now the methods to update the book’s propertiespublic void UpdatePublishedOn(DateTime newDate)…public IGenericErrorHandler AddPromotion(decimal newPrice, string promotionalText)…public void RemovePromotion()…//now the methods to update the book’s aggregatespublic void AddReview(int numStars, string comment, string voterName, DbContext context)…public void RemoveReview(Review review)…}

What to look out for :

  1. Line 5: set-access to all entity properties is declared private. This means that the data can be changed either with the constructor or with the public methods described later in this article.
  2. Lines 9 and 10. Linked collections (those same aggregates from DDD) provide public access to IEnumerable<T> , not ICollection<T> . This means that you cannot add or remove items from the collection directly. You will have to use specialized methods from the Book class.
  3. Line 13. EF Core requires a parameterless constructor, but it can have private access. This means that other application code cannot bypass the initialization and create instances of classes with the parametric-free constructor. Unless you do not create entities purely with reflection.)
  4. Lines 16-20: The only way you can create an instance of the Book class is to use the public constructor. This constructor contains all the information needed to initialize the object. This way the object is guaranteed to be in a valid state.
  5. Lines 23-25: On these lines there are methods to change the state of the book.
  6. Lines 28-29: These methods allow you to change linked entities (aggregates)

The methods on lines 23-39 I will refer to below as "methods that provide access". These methods are the only way to change properties and relationships within the entity. In a nutshell, the Book class is "closed". It is created via a special constructor and can only be modified in part via special methods with appropriate names. This approach creates a stark contrast to the standard approach to creating/modifying entities in EF Core, in which all entities contain an empty default constructor and all properties are declared public. The next question is, why is the first approach better?

Entity creation comparison

Let’s compare the code for getting multiple book data from json and creating instances of Book classes based on it.

a. Standard approach

var price = (decimal) (bookInfoJson.saleInfoListPriceAmount ?? DefaultBookPrice)var book = new Book{Title = bookInfoJson.title, Description = bookInfoJson.description, PublishedOn = DecodePubishDate(bookInfoJson.publishedDate), Publisher = bookInfoJson.publisher, OrgPrice = price, ActualPrice = price, ImageUrl = bookInfoJson.imageLinksThumbnail};byte i = 0;book.AuthorsLink = new List<BookAuthor> ();foreach (var author in bookInfoJson.authors){book.AuthorsLink.Add(new BookAuthor{Book = book, Author = authorDict[author], Order = i++});}

b. DDD-style

var authors = bookInfoJson.authors.Select(x => authorDict[x]).ToList();var book = new Book(bookInfoJson.title, bookInfoJson.description, DecodePubishDate(bookInfoJson.publishedDate), bookInfoJson.publisher, ((decimal?)bookInfoJson.saleInfoListPriceAmount) ?? DefaultBookPrice, bookInfoJson.imageLinksThumbnail, authors);

Code of Book class constructor

public Book(string title, string description, DateTime publishedOn, string publisher, decimal price, string imageUrl, ICollection<Author> authors){if (string.IsNullOrWhiteSpace(title))throw new ArgumentNullException(nameof(title));Title = title;Description = description;PublishedOn = publishedOn;Publisher = publisher;ActualPrice = price;OrgPrice = price;ImageUrl = imageUrl;_reviews = new HashSet<Review> ();if (authors == null || !authors.Any())throw new ArgumentException("You must have at least one Author for a book", nameof(authors));byte order = 0;_authorsLink = new HashSet<BookAuthor> (authors.Select(a => new BookAuthor(this, a, order++)));}

What to look out for :

  1. Lines 1-2: the constructor makes you pass all the data you need for proper initialization.
  2. Lines 5, 6, and 17-9: the code contains several business rule checks. In this particular case, a rule violation is treated as a bug in the code, so an exception will be thrown if there is a violation. If the user could fix these errors, perhaps I would use a static factory returning Status<T> (translator’s note. I would use Option<T> or Result<T> as a more common name). Status is a type that returns a list of errors.
  3. Lines 21-23: The BookAuthor relationship is created in the constructor. The BookAuthor constructor can be declared with access level internal. This way we can prevent the creation of links outside of the DAL.

As you can see, the amount of code to create an entity is about the same in both cases. So why is the DDD style better? The DDD style is better because :

  1. Controls access. Accidental modification of a property is excluded. Any change takes place through a constructor or a public method with an appropriate name. It’s pretty obvious what’s going on.
  2. Corresponds to DRY (don’t repeat yourself). You may need to create instances of Book in multiple places. The assignment code is in the constructor and you don’t have to repeat it in multiple places.
  3. Hides Complexity. The Book class has two properties : ActualPrice and OrgPrice. Both of these values must be equal when creating a new book. In the standard approach, every developer needs to know this. In the DDD approach, it is sufficient that the developer of the Book class knows about it. Everyone else will know about this rule because it is explicitly written in the constructor.
  4. Hides the creation of the aggregate. In the standard approach, the developer must manually create an instance of BookAuthor. In DDD-style, this complexity is encapsulated for the calling code.
  5. Allows properties to have private write access
  6. One of the reasons to use DDD is to "lock down" entities, i.e. to prevent them from changing the properties directly. Let’s compare the change operation with and without DDD.

Property change comparison

One of the main advantages of DDD-style entities, Eric Evans calls the following : "They communicate design decisions about object access explicitly."

Translator’s note. The original phrase is difficult to translate into Russian. In this case design decisions are decisions made about how the software should work. What is meant here is that these decisions were discussed and confirmed. Code with constructors that initialize entities correctly and methods with correct names that reflect the meanings of operations tell the developer explicitly that the assignments of certain values are intentional, not an accident and not another developer’s whim or implementation detail.

My understanding of this phrase is as follows.

  1. Make it obvious how to change data within an entity and which data should be changed together.
  2. Make it obvious when you should not change certain data within an entity.

Let’s compare the two approaches. The first example is simple, and the second is more complicated.

1. Changing the publication date

Suppose we want to work on a draft of a book first and only then publish. When we create a draft, we set an approximate publication date, which is very likely to be changed during the editing process. We will use the PublishedOn property to store the publication date.

a. Entity with public properties

var book = context.Find<Book> (dto.BookId);book.PublishedOn = dto.PublishedOn;context.SaveChanges();

b. DDD-style entity

In DDD-style setter properties are declared private, so we will use a specialized access method.

var book = context.Find<Book> (dto.BookId);book.UpdatePublishedOn( dto.PublishedOn);context.SaveChanges();

The two cases are almost indistinguishable. The DDD version is even slightly longer. But there is still a difference. In the DDD-style you know exactly that the publish date can be changed because there is a method with an obvious name. You also know that you cannot change the publisher because there is no corresponding method for the Publisher property to change. This information will be useful to any programmer working with a book class.

2.Book discount management

Another requirement is that we must be able to manage discounts. The discount consists of a new price and a comment, such as "50% until the end of this week!"
The implementation of this rule is simple, but not too obvious.

  1. The OrgPrice property is the price excluding the discount.
  2. ActualPrice – the current price at which the book is sold. If there is a discount, the current price will be different from OrgPrice by the size of the discount. If not, the property value will be.
  3. The PromotionText property should contain the discount text if the discount is applied or null if no discount is currently applied.

The rules are pretty obvious to the person who implemented them. However, for another developer, say, developing a UI to add a discount. Adding the AddPromotion and RemovePromotion methods to the entity class hides implementation details. The other developer now has public methods with appropriate names. The semantics of using the methods are obvious.
Let’s take a look at the implementation of the AddPromotion and RemovePromotion methods.

public IGenericErrorHandler AddPromotion(decimal newPrice, string promotionalText){var status = new GenericErrorHandler();if (string.IsNullOrWhiteSpace(promotionalText)){status.AddError("You must provide some text to go with the promotion.", nameof(PromotionalText));return status;}ActualPrice = newPrice;PromotionalText = promotionalText;return status;}

What to look out for :

  1. Lines 4 -10: the addition of the PromotionalText comment is mandatory. The method checks that the text is not empty. Since this error can be corrected by the user, the method returns a list of errors to correct.
  2. Lines 12, 13: The method sets property values to the implementation that the developer has chosen. The user of the AddPromotion method does not need to know them. To add a discount, just write :

var book = context.Find<Book> (dto.BookId);var status = book.AddPromotion(newPrice, promotionText);if (!status.HasErrors)context.SaveChanges();return status;

The RemovePromotion method is much simpler: it does not involve error handling. Therefore, the return value is simply void.

public void RemovePromotion(){ActualPrice = OrgPrice;PromotionalText = null;}

These two examples are very different from each other. In the first example, changing the PublishOn property is so simple that the standard implementation is fine. In the second example, the implementation details are not obvious to someone who hasn’t worked with the Book class. In the second case, the DDD-style with specialized access methods hides implementation details and makes life easier for other developers. Also, in the second example, the code contains business logic. As long as the amount of logic is small, we can store it directly in the access methods and return an error list if the method is not used correctly.

3. Working with the unit – property-collection Reviews

DDD suggests working with the aggregate only through the root. In our case, the Reviews property creates problems. Even if the setter is declared private, the developer can still add or remove objects with add and remove methods, or even call the clear method to clear the entire collection. This is where the new EF Core feature comes in. backing fields
The backing field allows the developer to encapsulate the actual collection and provide public access to the IEnumerable<T> interface reference. The IEnumerable<T> interface does not provide add, remove, or clear methods. The code below is an example of using backing fields.

public class Book{private HashSet<Review> _reviews;public IEnumerable<Review> Reviews => _reviews?.ToList();//… rest of code not shown}

For this to work, you need to tell EF Core to write to a private field when reading from the database, not a public property. The configuration code is shown below.

protected override void OnModelCreating(ModelBuilder modelBuilder){modelBuilder.Entity<Book> ().FindNavigation(nameof(Book.Reviews)).SetPropertyAccessMode(PropertyAccessMode.Field);//… other non-review configurations left out}

I added two methods: AddReview and RemoveReview to the book class to work with reviews. The AddReview method is more interesting. Here is its code :

public void AddReview(int numStars, string comment, string voterName, DbContext context = null){if (_reviews != null){_reviews.Add(new Review(numStars, comment, voterName));}else if (context == null){throw new ArgumentNullException(nameof(context), "You must provide a context if the Reviews collection isn't valid.");}else if (context.Entry(this).IsKeySet){context.Add(new Review(numStars, comment, voterName, BookId));}else{throw new InvalidOperationException("Could not add a new review.");}}

What to look out for :

  1. Lines 4-7: I intentionally don’t initialize the _reviews field in the private, parameterless constructor that EF Core uses when loading entities from the database. This allows my code to determine if the collection was loaded using the .Include(p => p.Reviews) method. I initialize the field in the public constructor, so no NRE will happen when working with the created entity.
  2. Lines 8-12: If the Reviews collection has not been loaded the code should use DbContext for initialization.
  3. Lines 13-16: If the book has been successfully created and contains an ID, I use a different technique to add a review : I simply set a foreign key in an instance of the Review class and write it to the database. For more details, see section 3.4.5 of my book.
  4. Line 19: If we end up here, there is some problem with the logic of the code. So I throw an exception.

I designed all my access methods to reverse the case where only the root entity is loaded. How to update the aggregate is left up to the methods. Additional entities may need to be loaded.

Conclusion

To create DDD-style entities with EF Core, you must adhere to the following rules :

  1. Create public constructors to create correctly initialized instances of classes. If errors may occur during creation that the user can correct, create the object using a factory method that returns Status<T> , where T is the type of entity being created, rather than using a public constructor
  2. All setter properties are private. That is, all properties are read-only outside the class.
  3. For navigation properties of collections, declare backing fields, and declare the public property type as IEnumerable<T> . This will prevent other developers from uncontrollably changing collections
  4. Instead of public setters, create public methods for all allowed object change operations. These methods should return void if the operation cannot end with an error that the user can fix or Status<T> if it can.
  5. The scope of entity responsibility matters. I think it’s best to restrict entities to changing the class itself and other classes within the aggregate, but not outside of it. Validation rules should be limited to checking that entities create and change state correctly. That is, I don’t validate business rules such as stock balances. There is special business logic code for that.
  6. Methods that change states should assume that only the aggregation root is loaded. If a method needs to load other data, it must take care of that on its own.
  7. Methods that change states must assume that only the aggregation root is loaded. If a method needs to load other data, it should take care of that on its own. This approach makes it easier for other developers to use entities.

Pros and cons of DDD entities when working with EF Core

I like a critical approach to any pattern or architecture. Here’s what I think about using DDD entities.

Pros

  1. Using specialized methods to change state is a cleaner approach. It’s definitely a good solution, simply because properly named methods reveal the intent of the code much better and make it obvious what can be changed and what can’t. Also, methods can return a list of bugs if the user can fix them.
  2. Changing aggregates only through the root also works fine
  3. The implementation details of the one-to-many relationship between the Book and Review classes are now hidden to the user. Encapsulation is a basic principle of OOP.
  4. Using specialized constructors makes sure that entities are created and guaranteed to be initialized correctly
  5. Moving the initialization code to the constructor greatly reduces the chance that the developer is misinterpreting how the class should be initialized.

Minuses

  1. My approach contains dependencies on the EF Core implementation.
  2. Some people even call it anti-patterning. The problem is that the subject model entities now depend on the database access code. In DDD terms, this is a bad thing. I realized that if I didn’t, I would have to rely on the calling code knowing what should be loaded. This approach breaks the principle of separation of concerns.
  3. DDD forces you to write more code.

Is it really worth it in simple cases, like updating a book’s publication date?
As you may have noticed I like the DDD approach. However, it took me a while to structure it properly, but by now the approach has settled in and I’m applying it to the projects I’m working on. I already tried this style with small projects and I am satisfied, but all pros and cons still need to learn when I will use it with larger projects.
My decision to allow EFCore-specific code to be used in the method arguments of the subject model entities was not an easy one. I tried to prevent it, but ended up having to load a lot of navigation properties for the calling code. And if that wasn’t done, the change simply wouldn’t be applied without any errors (especially in a one-to-one relationship). This was not acceptable to me, so I allowed EF Core inside some methods (but not constructors).
The other bad side is that DDD forces you to write significantly more code for CRUD operations. I’m still not sure whether to keep eating cactus and writing separate methods for all properties, or whether in some cases it’s worth stepping back from such radical puritanism. I know there’s just a wagon and a wagon of boring CRUD that’s easier to write directly. Only work on real projects will show which is better.

Other aspects of DDD not covered in this article

This article is already too long, so I’m going to end it here. But, that means that there is still a lot of undisclosed material. Something I have already written about, something I will write about in the near future. Here’s what’s left over :

  1. Business logic and DDD. I’ve been using DDD concepts in business logic code for several years now and with the new EF Core features, I expect to be able to transfer some of the logic into entity code. Read the article "Again on business logic layer architecture with Entity Framework (Core and v6)"
  2. DDD and the repository pattern. Eric Evans recommends using a repository to abstract access to data. I’ve come to the conclusion that using the "repository" pattern along with EF Core is a bad idea. Why? Read the same article.
  3. Multiple DBContexts / bounded contexts. I’ve been thinking for a long time about dividing the database into multiple DbContexts. For example, create a separate BookContext to handle only Book class and its aggregate and another separate OrderContext to handle orders. I think the idea of "bounded contexts" is very important, especially scaling applications as they grow. I haven’t yet highlighted a pattern for this task, but I expect to write an article on this topic in the future.

The entire code for this article is available at GenericBizRunner repositories on GitHub This repository contains an example ASP.NET Core application with specialized access methods for modifying the Book class. You can clone the repository and run the application locally. It uses in-memory Sqlite as the database, so it should run on any infrastructure.
Happy development!

You may also like