Domain-Driven Design and Entity Framework Core – two years on

Last Updated: July 31, 2020 | Created: April 4, 2020

This article is about my experiences of applying a Domain-Driven Design (DDD) approach when working with Entity Framework Core (EF Core). I have now used DDD and my supporting libraries for two years on my own projects and client projects. Here are the bigger client projects where I used an DDD approach:

  • A six-month engagement where I architected the backend of a SASS system using ASP.NET Core/EF Core. My design used a lot of DDD concepts and my DDD libraries.
  • A four-month project design and build an adapter between two security systems – this had complex business logic.
  • A six-month engagement on an already started ASP.NET Core/EF Core application to take it to its first release. The project used a different DDD approach to the one I usually use, which taught me a lot.

This article looks at what I have learnt along the way.

TL;DR – summary

  • I really like DDD because I know exactly where the code for a given function is and, because DDD “locks down” the data, which means that the code is the only implementation of that function. The last sentence encapsulates the primary reasons I love DDD.
  • I have found that a DDD approach still works for with projects where the specification isn’t nailed down, or changes as the project progresses. Mainly because DDD functions are easy to find, test, and refactor.
  • But using an DDD approach does require more code to be written. The code is ‘better’ but using a DDD approach can slow down development. That isn’t what I, or my clients, want.
  • To offset the extra code of DDD I have built two libraries – EfCore.GenericServices for calling DDD methods in the classes and EfCore.GenericBizRunner for running business logic.
  • I love my EfCore.GenericServices and use it for lots of situations. It makes working with DDD-styled classes really easy. Maybe the best library I have built so far.
  • I find that business logic ranges from something that is super-simple up to super complex. I have found that I use three different approaches depending on the type and complex of the business logic.  
  • I have found my EfCore.GenericBizRunner library is useful but a bit ‘heavy’, so I tend to only it if the business logic is complicated.

Setting the scene – my approach to making DDD easier to use

