Uncategorized
(2)
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.
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:
-
Input the data via a DTO/ViewModel
-
Load the entity class via its primary key
-
Call the DDD method
-
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.
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));
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.
-
Validation checks, e.g. check a property in a range, which can be done by
validation attributes
.
-
Super-simple business rules, e.g. doing validation checks via code, for validations that can’t be done by validation attributes.
-
Business logic that uses multiple entity classes, e.g. building a customer order to some books.
-
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:
-
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).
-
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.StatusGeneric
ti 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.
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”.
EfCore.GenericBizRunner
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.
Hi mu88 again,
First, you should read my article
My experience of using the Clean Architecture with a Modular Monolith
– ignore the bit about the Modular Monolith an read about the good and bad parts. This explanation also applies to my client’s application (that wasn’t using Modular Monolith).
Where I put the business logic depends on what entity classes it uses. I’m using DDD so I place business logic inside the entity classes as long as the business logic worked on the root or aggregate entities – for instance, adding a Review to a book. For that I use my EfCore.GenericServices to do that (if I hadn’t got that I would have to inject a DbContext into the entity classes).
But for business logic that uses entity classes that aren’t all in an root/aggregate relationship, then DDD says I shouldn’t put the business logic inside the entity class, so I create a class above persistence layer that contain the business logic. For instance, I do this for an Order of a Book, because these two entities are not in the same root/aggregate. Have a look at
this example
from another article about DDD and business logic.
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?
Hi mu88,
My
EfCore.GenericServices library
can handle that. This library is designed to work with normal entity classes, which is uses AutoMapper to update properties, and DDD-styled entity classes where it calls the methods inside the class.
Last year I
updated this library
to be able to pre-load collections, which means it can add a Review to a Collection without the entity classes having to access the DbContext. EfCore.GenericServices makes simpel CRUD really easy to do and it saves me a lot of time.
Not quite sure what you are asking. From what this article about I assume you are asking if I use GenericServices an my GenericBizRunner in the same project.
If that’s what you are asking then the answer is yes. I use GenericServices for CRUD operations and GenericBizRunner for complex business logic. As you can see there is a middle ground where GenericServices can’t do what is needed, but the code isn’t complex enough to use GenericBizRunner. In these cases I just hand-code a class with the required code and register it in DI.
I hope that answers your question.
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…
Hi Demetrios,
Glad you found the article and the EfCore.GenericServices library useful.
Like you I’m always looking for better ways to do something. This article is the typical way I learn – I try something and then I look back to see if I could have done it better. Nearly all of my libraries came about from a previous project where part of it didn’t work as well as I thought it should and, on reflection, I came up with a better way to that part in future projects.
All the best with your project, and your future projects!
PS. I have a ton of articles on DDD (see the sidebar).
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?
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.
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.
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.
Thanks Shaun, really glad my library helped. Sorry I didn’t reply before, but Disqus didn’t alert me.
Keep up the good work!
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)
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.
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?
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.