Domain Modeling with Domain-Driven Design (From Scratch)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
in today's video we're going to do a domain modeling exercise where I'm going to discuss the use of Neds value objects card Clauses and how to unit test your domain so buckle up because this is going to be a value packed video this is the diagram of the sample domain that I will be working on and it represents a system for tracking your running workouts the workouts themselves are scoped to a particular user that can have one or many followers so it's going to be a sort of a social application where more Runners can connect with each other each user will be able to maintain a library of workouts and each workout consists of one or more exercises but the workout is only a template that you can apply to your actual training and each individual training instance is represented as an activity obviously you can have many activities and because of the social factor of the system these activities can have likes and comments from other users this is the high level overview of the system that I want to build and it's it's going to evolve over time into a modular monolith architecture but let's not get ahead of ourselves and let's dive into the code and create our first n the application is called Run Tracker and you'll see that I have an empty domain project with no classes inside so we're starting literally from scratch so I'm going to create a first folder that's going to group the types inside based on the feature they belong to and this is going to match the Aggregates that I had on the diagram earlier and I'll start from the users aggregate so the first thing I need inside of this folder is a type to represent the user of my system I'm going to make this type public so that it's accessible outside of the scope of this assembly and I will make it sealed and this will be my first domain entity the next thing I want to do is to start introducing some properties on my user and I'm actually going to only add one property which will be the user's name I'm going to start off by representing the name property as a string and it's going to have a private Setter the use of primitive types in your domain is something that you should generally avoid if the thing that you're modeling in this case a user's name has any properties that you can Define using a concrete type for example we could require that the user's name should consist of two parts for example the first and the last name secondly we want to enforce some constraints on this type and if we use a string we would have to impose those constraints on the user type to make sure the name is valid an alternative approach to this would be representing the name as a specific type so let me use a public sealed record which I will call a name and it's going to have a primary Constructor accepting a string value for this name and now what I can do is replace the Primitive string type on the user with the concrete name type and the benefit of this approach is that now I can use the name object to impose any constraints that a user's name should have this brings us to the next problem and that is the primary Constructor of this record which is public so anyone can use this Constructor and set whatever value they want for the name so you can make your design more robust by making for example a public Constructor but an explicit one inside of this record and let's say we want to accept a nullable string value I'm going to assign this value to an auto property called value and this value will be a non-nullable string because we are using nullable reference types we can enforce these invariance at compile time and the compiler can help us make sure that we are not working with nulls anywhere what you can do in this case when the string could explicitly be null and it could also be empty is write what's called a guard Clause so you could do something like this let's say if string is null or empty value and then we just throw an exception so let's say we throw a new argument null exception and we give it the name of this value as the parameter name another way you can write this is using the extension methods available on the argument exception type and there's a frow if null or empty method where you can pass your value and this will make sure that the name properties value is never a null or empty string what I just implemented with the name type is called a value object in domain driven design a value object is a type that is immutable and Records give us this quality and two value objects are equal if their values are the same and Again Records give us this quality so this is why I like to use records as value objects while modeling my domain so let's continue by adding a Constructor on the user entity and I'm going to make this Constructor private and it's only going to accept a name parameter and assign it to the respective Ive property I already mentioned that the user class is an entity in my domain and what this means in practice is that I need a way to track the user throughout the lifetime of the application which is why a user should have a unique identifier so I'm going to add another folder in my domain where I'm going to Define some abstractions that I'll need for representing my Concepts in the domain and the first one that I'm going to add will be a base type for an N this type is going to be straightforward to start with it's going to be a public abstract class and it's only going to expose one property which will be the identifier of this entity and it's going to have a get and an init Setter then I'm going to expose a protected Constructor on this entity which you can use to assign the value of the identifier to The Entity and then I'm going to inherit from this class in my user nid to make it explicit that this is an nid now I have to add the ID as the argument to the users Constructor and I'll pass this ID to the base class Constructor so now you might be asking why did I make the Constructor of the user private and why did I also make the set on the name property private how are we going to create a user instance if this type has a private Constructor the reason for doing this is to have more control over how the user entity is created and a popular pattern that you will see for creating your domain entities is using the static Factory pattern this involves creating a static method that returns an instance of your Ned and you can just call it create or something similar and I only need one argument to create a new user which is just the user's name so now I can call the user Constructor pass it a new goodd for the identifier and assign it the name that I got through the create method argument then I can return this user and this will implement the create method notice that I don't have have to check if the name is null because I'm using nullable reference types and I made it explicit that the name isn't null and another constraint is that the name is actually a value object so if I'm passing in a name instance it will never be null and the value property can never have an invalid name of course I could impose more rules on what this value should be what a name should look like and so on but that's besides the point and I think you get the idea of what I'm trying to do here now I'm going to move the name value object into its own file and I want to have a way to express some side effects when I create the user for example I might want to send an email to the user for them to verify their account or let them know that they need to subscribe to this system to continue using the application but the main problem with expressing side effects is how do you decouple them from your actual business logic this is where the domain events pattern is very useful in domain driven design and I'm going to show you just an initial implementation of this I'm going to define a marker interface to represent a domain event I will call it I domain event and then I'm going to expand my Ned Base Class to hold a list of domain events inside we're going to call this list domain events and we're going to give it an empty list as the default value and then I want to expose a protected method on the Ned it's going to be called raise and it will accept a domain event argument and in the implementation we're just going to append this domain event to The Domain events list another thing that could be useful would be to expose the list of domain events so I domain event let's call it the domain events property and I'm going to expose it by copying the internal domain events into a new list so that whoever has access to the domain events property isn't allowed to mutate my internal collection of domain event events how you use domain events is you raise them from your domain entities so in our case this will be the user so let's say after we create a user we're going to call the raise method and provide a new domain event right now I don't have any domain events so let's go ahead and create one I like to use records to represent my domain events because the primary Constructor syntax is very simple and they are immutable by Design so let's create a user created domain event I'm just going to give it the user identifier value and we need to implement the I domain event interface and now I can go ahead and pass in a new instance of the user created domain event and give it the users's identifier in the call to the raise method and now this allows me to express the side effect that a user has been created after processing my business use case and anyone that is interested can subscribe to this event is going to get published after we persist the main use case and it can be handled asynchronously to perform any side effects separately from the main use case or the main transaction I'm going to move the user created domain event into its own file and I'm going to add a unit test project to our solution so that we can write some initial unit tests for our domain types so let me create an xunit test project I will call it the main unit tests and I'm going to next test it under the test folder it's going to be at Net 7 project and let me create my unit test project I'm going to install an additional nugget package that I really like to use in my unit tests and this is the fluent assertions package it's going to allow me to write my test assertions in a fluent way and I really appreciate using this syntax so now let's go ahead and write our first unit test I'm going to match the folder structure that I have in the domain project so I'll create a users project now I'm going to move this unit test inside and let's rename it to for example name tests to write some test for the name value object because I'm using X unit I can use the theory attribute to allow me to specify some parameterized tests for example I can provide inline data and I want to provide a null and an empty string so what I want to do is to test the Constructor of the name value object and confirm that is going to throw an exception for an invalid name value so let's call this Constructor should throw argument exception when value is invalid this will describe my test case I need to Define my argument and now I can write my test body in order to test that an exception is being prone you can define a local function so I'm going to reference my domain project and import the using statement I'll call this local function just action and it's just going to call the name Constructor and pass it the value I can even use the simplified format of this Constructor because the return type is explicit in the local function and now I can write my assertion so let me add an assert comment and how you achieve this with fluent assertions is you call the fluent actions type and you say invoking you pass your action as the argument and then you can say should throw and we said we are expecting an argument exception so let's check for that exception and I can also say that the parameter name of this exception is equal to Value so I'll say should be value and this will actually match the name of the value that is expected in the name value object so if I run this test from my test test Explorer let's see what we're going to get in the results so I'm going to run all of the tests and the results come back and both of the tests pass but if I make a slight change to my test case and instead of throw I say Throw exactly the argument exception and run my tests again I'm going to get a different result the first test case which is checking for an empty string is going to pass but the second test case which is using a null value is going to fail and this is because the guard clause in the name value object that's implemented on the argument exception type is going to throw different exceptions based on the value passed in so for a null value it's going to throw an argument null exception and for an empty value is going to throw an argument exception so if you want to make your guard Clauses more explicit you can go ahead and Implement something yourself it's not really that complicated so I'll create a public static class that I will call ensure inside of it I'm going to create a simple static method it's going to be void and I'll say not null or empty so the call to this will be ensure that something is not null or empty let's give it a nullable value that we're going to check I'm also going to use the C feature that allows me to capture the name of the parameter in the calling method so I'll use the color argument expression I'll give it the name of the argument that I'm looking for which is my value here this will be a nullable string called Ram name and it's also going to have a default value of null and then in the implementation I'm going to do a simple string is null or empty check pass in the value and if this evaluates to True let's throw a new argument null exception and we're going to pass it the parameter name that we got from the color argument expression and now if we use our guard clause in the name Constructor by saying ensure not null or empty value we're going to get the same result making sure that the value is never null but you'll notice that the compiler is complaining that the value could be null even though we did a null check in this method and we made sure that we throw an exception if this evaluat to true so what you can do in this case is decorate the value with a not null attribute and this will tell the compiler that when this method completes the not null or empty method the value being checked which is basically our name here will never be null if we successfully return from this method and you can see that when I make this update the compiler warning goes away and now it's sure that the value is not null when we reach the assignment to the property because I'm using the argument null exception here let's go ahead and update the test to expect this specific exception and now if I run the test test they should both succeed and you see that this is the case now let's go ahead and write some unit tests for our user type so I'll create a user test class and inside of it we're going to write some unit test for the user NTI so let's start with our first test which will check that the create method actually returns a not null user I'll call this test create should return user when the name is valid so let's do the arrange act assert format here and I'll start by creating a variable for my name so I'll create a new name let's call this the full name of this user and then in the ACT step we're going to create a user variable which we are going to get by saying user create and we pass it the name and then I can do a simple assertion here checking that the user should not be null so with this in place I have a simple test let's go ahead and run this and see what we get back and you can see that this test passes and I'm going to copy this for the second test that I want to write so this one is going to check that the create method should raise a domain event when the name is valid the arrange and the ACT steps remain the same but in the assert step I'm going to update this to check the domain events collection and I'm going to say should contain a single domain event inside and I'm going to additionally check that this domain event should be of a specific type and I'm going to say user created domain event so I'm checking that the domain events collection after the call to the create method contains a single domain event and it is specifically the user created domain event and you see that both of our user tests are passing in the future videos I'll continue adding more Concepts to the Run Tracker system so make sure to subscribe to my channel so that you don't miss those videos and until next time stay awesome
Info
Channel: Milan Jovanović
Views: 18,545
Rating: undefined out of 5
Keywords: domain-driven design, domain-driven design eric evans, domain-driven design bounded context, domain-driven design in asp.net core applications, domain-driven design c#, domain-driven design example c#, domain-driven design tutorial, domain-driven design modeling, domain-driven design model, domain-driven modeling, domain-driven design clean architecture, domain-driven design domain layer, domain layer, clean architecture domain layer, domain modeling, ddd, clean architecture
Id: 5qzrgk5291g
Channel Id: undefined
Length: 18min 5sec (1085 seconds)
Published: Tue Oct 31 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.