How to Use Domain-Driven Design in Clean Architecture Domain Layer | .NET 6, DDD

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hello everyone my name is milan and in today's video we are going to be talking about the domain layer of the clean architecture i'm going to show you how to go from an anemic domain model to a rich domain model so if this sounds interesting to you make sure you stick around for the rest of the video i'm going to do a quick recap of the clean architecture here you can see the diagram of the clean architecture and notice that it has four distinct parts these are called layers we have the domain layer the application layer the infrastructure layer and the presentation layer in this video i'm going to focus on the domain layer i'm going to start from an anemic domain model and show you how we can move into a rich domain model by pushing all of the behavior and the interesting business logic inside of our domain we are going to build a gathering management system that i very creatively called gatherly and here is the class diagram for our system you can see that we have four entities in purple the first entity is the member which is the user of our system members can create gatherings to which they can invite other members by sending out an invitation and when a member responds to an invitation and accepts it we create a new attendee which represents a way for us to track which members are attending which gathering and now we can move on into the code my idea here is to start from a few command handlers and show you how we can push behavior down into the domain so that we can move from an anemic domain model to a rich domain model and we are going to start to apply some domain driven design principles the first command handler that i'm going to tackle is the create gathering command handler before i explain the actual handler let me first show you the create gathering command as you can see we have the member id who is creating the gathering the type of the gathering the date and time when it is scheduled and some additional information back in our handler we first fetch the member from the repository then we create a new gathering where we specify that our member is the creator then we have this interesting piece of code where we calculate the gathering type details we have two distinct gathering types the first is with a fixed number of attendees and in this case we need to set the maximum number of attendees on the gathering the other type is a gathering type with an expiration for invitations and we need to calculate and set the date and time when the invitations expire after we have created the gathering we add it to the repository and we save the changes using unit of work to prove that this is actually a completely anemic domain model i'm going to head over to the gathering entity and here you can see that there are no constructors there are no methods everything that we have inside of this class are just data properties so now i'm going to start moving the logic from our command handler inside of our domain into the gathering entity the first thing that i want to tackle is this part here where we are creating the actual gathering i'm going to create a constructor that is going to accept all of these values so let's first do that i'm going to say public gathering and we need to pass in the properties that we want to set the first is going to be id then i want to pass in the member who will be the creator then i need the gathering type i will also need the date and time when it is scheduled the name of the gathering and the location all right and i'm going to quickly assign these values to their respective properties we're assigning the type the schedule that date the name and the location awesome now back in the create gathering command handler let's go ahead and use this constructor so we pass in a new good for the id the member who is the creator we pass in the type the schedule that date and time the name and the location and we can go ahead and remove all of this code all right this was the first step now i need to tackle the second step which is this calculation here i personally don't like putting these kinds of calculations inside of a constructor so what i like to do in cases like this one is actually go for a static factory method on the entity so i'm going to go ahead and do that i will create a static method that returns a new gathering and i will call it create okay for the parameters we can use what we already have in our constructor because we are going to be calling it anyway okay and we can move first this part of code here creating the gathering inside of our factory method and i'm going to fix the parameters so the creator is fine the type the schedule that date the name and location okay this was the the first step and now i can go ahead and move all of this calculation logic inside of the static factory method so i'm going to do that now as you can see we have a few things to fix we are missing the maximum number of attendees and the invitations valid before in hours arguments so i'm going to add them as parameters so i need the maximum number of attendees and the invitations valid before in hours okay i'm going to fix this code now replacing this with the maximum number of attendees and i'm going to replace the second one with the invitation valid before in hours and now we have everything inside of our domain and we can return the gathering that we just created so inside of the command handler now we can use instead of calling the constructor we can use the gathering create method and we'll additionally need to pass in the remaining two parameters all right now we have moved all of the logic inside of the domain layer or rather inside of the gathering entity and our command handler is much cleaner we can also remove this comment because it's now redundant notice a few things i can still go ahead and create a gathering using the constructor so there's a way for me to avoid passing in the maximum number of attendees or the invitations valid before parameters this is not good because it breaks the constraints for creating a gathering so to prevent this i'm going to make the constructor private inside of the gathering class and as you can see now this will no longer be accessible one other thing that i want to address is that nothing is preventing me from just manually setting the gathering type property to whatever i want without conforming to the constraints that we have set inside of the entity and to prevent this you have to make all of the property setters private and this is something that you will often see when practicing domain driven design so i'm going to go ahead and make all of the properties private accept these i'm going to leave this for later and back in the command handler we can no longer change any of the properties we have a very nicely encapsulated gathering entity now that this is looking good we can move on to some of the other command handlers after we have created a gathering we now want to go ahead and send an invitation to some of the other members and for that we have the send invitation command which you can see here all it has is the member id which is being invited and the gathering id to which we are inviting a member and this is the send invitation command handler as you can see we are fetching the member from the repository and also the gathering then we are validating some rules first we want to check that we are not inviting the creator or rather the person creating the gathering cannot invite himself and we also want to check that the gathering is not already in the past because it makes no sense to send an invitation for a gathering that has already finished after we have validated these rules we go ahead and create an invitation over here the invitation needs the member id the gathering id it sets the initial invitation status to pending and the created on date to the current utc time then we add the invitation to the gathering invitations list we add it to the repository we save the changes using the unit of work and we also send an email to the member that is being invited to the gathering using the email service so i'm going to take a similar approach that we had previously first i want to tackle the invitation constructor as you can see here we are using the parameter less constructor and setting all of the properties i'm going to change all of the property setters to be private so that you cannot change them outside of the invitation class then i'm going to create a constructor that is going to accept all the things that we need to instantiate an invitation and in this case we need an id we're going to accept a member object and a gathering object all right and now i can set the id the member id will be will come from the member object and the gathering id will come from the gathering instance the initial status for the invitation will be pending and the created on date and time will be the current utc time all right now i can go back to the command handler and create the invitation using the constructor so i pass in the good for the id the member and the gathering and i can get rid of all of this code and as you can see we simplified the constructor because we don't need to pass in the initial invitation status and the created on date we are setting these values inside of our constructor as part of our domain rules i'm going to delete this code and now let's see what we can improve further so we are creating an invitation and then we are adding it to the gathering when i see a pattern like this what it tells me is that the gathering is the owner of the invitation so it makes sense to move the logic for creating the invitation inside of the gathering entity so to do this we are going to go back to our gathering class and i want to create another method we'll call it send invitation let's make it void for now and we'll see if we need to change that later so send invitation i'm going to copy over these few lines of code into the gathering entity and now let's see what we are missing so we need to somehow get the member and the gathering so the gathering part is easy that is just the current instance so here we can pass this and here we don't need anything we can just access the invitations and add the new invitation for the member we obviously need to get this from the parameter so let's do that and now everything is compiling now one additional thing we can do to even further improve our domain model is to notice that now the gathering entity is taking responsibility for creating the invitation and what that means is that we can make the invitation constructor internal so that it is only accessible from the domain project and everything is still working now we can go back to the send invitation command handler and notice that this is no longer accessible but it doesn't matter since we move the instantiation logic inside of the gathering so instead of having this let's do gathering send invitation and we pass in the member now it's obvious that we are missing the actual invitation so we need to return it from this method so i'm going to make the update instead of void this will return the new invitation and we just return the invitation instance that we created here so back in our command handler we return the local variable for the invitation and everything is working as before notice that we are adding the invitation to the imitations list inside of the send imitation method let me show you again here and we are also doing that here meaning nothing is preventing us from adding an imitation here or even worse somebody can go ahead and do gathering invitations remove and remove the invitation so when we are working with collections inside of our domain model a good rule of thumb is to encapsulate all collection access so how we can do that is if i go over to the invitations property i need to change this from a list to some sort of read-only collection there are a few that we can choose from let's go with i read only collection in this case and you will notice that the invitations no longer has the add and remove methods this is because the i read only collection does not expose any methods for modifying the collection but we still need to be able to add the invitation to the list so what we need to do is create a field that will contain our list so i'm going to say private read-only list of invitations and it's going to be called invitations and i'm going to instantiate it to an empty list here for the invitations property we want to just return the field that is the list of invitations and down here in the send invitation method we want to use the invitations list and everything is working as before and in the send invitations command handler we can get rid of this code one additional thing that we can move to the domain layer is this validation i think it can also fit nicely inside of the send invitation method so i'm going to go ahead and move that there so before we create an invitation we first do some validation we can get rid of this awesome now everything is encapsulated nicely inside of this send invitation method you can see that we first perform some validation and if everything passes we create a new invitation add it to the invitations list and then return it so that it can be added to the repository you're probably wondering why i'm throwing some exceptions with messages here and this is because i want to tackle domain validation in a separate video because i think it's a broad topic and there are more than one ways to approach it so i want to dedicate a special video to discussing this whole topic so i'm going to leave this as is for now back in our command handler now we can get rid of this comment because i don't think it's necessary anymore and you can see that our command handler is looking much cleaner now we just send an invitation using the gathering entity and add the new invitation to the repository and save the changes now that we have sent an invitation to a member we need to expose a way for a member to accept an invitation and for this we have the accept invitation command you can see it here all it has is an invitation id that is being accepted the accept invitation command is handled inside of the accept invitation command headler let's see what we have there the first thing we do is we fetch the invitation using the imitation repository then we perform some checks if the invitation is null or the invitation status is not pending meaning the invitation has either been accepted rejected or something else then we just return otherwise we continue with fetching the member that is associated with the invitation and we also fetch the gathering that is associated with the invitation moving on we get to the actual business logic we need to check if the invitation is expired and this can be in two cases the first case is when the gathering type is with a fixed number of attendees and the current number of attendees on the gathering is equal to the maximum number of attendees the second case is when the gathering type has an expiration for invitations and this expiration date has already passed if the invitation is expired we go ahead and set the status to expired we save the changes using the unit of work and we can return from our handler at this point otherwise we go ahead and accept the invitation by setting the corresponding status we also create a new attendee entity instance which we add to the gathering attendees property we also increase the number of attendees by one we add the newly created attendee to the repository we save the changes and additionally we send an invitation accepted email to the creator of the gathering so that he is aware that somebody has accepted an invitation so let's see how we can go about moving all of this logic into the domain layer the first thing that i need to tackle is the fact that we have code that does not compile you can see that the lines of code where we set the status property and the modified on date are read this is because we set the properties to have a private setter and now we can't change them outside of the class so to solve this i'm just going to create a method on the invitation entity that will do the same thing so the first one is going to be expire i'm going to create it and just copy over what we had in the comment handler and for this to work i need to just remove this and set the property values to expired and the current date and time let's move over back to the command handler and i'm going to do the same thing for these lines of code i will create a new method on the invitation entity and this one i'm going to call accept let's go ahead and create it and paste over what we had in the command handler and fix the compile errors now we have moved the logic for expiring or accepting the invitation into the entity and it is nicely encapsulated back in the command handler we can see that everything is compiling now so at least we have working code now we need to see how we can further move this down into the domain layer i'm going to tackle it one by one first notice that the attendee being created here is using the properties coming from the invitation instance so i'm going to create a constructor on the attendee class which will accept an invitation object and set the proper values so i'm going to say invitation and the gathering id will be the invitation gathering id the member id will be the invitation member id and the created on date and time will be the current utc now all right let's also change the properties so that they have private setters and they can't be changed outside of the attendee class okay looking good back in the command handler we need to now call our constructor we can get rid of this now i also want to point out that the attendee is only created when an invitation is accepted if it is expired we return from this method and we don't end up creating an attendee so i can go ahead and move the piece of code for creating the attendee inside of the accept invitation method i'm going to add that here instead of passing the invitation i pass the current instance and i return the attendee and i also need to change the return type of the accept invitation method okay so back in our accept invitation command handler now our code is going to look like this and notice that i can access the constructor so i'm going to mark it as internal after this and we can get rid of these lines let me go to the attendee entity again and make this constructor internal so that it is only accessible from inside of the domain project back to our command handler now we have a similar pattern that we had previously where we have the attendees collection on the gathering entity and we are adding a new object to that collection we are also increasing the number of the attendees by one what this tells me is i can again do a similar thing and move all of this logic into the gathering entity so i'm going to go ahead and do that i will create a new method that is going to be called accept invitation and it will accept an invitation parameter okay so let's take the piece of code that we have here and move it inside of the gathering entity we can get rid of this to make the code compile and everything is looking good notice that we are going to need the attendee in the command handler so that we can add it to the repository so i'm going to go ahead and return it from this method change the return type and now we can use this method inside of our command handler so i'm going to replace all of this with a call to gathering accept invitation pass in the invitation and create the attendee variable okay now our code is looking a little bit better i want to go back to the accept invitation method and clean up a few more things remember that our attendees property is still a list so anybody can go ahead and add or remove values from this list so let's go ahead and encapsulate this list by creating a private field so i'll create a private read-only list of attendee i will call it attendees and instantiate it to an empty list i also need to change the property here instead of a list it's going to be a read-only collection and it's only going to return the attendees i will also add the private setter to the number of attendees property and i need to fix the code here instead of the attendees property we need to access the attendees field now our code is looking a little bit better inside of our command header we still have some more logic that we can push into the domain so what is preventing us from moving all of this logic into the domain layer we have this call to the unit of work which we don't want to call from the entity but i'm going to go ahead and copy this code over anyway and i will clean it up a little so back in our accept invitation method we first have this logic for checking if the invitation has expired let's clean up the code so that it compiles okay what we can do here is remove these lines of code and in the case when an invitation has expired we can return for example a null value for the attendee now to make this explicit i'm going to change the return type for the accept invitation method so that it is explicitly returning a nullable attendee now is this the best way to approach this i would argue not but i'm intentionally leaving it like this because i want to tackle this in a separate video i only want to focus here on moving logic from the application layer into the domain let's go back to our accept invitation command handler now we can get rid of this logic because it has been moved into the domain layer also this check for the invitation expired and i'm going to also delete this so we are left with this where we accept an invitation we get an attendee back we added to the repository and save the changes now the problem is if an invitation is expired the attendee is going to be null and the compiler is also warning us here so i will do something like this i will say if the attendee is not now then we can go ahead and add it to the repository now i want to save the changes regardless of if it is expired or accepted because i have modifications on the entities in both cases and we need to make one slight change here we don't want to send an invitation accepted email if the invitation was not accepted so let's add a check for that here i will say if invitation status is accepted then in that case go ahead then send an email and that is looking pretty clean we managed to move all of the logic that we had in this command handler into the accept invitation command let's see again what it looks like we have the check if the invitation has expired if so we mark it as expired and we return null otherwise we accept an invitation where we create a new attendee add it to the attendees array and increase the number of attendees by one we can also go ahead and change the accept and expire method on the invitation to be internal because they are only called from inside of the gathering entity this will further prevent anyone from calling these methods outside of the domain layer and i'm pretty happy with what we have here i think also the gathering entity is looking pretty good we managed to move most of the logic from the application layer into this entity and we ended up with a rich domain model in the process so a reminder again what is a rich domain model it means that our class no longer has only simple data properties we have actual logic inside of our entity so here we have a constructor setting some values all of the properties have a private setters they cannot be changed outside of the entity and we have factory methods for creating a new gathering and some methods for sending and accepting the invitation there are a few things that i did not tackle inside of this video and this was intentional let's go back to the accept invitation command handler and notice that we are sending an email here why am i bringing your attention to this specific line of code sending an email is a call to an external service whatever we are using as the email provider let's consider how the flow of our command handler looks like we have some logic in this case for accepting the invitation we then save the changes using the unit of work meaning we are calling the database here and then we are calling an external system in this case sending an email so i have a question for you what would happen if the call to the database completes but sending the email fails for whatever reason another question to consider is can we safely move this line of code here meaning that we first send the invitation accepted email and then we call the database so this is just some food for thought we are going to be tackling this in one of the next videos this is just a little brain teaser for you while you wait for the next video in this series to come out i really hope that you like this video we started with a completely anemic domain model and i showed you how we can slowly move the logic into our entities and our domain layer how we can enforce some constraints by encapsulating behavior and access to the properties of the entities all of these things that i showed you are ideas from domain driven design so i invite you to go ahead and read the book about domain driven design written by eric evans it's a really good book about software engineering and i'm sure you are going to learn a lot from this book if you want access to the source code that i used in this video so that you can follow along with me you can do that by supporting me on patreon it really means a lot if you like this video make sure to leave it a like subscribe to my channel so that you don't miss any of my future videos and until next time stay awesome you
Info
Channel: Milan Jovanović
Views: 29,639
Rating: undefined out of 5
Keywords: clean architecture domain, clean architecture, clean architecture & ddd, ddd, domain-driven design, clean architecture .net 6, clean architecture asp.net 6, clean architecture .net core, clean architecture asp.net core, clean architecture ddd, clean architecture domain-driven design, clean architecture domain layer, clean architecture domain layer .net 6, clean architecture domain layer asp.net 6, domain-driven design clean architecture
Id: 1Lcr2c3MVF4
Channel Id: undefined
Length: 30min 26sec (1826 seconds)
Published: Tue Aug 23 2022
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.