I have used a DDD approach for many years, but it wasn’t until EF Core came out that I felt I could build property DDD-styled classed (known as domain entities in DDD, and I use the term entity classes in C#). Once I had that I was full on with DDD, but I found that came at a cost – it took me longer to write a DDD application than my previous approach using my EF6.x library called GenericServices.

As I wrote DDD code I was looking for the repetitive code in using DDD. With my experience of writing the original GenericServices library I knew where to look and I came out with a library called EfCore.GenericServices. This used EF Core and supported non-DDD in a similar way to the original GenericServices library by using AutoMapper’s object-to-object mapping. But the extra bit was its ability to work with EF Core entity classes by providing an object-to-method-call mapping for DDD-styled classes (I also created a EfCore.GenericBizRunner library for handling business logic, but that that works the same for non-DDD and DDD approaches).

The difference these libraries, especially EfCore.GenericServices, has made to my speed of development is massive. I would say I am back up to the speed of development I was with the non-DDD approach, but now my code is much easier to find, fix, and refactor.

NOTE: all my examples will come from a ASP.NET Core application I build to go with my book, “Entity Framework Core in Action”. This example e-commerce site that “sells” books (think super-simple amazon). You can see a running version of this application at http://efcoreinaction.com/ and the code can be found in this GitHub repo.

Why DDD take longer to write, and how can my library help?

Let’s really look at the extra code that DDD needs to work. In the diagram below I have a trivial requirement to update the publication date of a book.

You can immediately see that the DDD code is longer, by about 9 lines. Now you might say 9 lines isn’t much, but in a real application you have hundreds, if not thousands, of different actions like this, and that builds up. Also, some of it repetitious (and boring!), and I don’t like writing repetitious code.

My analysis showed that the process of calling a DDD method had a standard pattern:

  1. Input the data via a DTO/ViewModel
  2. Load the entity class via its primary key
  3. Call the DDD method
  4. Call SaveChanges

So, I isolated that pattern and built library to make it easier, let’s now compare the UpdateDate process again, but this time using my EfCore.GenericServices helping with the DDD side – see diagram below.

Now the DDD code is shorter than the non-DDD code, and all the repetitive code has gone! You can see that the call in the ASP.NET Core action has changed, but it’s the same length. The only extra line not shown here is you need to add ILinkToEntity<Book> to the DateDto class. ILinkToEntity<T> is an empty interface which tells EfCore.GenericServices which entity class the DTO is linked to.  

Also, EfCore.GenericServices has code to handle a lot of edge-cases, like what happens if the entity class isn’t found, and what if the data in the DTO doesn’t pass some validation checks, etc. Because it’s a library its worth adding all these extra features, which takes out other code you might have needed to write.

The pros and cons of putting EF Core code in your entity classes

However, there is an issue with EfCore.GenericServices that I needed to handle – I can load the main entity class, but some actions work on EF Core navigational properties (basically the links to other tables), and how do I handle that? – see EF Core docs where it defines the term navigation properties.

As an example of accessing navigational properties I want to add a Review to a Book (think Amazon reviews). The DDD approach says that a Review is dependant on the Book, so any changes to the Reviews collection should be done via a method in the Book entity class (the term for the Book/Reviews relationship in DDD is Root and Aggregates). The question is, we have the Book loaded, but we don’t have the collection of Reviews loaded, so how do we handle this?

In the first version of EfCore.GenericServices I gave the responsibly for handling the Reviews to the entity class method. This required the method to have access to the application’s DbContext, and here is one simple example of a method to add a new Review to a Book.

public void AddReview(int numStars, string comment, string voterName, 
    DbContext context) 
{
    context.Entry(this)
        .Collection(c => c.Reviews).Load();
    _reviews.Add(new Review(numStars, comment, voterName));                            
}

NOTE: I’m not explaining why I use a backing field, _reviews, in this example. I suggest you have a look at my article “Creating Domain-Driven Design entity classes with Entity Framework Core” for why I do that.

That works, but some people don’t like having the DbContext accessible inside an entity class. For instance, one my client’s project used a “clean architecture” approach with DDD. That means that the entity class has no external references, so the entity classes didn’t know anything about EF Core or its DbContext.

Early in 2020 I realised I could change the EfCore.GenericServices library to load related navigational properties by providing an IncludeThen attribute which defined what navigational property(s) to load. The IncludeThen is added to the DTO which has properties that match the method’s parameters (see this example in one of my articles) . This means I can write code in the DDD method that doesn’t need access to the application’s DbContext, as shown below.

public void AddReviewWithInclude(int numStars, string comment, string voterName)
{
    if (_reviews == null)
        throw new InvalidOperationException(
             "The Reviews collection must be loaded");
    _reviews.Add(new Review(numStars, comment, voterName));
}

Now, you might think that I would use this approach all the time, but it turns out there are some advantages of giving the DbContext to the method, as it has more control. For instance, here is another version of the AddReview method which had better performance, especially if there are lots of reviews on a book.

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.");  
    }
}

This code is longer, mainly because it handles the situation where reviews are already loaded and does some checks to make it more secure. But the main point is that it doesn’t need to load the existing reviews to add a new review – it just adds a single review. That is MUCH faster, especially if you have hundreds of reviews.

Also, it’s not possible to think of all the things you might do and build them into a library. Having the ability to access the application’s DbContext means I have a “get out of jail” card if I need to do something and the EfCore.GenericServices doesn’t handle it. Therefore, I’m glad that feature is there.

But, over the last few years I have concluded that I should minimise the amount of database access code in the entity class methods. That’s because the entity class and its methods start to become an God Object, with way too much going on inside it. So, nowadays if I do need complex database work then I do it outside the entity class, either as a service or as business logic.

To sum up, there are pros and cons allowing the DbContext being injected into the method call. Personally, I will be using the IncludeThen version because it is less coding, but if I find there is a performance issue or something unusual, then I have the ability do fix the problem by adding specific EF Core code inside the entity class method.

Business logic, from the simple to the complex

Back in 2016 a wrote an article “Architecture of Business Layer working with Entity Framework (Core and v6) – revisited”, and also in my book “Entity Framework Core in Action” chapter 4 I described the same approach. Lots of people really liked the approach, but I fear that is overkill for some of the simpler business logic. This section gives a more nuanced description of what I do in real applications.

In the UK we have a saying “don’t use a hammer to break a nut”, or in software principle, KISS (Keep it Simple, Stupid). From experience working on medium sized web apps I find there is a range of business rules.

  1. Validation checks, e.g. check a property in a range, which can be done by validation attributes.
  2. Super-simple business rules, e.g. doing validation checks via code, for validations that can’t be done by validation attributes.
  3. Business logic that uses multiple entity classes, e.g. building a customer order to some books.
  4. business logic that it is a challenge to write, e.g. my pricing engine example.

