DDD-style Entities with Entity Framework Core

Original author: Jon P Smith
  • Transfer
  • Tutorial
This article is about how to apply the principles of the Domain-Driven Design (DDD) to the classes displayed by the Entity Framework Core (EF Core) on the database, and why this might be useful.

Tldr


There are many advantages in the DDD approach, but the main thing is that DDD transfers the code of create / modify operations into the entity class. This greatly reduces the chances of the developer misinterpreting / interpreting the rules for creating, initializing, and using class instances.

  1. In the book of Eric Evans and his speeches are not so much information on this subject:
  2. Provide the client with a simple model for obtaining permanent objects (classes) and managing their life cycle.
  3. Your entity classes must explicitly state whether they can be changed, exactly how, and by what rules.
  4. In DDD, there is the concept of aggregate. An aggregate is a tree of related entities. According to the DDD rules, work with aggregates should be carried out through the “aggregation root” (the root essence of the tree).

Eric mentions repositories in his speeches. I do not recommend implementing the repository with EF Core, because EF already implements the “repository” and “unit of work” patterns by itself. I tell you more about this in a separate article “ Is it worth it to use the repository with EF Core ?”

DDD style entities


I will start by showing the entity code in the DDD style and then comparing them with how they usually create entities with EF Core (the translator calls the word “usually” an anemic model. ”For example, I will use the Internet database of the book store (a very simplified version of Amazon. ”The structure of the database is shown in the image below.

image

The first four tables represent everything about the books: the books themselves, their authors, reviews. The two tables below are used in the code of business logic. separate article.
All the code for this article is uploaded to the GenericBizRunner repository on GitHub . In addition to the code of the GenericBizRunner library, there is another example of an ASP.NET Core application that uses GenericBizRunner to work with business logic. More about this is written in the article "a library for working with business logic and the Entity Framework Core ."
And here is the entity code corresponding to the database structure.

publicclassBook
{
    publicconstint PromotionalTextLength = 200;
    publicint BookId { get; privateset; }          
    //… 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 CoreprivateBook() { } 
    //public constructor available to developer to create a new bookpublicBook(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 propertiespublicvoidUpdatePublishedOn(DateTime newDate)…
    public IGenericErrorHandler AddPromotion(decimal newPrice, string promotionalText)…               
    publicvoidRemovePromotion()…
    //now the methods to update the book’s aggregates publicvoidAddReview(int numStars, string comment, string voterName, DbContext context)…
    publicvoidRemoveReview(Review review)…                        
}

What to look for:

  1. Line 5: set access to all properties of entities declared private. This means that the data can be modified either by using the constructor, or by using the public methods described later in this article.
  2. Lines 9 and 10. Associated collections (the very 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 non-parametric constructor, but it can have private access. This means that another application code will not be able to bypass initialization and create instances of classes using a non-parametric constructor (note a translator. If you don’t of course create entities solely using reflection)
  4. Lines 16-20: The only way you can create an instance of the Book class is to use a public constructor. This constructor contains all the necessary information to initialize the object. Thus, the object is guaranteed to be in the allowed (valid) state.
  5. Lines 23-25: These lines are methods for changing the state of the book.
  6. Lines 28-29: These methods allow you to change related entities (aggregates)

The methods on lines 23-39 will be referred to as “methods providing access”. These methods are the only way to change the properties and relationships within an entity. In the bottom line, the Book class is “closed”. It is created through a special constructor and can only be partially changed through special methods with suitable names. This approach creates a sharp contrast with 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?

Comparing entity creation


Let's compare the code for getting data about several books from json and creating on their basis instances of classes Book.

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 class constructor

publicBook(string title, string description, DateTime publishedOn, 
    string publisher, decimal price, string imageUrl, 
    ICollection<Author> authors)
{
    if (string.IsNullOrWhiteSpace(title))
        thrownew 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())
        thrownew 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 for:

  1. Lines 1-2: the constructor forces you to transfer all the data necessary for proper initialization.
  2. Lines 5, 6, and 17-9: the code contains several checks of business rules. In this particular case, violation of the rules is considered as an error in the code, so in case of violation, an exception will be thrown. If the user could correct these errors, perhaps I would use a static factory that returns Status <T> (comment of the translator. I would use Option <T> or Result <T> as the more widely used name). Status is a type that returns a list of errors.
  3. Lines 21-23: The BookAuthor link is created in the constructor. The BookAuthor constructor can be declared with the access level internal. In this way, we can prevent the creation of links outside of 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. Random property change is excluded. Any change occurs through the constructor or public method with the appropriate name. It is quite obvious what is happening.
  2. Complies with DRY (don't repeat yourself). You may need to create Book instances in several places. The assignment code is in the constructor and you do not have to repeat it in several places.
  3. Hides complexity. There are two properties in the Book class: ActualPrice and OrgPrice. Both of these values ​​should be equal when creating a new book. In the standard approach, every developer should be aware of this. In the DDD approach, it’s enough for the Book class developer to know about it. The rest will know about this rule, because it is explicitly written in the constructor.
  4. Hides the creation of an aggregate. In the standard approach, the developer must manually create an instance of BookAuthor. In the DDD style, this complexity is encapsulated for the calling code.
  5. Allows properties to have private write access
  6. One of the reasons for using DDD is to lock an entity, i.e. Do not give the ability to change properties directly. Let's compare the change operation with and without DDD.

Property change comparison


One of the main advantages of DDD-style entities is Eric Evans, who calls the following: “They clearly describe the rules for accessing an object”.
Note translator. The original phrase is difficult to translate into Russian. In this case, design decisions are decisions made about how the software should work. It is understood that the decisions were discussed and confirmed. Code with constructors that correctly initialize entities and methods with correct names, reflecting the meaning of operations, explicitly informs the developer that assignments of certain values ​​are made intentionally, not by mistake, and are not a whim of another developer or implementation details.
I understand this phrase as follows.

  1. Make it obvious how to change the data inside the entity and what data should change together.
  2. Make it obvious when you do not need to change certain data in essence.
Let's compare the two approaches. The first example is simple, and the second is more complicated.

1. Change the publication date


Suppose we want to first work with the draft of the book and only then publish it. At the time the draft is created, the estimated publication date is set, which is very likely to be changed during the editing process. To store the publication date, we will use the PublishedOn property.

a. Entity with public properties


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

b. DDD-style entity


In the DDD style, the 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();                    

These two cases are almost the same. The DDD option is even slightly longer. But the difference is still there. In the DDD style, you know for sure that the publication 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 appropriate method for the Publisher property to change. This information will be useful to any programmer working with a book class.

2. Manage a discount for a book


Another requirement is that we should be able to manage discounts. The discount consists of a new price and a comment, for example, “50% by the end of this week!”

The implementation of this rule is simple, but not too obvious.

  1. The OrgPrice property is the price without discount.
  2. ActualPrice - the current price at which the book is sold. If the discount is valid, then the current price will differ from OrgPrice on the size of the discount. If not, then the value of the properties will be equal.
  3. The PromotionText property must contain the discount text if the discount is applied or null if the discount is not currently applied.

The rules are pretty obvious to the one who implemented them. However, for another developer, say, developing a UI to add a discount. Adding the AddPromotion and RemovePromotion methods to an entity class hides implementation details. Now another developer has public methods with corresponding names. The semantics of using methods is obvious.

Let's 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 for:

  1. Lines 4 -10: the addition of a PromotionalText comment is required. The method checks that the text is not empty. Because This error can be corrected by the user. The method returns a list of errors to correct.
  2. Lines 12, 13: the method sets the property values ​​according to the implementation that the developer chose. The user of the AddPromotion method does not need to know them. To add a discount, simply 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.

publicvoidRemovePromotion()
{
    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 quite appropriate. In the second example, the implementation details are not obvious to someone who did not work with the Book class. In the second case, the DDD-style with specialized access methods hides implementation details and makes the lives of other developers easier. 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 a list of errors if the method is not used correctly.

3. Work with the unit - property collection Reviews


DDD offers to work with the unit only through the root. In our case, the Reviews property creates problems. Even if setter is declared private, the developer can still add or remove objects using the add and remove methods or even call the clear method to clear the entire collection. This is where the new EF Core feature will help us - backing fields .

Backing field allows a developer to encapsulate a real collection and provide public access to the IEnumerable <T> interface reference. IEnumerable <T> does not provide add, remove, or clear methods. In the code below, an example of using backing fields.

publicclassBook
{
    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 that when reading from a database, you need to write to a private field, and not a public property. The configuration code is shown below.

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

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

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

What to look for:

  1. Lines 4-7: I intentionally do not initialize the _reviews field in the private non-parametric 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. In a public constructor, I initialize the field, so that NRE does not happen when working with the created entity.
  2. Lines 8-12: If the collection of Reviews was not loaded, the code should use DbContext for initialization.
  3. Lines 13-16: If the book was successfully created and contains an ID, then I use another technique for adding a review: I simply set the foreign key in the Review instance and write to the database. More on this is written in section 3.4.5 of my book.
  4. Line 19: If we are here, then there is some problem with the logic of the code. Therefore, I throw an exception.

I designed all my access methods for reversed the case when only the root entity is loaded. How to update the unit is left to the discretion of the methods. You may need to load additional entities.

Conclusion


To create DDD-style entities with EF Core, you must follow these rules:

  1. Create public constructors to create correctly initialized instances of classes. If during the creation process errors can occur that the user can correct, create an object not using a public constructor, but using a factory method that returns Status <T>, where T is the type of entity being created
  2. All properties setters are private. Those. all properties are read-only outside the class.
  3. For the navigation properties of the collections, declare the backing fields, and the type of the public property declare IEnumerable <T>. This will not allow other developers to change collections uncontrollably.
  4. Instead of public setters, create public methods for all allowed object modification operations. These methods should return void if the operation cannot complete with an error that the user can fix or Status <T> if they can.
  5. The scope of responsibility of the entity matters. I think it is best to limit entities to changing the class itself and other classes inside the aggregate, but not outside. Validation rules should be limited to validating the creation and change of state of entities. Those. I do not check business rules such as stock balances. To do this, there is a special code of business logic.
  6. State-changing methods should assume that only the aggregation root is loaded. If a method needs to load other data, it must take care of this itself.
  7. State-changing methods should assume that only the aggregation root is loaded. If a method needs to load other data, it must take care of this itself. This approach simplifies the use of entities by other developers.

Pros and cons of DDD entities when working with EF Core


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

pros


  1. Using specialized methods for state change is a cleaner approach. This is definitely a good solution, simply because the correctly named methods reveal the intent of the code much better and make it obvious what can be changed and what is not. In addition, methods can return a list of errors if the user can correct them.
  2. Changing aggregates only through the root also works well.
  3. The details of the one-to-many communication between the Book and Review classes are now hidden to the user. Encapsulation is the basic principle of OOP.
  4. Using specialized constructors allows you to make sure that the entities are created and guaranteed to be correctly initialized.
  5. Moving the initialization code to the constructor significantly reduces the likelihood that the developer does not correctly interpret how the class should be initialized.

Minuses


  1. My approach contains dependencies on the implementation of EF Core.
  2. Some people even call it the anti-pattern. The problem is that now entities of the subject model depend on the database access code. In DDD terms, this is bad. I realized that if I had not done this, I would have to rely on the fact that the calling code knows what needs to be loaded. This approach breaks the principle of separation of concerns.
  3. DDD makes writing more code.

Is it really worth it in simple cases, like updating a book's publication date?
As you can see, I like the DDD approach. However, it took me some time to structure it correctly, but at the moment the approach has already settled down and I apply it in the projects I am working on. I have already managed to try this style in small projects and am satisfied, but all the pros and cons are yet to be learned when I apply it in large projects.

My decision to allow the use of EFCore-specific code in the arguments of the methods of the entities of the subject model was not easy. I tried to prevent this, but in the end I came to the conclusion that the calling code had to load a lot of navigation features. And if this is not done, the change will simply not be applied without any errors (especially in a one-to-one relationship). This was not acceptable to me, so I allowed the use of EF Core inside some methods (but not constructors).

Another bad side is that DDD makes you write significantly more code for CRUD operations. I'm still not sure whether to continue to eat cactus and write separate methods for all properties, or in some cases it is worth moving away from such a radical puritanism. I know that there is just a car and a small truck of a boring CRUD, which is easier to write directly. Only work on real projects will show what is best.

Other aspects of DDD not covered in this article.


The article was too long, so I'm going to finish here. But, it means that there is still a lot of undisclosed material. I already wrote about something, I will write about something in the near future. Here is what is left behind:

  1. Business logic and DDD. I have been using DDD concepts in business logic code for several years now and using the new features of EF Core, I expect that I can transfer part of the logic to entity code. Read the article "Again about the architecture of the business logic layer with the Entity Framework (Core and v6)"
  2. DDD and the repository pattern. Eric Evans recommends using a repository to abstract data access. I came to the conclusion that using the “repository” pattern together with EF Core is a bad idea. Why? Read in the same article.
  3. Multiple DBContext / bounded contexts. I have been thinking about dividing the database into several DbContext for a long time. For example, create a separate BookContext to work only with the Book class and its aggregate, and another separate OrderContext for processing orders. I think the idea of ​​“limited contexts” is very important, especially scaling applications as they grow. So far I have not identified a pattern for this task, but I expect to write an article on this topic in the future.

All the code for this article is available in the GenericBizRunner repository on GitHub . This repository contains an example of an ASP.NET Core application with specialized access methods for changing the Book class. You can clone the repository and run the application locally. It uses in-memory Sqlite as a database, so it should run on any infrastructure.

Happy development!

Also popular now: