Improving Domain-Driven Design updates in EfCore.GenericServices

Last Updated: July 31, 2020 | Created: January 27, 2020

I love giving talks, because it makes think about what I am talking about! The talk I gave to the Developer South Coast meetup about EF Core and Domain-Driven Design (DDD) gave me an idea for a new feature in my EfCore.GenericServices library. And this article explains what this new feature is, and why people who use DDD with EF Core might find it useful.

TR;DR; – summary

  • EfCore.GenericServices is a library designed to make CRUD (Create, Read, Update, Delete) actions on an EF Core database easier to write. It works with standard classes and DDD-styled classes.
  • In a new release of EfCore.GenericServices I added a new feature to help when writing DDD methods to update relationships – known in DDD as Root and Aggregate.
  • The new approach has two advantages over the existing method in EfCore.GenericServices
    • The update method inside your DDD class is shorter and simpler
    • The new approach removes any reference to EF Core, which works with architectural approaches that isolate the entity class from the database.

NOTE: The EfCore.GenericServices is an open-source (MIT licence) library available at https://github.com/JonPSmith/EfCore.GenericServices and on NuGet. There is an article all aspects of the library (apart from the 3.1.0 improvement) via this link.

Setting the scene – updating a DDD-styled class

If you are not familiar with DDD-styled classes then you might find my article “Creating Domain-Driven Design entity classes with Entity Framework Core” first, especially the part about updating properties in a DDD-styled class.

DDD covers a very wide range of recommendations, but for this article I’m focus on how you might update an entity class. DDD says we should not simply update various properties, but create methods with a meaningful name to do any updates. Plus, it says we should make the class properties read-only so that the methods are the only way to do any update.

Here are two examples of the different approaches: NOTE: the dto (also known as a ViewModel) is a simple class holding the data from the user/system. It contains the key of the Book and the new publish date.

Normal (non-DDD)

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

DDD style

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

The advantages of DDD is the code is in one place, which makes it easy to find and refactor. The disadvantages are you need to write more code, which why I created the EfCore.GenericServices library to reduce the amount of code I have to write. Having started using a DDD approach I looked at my create/update code and I noticed a common set of steps:

  1. Load the class we want to update from the database.
  2. Call the correct method (update) or constructor (create).
  3. Call SaveChanges to update the database.

Having seen this pattern, I decides I could automate all of these steps, so I created the library EfCore.GenericServices. As well as automating create/update the library also handed Read and Delete – all four are known as CRUD (Create, Read, Update, Delete).

The EfCore.GenericServices library contains a object-to-method call mapping capability to automate the calling of methods in a DDD-styled class.The library offers these features for both normal classes and for DDD-styles classes, but the clever bit is around calling DDD methods. For standard classes my library relies on AutoMapper with its object-to-object copying capability, but for DDD classes the library contains and object-to-method call mapping capability. This part of the library maps properties in a DTO/ViewModel to the parameters of methods/constructors in the DDD class, and then calls that methods/constructor (you can read how I map a class to a method in this GenericServices documentation section).

The idea of EfCore.GenericServices is to reduce as much as possible what code the developer has to write. The aim is to remove one of the disadvantages of DDD, i.e. that typically using a DDD requires more code than a non-DDD approach.

UpdateAndSave of relationships is difficult

In the example above I updated a property, PublishedOn, in my class book. That works fine, but if I wanted to Create/Update/Delete (CUD) an associated class (known as Aggregates in DDD) then it gets more complicated.  Typically, the update of an aggregate is done via the Root entity, e.g. a Book class might have a collection of Reviews – DDD says the method to add/update/delete an Aggregate (Review) should be done via the Root (Book).

The problem with this in EF Core is (typically) you need to read in the current relationships before we can apply any CUD action. For example, for my Book class with a collection of Reviews, then adding a new review would require loading the existing Reviews collection before we could add our new Review.

Previously the EfCore.GenericServices library the update method in the entity class had to load the relationship(s). This worked by providing the EF Core DbContext as a parameter in the update method, which allowed the method to use explicit loading (or other methods) to load the relationship. In our Book/Reviews example that would the Reviews collection inside the Book class.

This works, but some people don’t like have database commands in the DDD classes. In fact my current client follows the Clean Architecture approach and has the classes that are mapped to the database at the lowest level, with all the EF Core one level up. This means my approach doesn’t work.

New Feature – IncludeThen attribute!

I have just released version 3.1.0 of the EfCore.GenericServices library with second way to do updates. This uses an IncludeThen attribute to tell the UpdateAndSave method to pre-load the relationships set by the developer in the attribute. This removes the update method to have to load the relationship(s), which has two benefits, a) less code for developer has to write, b) removing the requirement for the entity class to interact with EF Core.

The new feature revolves around an IncludeThen attribute which allows the developer to define a set of relationship(s) they want loaded before the method in the DDD class is called. I give two examples of how that works below.

1. Simple example with just one .Include

The IncludeThen attribute is be added to the DTO/ViewModel you sent the library’s UpdateAndSave method. In this IncludeThen attribute the developer lists the relationship(s) they want loaded before the update method in your DDD classis called. The example below will include the Reviews collection before calling the access method inside the Book class.

[IncludeThen(nameof(Book.Reviews))]
public class AddReviewWithIncludeDto : ILinkToEntity<Book>
{    
    public int BookId { get; set; }
    public string VoterName { get; set; }
    public int NumStars { get; set; }
    public string Comment { get; set; }
}

The lines to point out are:

  • Line 1; The IncludeThen attribute takes a string, so I could have used “Reviews” but I typically use the nameof operator, as if I rename the relationship property the code will still work.
  • Line 2: The ILinkToEntity<Book> tells EfCore.GenericServices that the main class is Book.
  • Line 4: This provides the primary key of the book data that we want to load.
  • Lines 5 to 7: These properties should match in Type and name to the parameters in the access method in the DDD-styled we want to call.

This example translates into the following query:

var book = context.DbSet<Book>()
       .Include(x => x.Reviews)
       .SingleOrDefault(x => x.BookId == dto.BookId) ;

This means the access method to add a new Review is shorter and simpler than the previous approach. Here is my implementation of the AddReview method, where IncludeThen.

public void AddReviewWithInclude(int numStars, string comment, string voterName)
{
    if (_reviews == null)
        //Check that the IncludeThen attribute set the the correct relationship. 
        throw new InvalidOperationException("Reviews collection must not be null");
    _reviews.Add(new Review(numStars, comment, voterName));
}

NOTE: I like shorter/simpler code, but the original version of the Addreview method adds a single Review to the database without loading the Reviews, so it is quicker than the Include version if there were lots of Reviews. Therefore, there is room for both approaches.

2. Example with .Include and .ThenInclude

The IncludeThen is not limited to a single .Include: the IncludeThen attribute has a second parameter of params string[] thenIncludeNames ,to define ThenIncludes to be laded too. There is an example below (this only shows one .ThenInclude, but you can have more).

[IncludeThen(nameof(Book.AuthorsLink), nameof(BookAuthor.Author))]
public class AddNewAuthorToBookUsingIncludesDto : ILinkToEntity<Book>
{
    [HiddenInput]
    public int BookId { get; set; }
    public Author AddThisAuthor { get; set; }
    public byte Order { get; set; }
}

This would translate into the following query:

var book = context.DbSet<Book>()
       .Include(x => x. AuthorsLink).ThenInclude(x => x.Author)
       .SingleOrDefault(x => x.BookId == dto.BookId) ;

You can also have multiple IncludeThen attributes on a class, e.g.

[IncludeThen(nameof(Book.Reviews))]
[IncludeThen(nameof(Book.AuthorsLink), nameof(BookAuthor.Author))]
public class AnotherDto : ILinkToEntity<Book>
{
    //… rest of code left out    

NOTE: You can get a more detailed list of the IncudeThen attribute in this page in the GenericServices documentation.

Updating to EfCore.GenericServices 3.1.0

If you are already using then updating to EfCore.GenericServices version 3.1.0 shouldn’t cause any problems, but you should be aware that the status code (e.g. IGenericStatus) are now coming from the NuGet library GenericServices.StatusGeneric. All the status code is now in a separate library that all my libraries use.

This has two benefits:

  1. You don’t need to include the EfCore.GenericServices in the assemblies that have the classes the EF Core maps to the database, instead you can use the GenericServices.StatusGeneric library. If you are using an architecture like the Clean Architecture approach where you don’t want EF Core included, then swapping from EfCore.GenericServices (which includes EF Core) to GenericServices.StatusGeneric library removes any EF Core references.
  2. This allows EfCore.GenericServices to work with my EfCore.GenericEventRunner library. Errors from the EfCore.GenericEventRunner library can be passed up to EfCore.GenericServices.

Conclusion

The idea I had while explaining about DDD was pretty easy to implement in EfCore.GenericServices (the documentation took twice as long to update!). The change only applies to updates that need access to relationships, but if you are following the DDD’s Root/Aggregate approach there can be quite a few of these.

Using the IncludeThen feature typically means that the access method is simpler and shorter, which makes me (and you!) more efficient. And developers with an architecture or approach where the entity classes have no access/knowledge of EF Core (like my current client) can now use this library.

So, thank you to the Developer South Coast Meetup people approaching me to give a talk, and for the insightful questions from the audience.

Happy coding.

0 0 vote
Article Rating
Subscribe
Notify of
guest
8 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
Sébastien Leroux
1 month ago

Hi Jon,
You book is on its way but I have to wait because of confinement.
I followed all the steps to implement generic services. Everything compiled perfectly but I am getting an error when opening the razor page injecting ICrudServices: InvalidOperationException:
Unable to resolve service for type ‘GenericServices.ICrudServices’ while attempting to activate ‘ObserVision1Web.Pages.Admin.Roles.CreateModel’.
I added the service in the startup file like this :
services.ConfigureGenericServicesEntities(typeof(ExtraAuthorizeDbContext), typeof(MultiTenantDbContext))
.ScanAssemblesForDtos(Assembly.GetAssembly(typeof(CreateRoleDto)))
.RegisterGenericServices();
My shared library (EfCore and models) is netcoreapp3.0
with this EfCore packages : My web project is netcoreapp3.1
Is there a conflict between the versions?
Thanks for your great content.
Best regards
Sebastien

FedeC87p
FedeC87p
1 month ago

Thanks for this new article.

I bought your book and am carefully reading every article.

I like your GenericServices library and I started using it in my projects.

I have a question for update the aggregate entity. If I have to add an item, I have to download all the aggregated items (and if they were thousands of items wouldn’t it be a waste of memory?)

Thank you

Jon P Smith
1 month ago
Reply to  FedeC87p

Hi,

You are right that using Including the Reviews can be slower than directly adding a review. In fact I mentioned that in a NOTE in the article. That’s were the original approach can be better – it will add to the Reviews if it is already loaded, otherwise it doesn’t load the Reviews but creates a single review linked to the book. But to do that you need to use the DbContext that GenericServices can provide as a parameter to the method, which some developers don’t like.

You can see the original method, called “AddReview” in the Book class (see https://github.com/JonPSmith/EfCore.GenericServices/blob/master/DataLayer/EfClasses/Book.cs ). I have also added links to the Book class in this article so other can get there more easily.

FedeC87p
FedeC87p
1 month ago
Reply to  Jon P Smith

Thanks for your reply, i miss that note 🙂

and thanks for the new version, today i will update the version on my project 🙂

Sébastien Leroux
1 month ago

Hi Jon,
Sorry to bother you again.
I posted this a couple weeks ago and I am still stuck when I want to register multiple dbcontexts to use ICrudServices
http://disq.us/p/28iz0s2
I read your documentation multiple times with attention and didn’t find the solution
Please Help would be awesome
Thanks in advance
Sebastien

Jon P Smith
1 month ago

Hi Sebastien,

I *think* the problem that disque wasn’t notifying when a comment was added or I replied. I think its fixed now – can you reply to this if you got a notification. Thanks in advance.

Sébastien Leroux
1 month ago
Reply to  Jon P Smith

No problem at all Jon. Thanks again for sharing your knowledge.

Jon P Smith
1 month ago

Sorry Sebastien, Disqus didn’t alert me to your comments, but your email did it. In future I suggest creating an issue on my library’s GitHub repo – that allows you to include your code and its formatted properly