Now I will describe what I do, especially on client work where time is money.

business logic types 1 and 2 – different types of validation

My experience is that the first two can be done by via my GenericServices library. That because that library can:

  1. Validate the data in any entity class that is being created or updated (this is optional, as the validation is often done in the front-end).
  2. It looks for methods that return either void, or IStatusGeneric. The IStatusGeneric interface allows the method to return a successful status, or a status with error messages.

The code below shows an example of doing a test and returning a Status. This example is taken from the https://github.com/JonPSmith/EfCore.GenericServices repo. This uses a small NuGet package called GenericServices.StatusGenericti supply the IStatusGeneric that all my libraries use.

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

    ActualPrice = actualPrice;  
    PromotionalText = promotionalText;

    status.Message = $"The book's new price is ${actualPrice:F}.";

    return status; 
}

business logic type 3 – working over multiple entity classes

For this business type I tend to just create a class/method to do the job. I combine the business logic and the EF Core database accesses in the same code, because its quick. The downside of this approach is you have business logic mixed with database accesses, which is NOT what DDD says you should do. However, the code is in one place and DRY (only one version of this business logic exists), so if the code starts to get complex, then I can always take it up to business logic type 4.

Practically, I put the business logic in a class and register it with the dependency injection service. If there are a several different business features linked to one entity/area I would typically put a method for each function, but all in one class. I also have a NuGet Status library, which all my libraries use, so it’s easy for each function to return a Status if I need to.

The fact that unit testing with a real database is easy with EF Core means its quite possible to test your business logic.

NOTE: Some people don’t think its right to unit test with a real database, but I find it works for me. If you don’t like unit testing your business logic with real databases, then use the next approach which make it really easy to mock the database accesses.

business logic type 4 – a challenge to write

For this business type, then its correct to apply a strong DDD approach, as shown in my article “Architecture of Business Layer working with Entity Framework (Core and v6) – revisited”. That means I separate the business logic from the database access code by creating a specific repository class for just that business logic. It does take more code/time to do, but the advantages are:

  • Your business logic works on a series of in-memory classes. I find that makes the writing the code much easier, as you’re not having to think about the database side at the same time.
  • If the database classes aren’t a good fit for the business logic you can create your own business-only classes. Then you handle the mapping of the business-only classes to the database classes in the repository part.
  • It very easy to mock the database, because the business logic used a repository pattern to handle the database accesses.

I generally use my EfCore.GenericBizRunner library with this complex type of business logic, but it can be used for business type 3 too. The library is helpful because it can adapt the input and output of the business logic, which helps to handle the mismatch between the business logic level and the front end – bit like a mini DDD anticorruption layer (see the article “Wrapping your business logic with anti-corruption layers”).

Summary diagram

This is fairly long article which covers both CRUD (Create, Read, Update and Delete) functions and business logic function. DDD doesn’t really use terms “CRUD” or “Business logic”. In DDD everything is domain problem, which is solved by calling appropriately-named method(s) in the entity classes.

However I still find the terms “CRUD” or “Business logic” useful to categorise the functions I need to code. Here is a diagram where I try to map the complexity of code to the ways I work.

Conclusion

I hope this article will help people who are starting to use DDD. Obviously, this is my approach and experience of using DDD, and there is no right answer. Someone else might use a very different approach to DDD, but hopefully we all agree that Eric Evans’s Domain-Driven Design book has been one of the key books in making all of us think more about the business (domain) needs rather than the technology.

Nowadays we build really complex applications really quickly because the millions of libraries and documentation that is so easy to access. But as Eric Evans said in his book “When the domain is complex, this is a difficult task, calling for the concentrated effort of talented and skilled people”. That means we need to up our game as developers if we are going to build applications that a) work, b) perform well, and c) doesn’t become an unmanageable “ball of mud”.

Happy coding.

Some of the other articles I have written about Domain-Driven Design:

EfCore.GenericServices

EfCore.GenericBizRunner

4.5 2 votes
Article Rating
Subscribe
Notify of
guest
28 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
mu88
2 years ago

