The Identity Paradox | DDD, EF Core & Strongly Typed IDs

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
okay so today's video is about a very interesting Paradox when it comes to domain driven design and anti-firmal core this is something that has been considered unsolved and I hope in today's video then when you follow along then you'll also see how this can actually be solved in a pretty creative way that I think you're going to like let's Jump Right In and understand what we're talking about so all the fuss is about whether or not to use IDs that are strongly typed or not so what do we mean when we're defining the ID of an object then we can either use the Primitive underlying type for example over here using the string as the underlying type of the ID or we can encapsulate the user ID inside an a value object let's call this Rd and store that okay now if you're asking yourself why would I even go through the effort of wrapping each one of my IDs inside an object then I want us to take a look at an example okay so let's see what we have over here so we have the get user method and this method receives the user ID and the Tenant ID and it returns a user object now over here we can see that we're calling it correctly with the user ID and the Tenant ID because it corresponds to the values over here but if you created products for companies that have many tenants then I'm sure you're familiar with the very very common bug that these two are flipped around and are passed in the wrong order obviously when you take this and you switch it with strongly typed IDs then this is perhaps one of the main benefits right so you have over here now the user ID and the Tenant ID and you can no longer get this wrong but you have to pass the correct ID because the types must match but of course there are other things as well that you can do so once you have this inside an object so let's imagine we have some reservation ID and we want the reservation ID to be a function of both the guest ID and the dinner ID then what we can do is we can create some method that encapsulates the logic of creating this ID based on different IDs right so over here let's say for every dinner you can only have one reservation per guest then over here you're also enforcing that you also have an ID that's a bit more readable if you just come across it and you can also validate that it must be created via these IDs and also over here you can't mix up the order between them because what we talked about before okay moving on to the last made the best reason why to take this approach so let's imagine we have two objects and both of them are referencing the same ID so if for some reason we need to change the type of the ID from a guiit to a string in the future then if we have it like we have over here where it's Camp it's encapsulated inside an object then we don't need to go to each one of the IDS and change it from going to string but we simply change the underlying type over here because this is just an implementation detail and it's encapsulated inside the ID object okay so there are some reasons to take this approach the question is what is the recommendation when you're creating a new project or you need to make the decision whether to use strongly typed IDs or not so the obvious thing to do is to to go to charge EBT and ask chargpt what it thinks and I talked to rgbt we had a very short discussion on the matter and I asked it if this is what the pros do and it said that yes all right so there's no question about it this is what we should do but we can also see that there are many blogs written about it and articles and if we look at Von Vernon the author of The Big Red Book and we look at some of his code examples then we can see that he also creates an ID object that encapsulates the actual ID value which is over here okay now let's put that together with the fact that Aggregates only reference each other through the ID of another aggregate root so if we have two aggregate roots and they need some reference between them then they're only allowed to hold the ID of the other aggregate so let's see what happens when we put all this together okay so here's an example of a domain layer and we can see that we have many ID objects across our our domain layer each one encapsulates the corresponding ID of the entity so where does this become a bit awkward well because this doesn't work with empty thermal core okay so I want us to look specifically at the host and the menu like we talked about before and see why this doesn't work from a technical point of view understand what's actually happening and how we can solve this so let's imagine like we said before we have some menu the menu has an ID again this over here is a value object and we have the host the host has a list of menu IDs where again each one of these is a value object okay so this is the data that we have behind the scenes and we can model it in in c-sharp something like the following so we have the menu ID value object and over here it is the ID of the menu aggregate but it's also over here a list of value objects in the host okay now the the way we would like to store it in the database is the following so we have over here the minis table and we have here the ID where the underlying value inside the menu ID right so the menu ID has the actual value of the ID then it is simply mapped to a column in the menus table then we have the list of menu IDs in the host address where this time it's not mapped to a column in the table that's because it's a list so it's mapped to another table it's called host menu IDs and over here we have the underlying value sitting over here so how do we configure this with anti-firmware core so it's pretty simple right from the menu point of view then we Define that for the ID property on the way in we're grabbing the underlying value and on the way out we're recreating the menu ID okay and from the host point of view then we want to Define that it owns multiple of these menu IDs because we want it to be Cascade deleted as well and to have the ownership relationship between the nested entities inside the algorithm so that's what we're doing over here and we have some definitions to have it looking like similar to what we have over here okay so this is the definition that we have I highly encourage that you pause the video when I finish this sentence and think why what we have over here doesn't work even though it compiles why when we try to create a migration this won't work and we'll get an exception okay and the answer is that because on the left we're defining the menu ID as a non-entity type but on the right we're defining it as an entity type so when we Define the menu entity configuration in anti-framel core then we're defining the ID over here to be a non-entity type but when we're defining the entity configuration for the host then we're defining it to be an entity type and this over here will throw an exception for some reason probably shouldn't be throwing a no reference exception but that's what actually happens okay so even though this is the recommendation of chai GPT and many other experts this doesn't actually work with anti-firmware core there's actually multiple GitHub issues open on this matter and this is considered something that's unsupported and it's not planned to be supported in entity firmware core so we can do it okay now how this is commonly solved in other projects is the following so this is my least favorite solution so what many projects do is they have over here in the domain layer the set of domain objects defined as the domain needs and then they have a duplicate set of domain objects in the infrastructure layer and then when they want to store things in the database then it goes through some conversion whether it's a manual mapping to the infrastructure object or using something like mapstore automaper to do the snapping okay now if you do this kind of duplication then you have both a set of objects for the request and the response in the presentation layer you have a set of requests and responses in the application layer for the commands and queries and then you have your set of objects in the domain layer and then you have another set of object over here in the infrastructure layer and it needs to go through mappings every time it goes between the layers this of course is a huge overhead and one of the main reasons why we wanted to use strongly type IDs in the first place is because we wanted to avoid this duplication when we refactor so great now we have even more duplication and instead of Simply changing all the goods to Strings once in a billion years when you change the type of the underlying ID then you need to now create another duplicate set of objects in the infrastructure layer maintain that maintain the mapping and that's that can introduce bugs much more than mismatches between the types because it's the IDS aren't strongly typed so I dislike the solution I think it's a huge Overkill to solve this problem but this is extremely common many many projects have another set of objects in the infrastructure layer okay the next solution is basically creating two IDs per ID so you have the ID that's used by the algorithm and this is the one that's mapped or configured as a non-ency type and you have the other one used by all the other Aggregates that are referencing the menu and that one will be configured as an entity type for all the tables okay if you're looking at this and you dislike it then you're not alone because what we want to do is we want to have less duplication and not more duplication also what happens when you want to take the menu ID from the menu and now use it for reference you now now you need to start maintaining also conversions between these two objects to mix and match them and have the equality and the translation from one to the other a as simple as possible throughout your code base so this is also in my opinion a huge overkill for the solution okay and moving on to the last option is simply reverting back to the Primitive types another common solution amateur I forgot to mention is using strongly typed IDs but instead of referencing other Aggregates by ID defining relationships via EF core this is something we want to avoid since we don't want changes to one aggregate to have side effects on other aggregates okay so let's take a look at what the underlying problem really is and if we can do something a bit tacky to solve this in an elegant way where we can have our configurations the way we would expect them to work and also not have to duplicate all our objects and have everything strongly typed have both the benefits of the strongly typed IDs and also the benefit of not having to duplicate all the code a thousand times in different layers okay so taking a look again at what we had before so we have the menu and the host and like we said the actual underlying problem is that the menu IDs used over here and the menu ID used by the aggregate root are both talking about the same underlying menu ID object right or type okay now if we take a step back and we look at all of our system as a whole then we can see that for all of our Aggregates the ID is going to be a a non-entity type and for all the referencing objects it's going to be a entity type okay so all we need to do is somehow when we're talking about the ID of the aggregate to have it be a different object not the menu ID object but rather a different object okay now once you look at it that way then you can see that the solution is actually pretty simple you can pause the video and think about it for a minute because I'm going to reveal it and three two one no so the menu ID instead of Simply being a value object then it's an aggregate root ID and what we want to do is we want to override the ID in the entity and every time we're talking about dot ID of the aggregate so every time we say menu dot ID then we're not talking about this ID over here which is the menu ID but we're talking about the aggregate root ID type which is a different type altogether and we don't mind that this type will be marked as a non-entity type because this is always going to be a column in the database configured as the primary key of the edit so that's it that's the solution it's that simple this is all we need to do so I want us to go to our buber dinner application take a look at how this actually pans out with all the configuration files and how everything comes into play together okay so I have our project open in Visual Studio code and let's see what we have now after what we talked about so in the common folder inside the domain layer then under models we have other than the aggregate root The Entity and the value object we have also the aggregate root ID which is a very simple object that extends the value object object and has over here the underlying value now what this means if we go to the menu ID we no longer inherit from the value object but we inherit from the aggregate root ID and we pass over here the underlying type that is used for the value and this is what we have over here then in the menu then all we need to do is instead of passing it only the menu ID then to pass also the underlying type as well this means that if we want to change the menu ID from being a guide to a string then we'll also have to go over here and change it to a string this is definitely a downside but that's the only other place that we'll have to do it then in the area route then we have over here the actual ID that is the one that will be referenced every time we use the ID so going to the menu configurations then we can see over here that we have the exact same configuration like we had in the previous video where we're simply referencing the ID but now instead of the ID in the entity base class then we're referencing the ID in the aggregate then for the host then we can have the exact same configuration like we had before where like you can see over here so we have owns many we have here the menu IDs and this works the same thing like before okay so that's all we have to do and now if we try creating a migration then we can see that the migration is created successfully and the tables look like we would expect when we imagined what it will look like similar to what we did in the previous video okay so the migration was created successfully let's take a look at what we have so we have the migrations and over here we can see the migration definition where we have here all the various tables and you can see this became quite long already with all the various types that we have but this works this compiles the migration is created successfully and really the change that we had to do in our domain layer is very small and I think it's pretty elegant given the constraints that we have where we basically don't need to touch the configurations the models we don't need to make any changes other than add that ID property in the aggregate route let me know what you think tell me if you like it or not and any comments or suggestions that you might have then feel free to leave them below and of course if you enjoyed this then make sure to smash that like button smash the Subscribe button and I'll see you in the next one
Info
Channel: Amichai Mantinband
Views: 24,767
Rating: undefined out of 5
Keywords:
Id: B3Iq346KwUQ
Channel Id: undefined
Length: 16min 15sec (975 seconds)
Published: Mon Apr 10 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.