For instance, one my client’s project used a “clean architecture” approach with DDD. That means that the entity class has no external references, so the entity classes didn’t know anything about EF Core or its DbContext.

I’d love to get a bit more of information about your client’s approach. Did they extract all DB logic into a repository?
Our team is thinking about a similar approach: at the moment, our business entities are located within the DAL and the BLL references the DAL. But following Clean Architecture recommendations and therefore the Dependency Inversion Principle, I’d expect the business entities in the domain center (e. g. BLL) and the DAL references them – not vice versa like now.

mu88
2 years ago
Reply to  Jon P Smith

Extremely interesting articles, thank you!

To make it concrete: assuming I want to add a new Review to a Book via AddReview() (<a href=”https://www.thereformedprogrammer.net/creating-domain-driven-design-entity-classes-with-entity-framework-core/#3-handling-aggregates-the-reviews-collection-property”>see here</a>). According to <a href=”https://www.thereformedprogrammer.net/wp-content/uploads/2021/02/CleanCodeExample.png”>this</a>, I’d place to Book and Review classes within Layer 1 (Domain). But since I need the DbContext for AddReview(), this method has to be placed in Layer 2 (Persistence). How do you solve that? Is the Book class partial?

mu88
2 years ago
Reply to  mu88

Oops, sorry for the bad formatting obviously WordPress doesn’t honor HTML formatting

mu88
2 years ago
Reply to  Jon P Smith

Hi Jon,
And in case your neat libraries wouldn’t exist, what would be your architecture?

Jon
Jon
2 years ago

Do you sometime use GenericServices along side BizLogic in a single project?

Demetrios Seferlis
Demetrios Seferlis
3 years ago

Found your blog while I was looking for reasons to get rid of repository pattern and mappers in an asp.net project!!!
After 20min reading your posts I was convinced to give a try to EfCore.GenericServices. After 3 hours coding and try to figure out how this lib works I m really amazed (and my project about a dozen Interfaces and classes less)!!!
You also made me understand parts of DDD I hadn’t realized before…
There are still lot’s of things to learn but felt the need to thank you for this work.
God bless you Jon Smith…

Liarus
Liarus
3 years ago

Fo me personally domain is a clean layer without infrastructure (data access). You’re saying that you have dbcontext included when adding a review to a book because of the performance. Also you mention that review has to be a member of a book. Maybe it doesn’t? Aggregate root i responsible for transaction. If you’re adding a review without checking others belonging to a book, then maybe review should be your aggregate root instead? What is the reason to add review to a collection instead add review with a reference to a book as a value object?

Jon P Smith
3 years ago
Reply to  Liarus

I will try to answer your questions in order:

For me personally domain is a clean layer without infrastructure (data access).

Yes, with EfCore.GenericServices you can now have no database code in your clean layer. The GenericServices can now live outside your domain layer.

You’re saying that you have dbcontext included when adding a review to a book because of the performance.

Yes, I have the option of including the DbContext in the call to the method because I don’t use the clean layer approach. Obviously for you you can’t do that – I would suggest a service in your infrastructure or ServiceLayer instead.

If you’re adding a review without checking others belonging to a book, then maybe review should be your aggregate root instead?

I’m pretty sure that DDD would say that the Review is an aggregate of the root Book. That’s because if the Book isn’t there then the Review has no meaning. I use a performance trick to add a single Review to the database – that doesn’t mean I have changed the Root/Aggregate setup.

I hope that helps your understanding of my article.

jon smith testing
jon smith testing
3 years ago

This is a comment with disqus moderation OFF

Jon P Smith
3 years ago

I am replying to the test comment with notification OFF

jon smith testing
jon smith testing
3 years ago

This is another test of disqus to send me an notification

Jon P Smith
3 years ago

I got a notification and I am replying to this test

jon smith testing
jon smith testing
3 years ago

This is a test to check Disqus is working propertly

Jon P Smith
3 years ago

It isn’t working!

Shaun Cobb
Shaun Cobb
3 years ago

I just wanted to tell you that this library has been a life saver. I work for a hospital and have been building numerous CRUD apps for COVID19 to track supplies, beds, staffing, and much more to ultimately produce dashboards for the executive teams to make important decisions related to this crisis. The work you put in to map DTOs to entities has really sped up my application dev time. Thank you so much for sharing this.

Jon P Smith
3 years ago
Reply to  Shaun Cobb

IHope you don’t mind, but I tweeted your comment – see https://twitter.com/thereformedprog/status/1269203325452517376 It’s nice to know it a) was used for such a good use, and b) that it speeded up your development!

Note – I had to edit your works a bit to fit twitter.

Jon P Smith
3 years ago
Reply to  Shaun Cobb

Thanks Shaun, really glad my library helped. Sorry I didn’t reply before, but Disqus didn’t alert me.

Keep up the good work!

akash
akash
3 years ago

Hi Jon,
Is there any sample code for how to call RemoveReview method using Efcore.GenericService(3.2.2) with DDD approach?
I’ve tried this
await service.UpdateAndSaveAsync(item, nameof(Book.RemoveReview));

and item is of type
public class RemoveReviewDto : ILinkToEntity{
public int BookId { get; set; }
public int ReviewId { get; set; }
}

But I’m getting this error, not sure what I’m doing wrong

System.NullReferenceException: Object reference not set to an instance of an object.
at GenericServices.Configuration.PropertyMatch.ToString()
at GenericServices.Internal.Decoders.MethodCtorMatch.<>c.b__19_1(PropertyMatch x)
at System.Linq.Enumerable.SelectIListIterator`2.MoveNext()
at System.String.Join(String separator, IEnumerable`1 values)
at GenericServices.Internal.Decoders.MethodCtorMatch.ToString()
at GenericServices.Internal.Decoders.DecodedDto.<>c.b__22_3(MethodCtorMatch x)
at System.Linq.Enumerable.SelectListIterator`2.MoveNext()
at System.String.Join(String separator, IEnumerable`1 values)
at GenericServices.Internal.Decoders.DecodedDto.FindMethodCtorByName(DecodeName nameInfo, List`1 listToScan, String errorString)
at GenericServices.Internal.Decoders.DecodedDto.GetMethodToRun(DecodeName nameInfo, DecodedEntityClass entityInfo)
at GenericServices.Internal.MappingCode.EntityUpdateHandler`1.RunMethodViaLinq(TDto dto, String methodName, Object entity, CreateMapper mapper)
at System.Dynamic.UpdateDelegates.UpdateAndExecute5[T0,T1,T2,T3,T4,TRet](CallSite site, T0 arg0, T1 arg1, T2 arg2, T3 arg3, T4 arg4)
at GenericServices.Internal.MappingCode.EntityUpdateHandler`1.ReadEntityAndUpdateViaDtoAsync(TDto dto, String methodName)
at GenericServices.PublicButHidden.CrudServicesAsync`1.UpdateAndSaveAsync[T](T entityOrDto, String methodName)

Jon P Smith
3 years ago
Reply to  akash

Hi akash,

Your call looks OK (NOTE: because you called the DTO RemoveReviewDto it will look for a method called “RemoveReview”). The exception says to me that GenericServices had a problem with the properties in your DTO. It shouldn’t throw an exception so something funny is going on. Could you raise an issue in https://github.com/JonPSmith/EfCore.GenericServices with your Book class and your DTO. I’ll have a look at it, but most likely not till the weekend.

akash
akash
3 years ago
Reply to  Jon P Smith

No prob. i’ll create a repo to reproduce when I get a chance and will link to github issue.
Still my question is if we provide method name in the second parameter of UpdateAndSaveAsync(), will Generic Service still try to match the other methods?
Shouldn’t it just call the method that we supplied?

Jon P Smith
3 years ago
Reply to  akash

Sorry, you code will work. I was just pointing out that GenericServices tries to match using parameters and the DTO name, thus saving you having to add the method name.

Gary Furash
Gary Furash
3 years ago

Do you have some comments or examples you can share about business logic type 3 – working over multiple entity classes?

Jon P Smith
3 years ago
Reply to  Gary Furash

Hi Gary,

Sorry I missed your comment. Disque sometimes doesn’t notify me of a comment.

I don’t have an example of type 3 business logic in my own code, but I was called in towards the end of the project to help out and it wasn’t appropriate to add my EfCore.GenericBizRunner and the BizLogic and BizDbAccess layers so I just build classes in the ServiceLayer to hold the business logic. That works OK, but you don’t get the level of separation that type 4 gives you.