@@zoran-horvat I think it's just the general vibe to be honest :) it used to feel like a nice, laid-back lecture (and it was great), and now it feels smooth - the code appears swiftly and all the details and comments are displayed next to it. Both formats are great, but this one just feels fresh Keep posting please :)
mr. horvat your content is incredibly inspiring! i often rewatch your videos experienceing some kind of sudden ephiphany of education, enticing me to reiterate on my past solutions!!! so much that i recently became a patreon, thank you zoran
Great video as always. This was pretty much my journey with my product. I started off with some uint for the ID, than we decided to change it into a string and back into an int, I got fed up with changing the ID all over the project everything management decided to change things around -> So I wrapped it in a class UserIdentifier. That in time turned into a struct, and after your video about records I changed it into a record, which removed quite a bit of code from the Equality functions. It's a shame but I cannot use record structs in this project, so might have to revert it back to a struct. I really enjoy your videos and learn a lot from them, thank you!
I know the path you walked because that happened to me, too. Some critics don't realize that everything I say, I have pushed to production first at some time during my career. All this code just works.
Derived types (like in Ada) would be really useful for this purpose. They would be essentially just an int, only the compiler would tag them with additional type info, treating them as totally distinct types.
Love this idea but still can't find a good way to handle it in an application due to JSON, EF Core, API endpoints, etc. all needing converters, serialization methods, parsing, etc. Just a UserId wrapping a Guid, for example: - UserId must implement IParsable so it can be used in API endpoints. - UserId needs a UserIdJsonConverter so it isn't drawn as id: { value: "guid" } in JSON responses. - UserId needs a UserIdValueConverter so that EF Core can treat it like a Guid rather than an owned entity. And none of this works with inheritance, so you must do this for every ID in the domain, even if you use a ValueObject or StrongId base record.
@@tuckertcs Actually, it is not the duty of the domain model to serialize/deserialize itself, just like it is not its responsibility to persist itself.
@@zoran-horvat This is true, however it's generally expected, so whether you handle it in the domain layer or the persistence/presentation layers, it's still something you generally need to do. Additionally, many objects do handle this logic within themselves. Guid implements IParsable, so why shouldn't UserId?
Very good! I would however add to this video a section about serialization problems (Your key will be serialized as an object by default) and other reasons why you should use a library for doing this.
You should not burden the domain model with serialization. That should be the ability of the corresponding DTOs, and DTOs, conversely, should not burden with such domain problems as stringly-typed IDs. They should only contain primitive values, such as GUID, int, or long for the ID.
Hi Zoran, great content as always. What about making it generic? public readonly record struct StronglyTypedId(Guid Value) where T : notnull { public static StronglyTypedId Empty => new(Guid.Empty); public static StronglyTypedId New => new(Guid.NewGuid()); public override string ToString() { return Value.ToString(); } }
I see your point with this type, but I object that it is very abstract for the purpose. Using it almost looks like Yoda speak. If it is to save three lines of code per entity class, I'm not sure it is worth the mental effort associated with its use. Anyway, on first look, I'd say it would work fine.
Two questions: 1) Who's gonna automate that? 2) Do you tend to stringify models and leave their correctness to automated tests, with respect to question 1?
1. The Developer that writes the Feature. 2. Im with you on the DDD approach of introducing specific types rather than falling victim to primitive Obsession though. Just wanted to Point Out that If there is a Bug because someone passed the wrong value that should usually be caught by a unit or Integrationtest.
@@pinguincoder That's fine, so let's focus on point 1. There is one place to define a strong type and dozens where the assignments happen. You are advocating weak assignment checks, dismissing the most powerful static code analysis tool we have - the compiler - and request, say, writing 20 unit tests, plus 2 which you forget, as a better idea? Be so kind to explain to me the economy of that, excuse my language, lunacy.
Also, don’t create IMPLICIT operator overloads on Strongly Typed ID classes. If you do, have fun debugging. U can use Explicit, but I wouldn’t recommend implilcit ones.
Hello Zoran, thank you for this video. I wanted to apply this idea to a Guid based ID which would be serializable. Do you see any pitfalls to an approach which guards against empty GUIDs by always setting them to Guid.NewGuid if empty? I'm wondering whether this can lead to bugs in a context of JSON serialization. My reasoning for not allowing Guid.Empty is that the IDs are then not unique anymore and thus two objects can clash.
You could pass the object instead of the ID, you then don't have to overengineer a solution, as you already then have type safety. In DDD it is common practice to pass around objects and not Ids, this way you can perform other DDD functions whereas before you are limited to an ID.
I'm not following you. What object do you mean instead of the ID? Don't forget that any type you use as the identity must map to a primitive type supported by the database.
@@djdaveydave123consider a common situation where you get the id of a book from a client. you will then have to first fetch the entire book from the database only to pass it to the makeFamouse method?
I understand that for different entities, I would need to create different strongly typed Ids. So it means also to declare the Empty properties and NewId(). Any way to avoid rewriting each time? I guess compiler generated codes?
I use C# for game dev and don't find this applicable to my current project. That said, I am 100% with you on this, besides the already mentioned JSON annoyance.
The DTOs should contain primitive types, because their purpose is to transfer low-definition data. Therefore, the conversion to and from a DTO would also include unwrapping and wrapping the GUID into a strongly typed ID. This process is the same as for all other pieces of data in the DTO and it stems from the fact that the domain model is built around processes and DTOs are built around values. Putting both aspects into the domain model would cause lots of trouble everywhere, and hence we separate those.
Interesting idea -- what I get is I need to learn about records, since they don't exist in Java or C++ and I hadn't come across them in C# before -- so until now "record" was just what Pascal call a "struct" to me. Not that I have much immediate practical use for this.
Suppose that you needed access to the underlying Guid, what's the best way to cast it back into a Guid? Attempting to `(Guid)stronglyTypedId` results in 'CS0030: Cannot convert type 'stronglyTypedId' into 'System.Guid'.' Converting it to a `string` then back to a Guid feels wrong.
Conversion back and forth happens at the edges of the model. That is at the endpoint or UI, where it is transformed into a primitive type, often a string, or at the persistence layer where it is transformed into a type supported by the storage. For example, when using EF Core, you would define a value conversion that applies to a strongly typed ID.
Thx @@zoran-horvat, that's precisely where I'm having to do the conversion. It's a D365 integration piece, and I need to convert my model into an Entity, which takes a Guid as it's Id. So, right at the margin, or endpoint.
In what way does it make it more risky? I expect certain complications in serialization, mostly coming from syntax, but not from any ambiguity that could cause risks.
@@zoran-horvat Thank you for the reply and for the videos, always apreciated. Since this is not a transparent in-place update, care should be taken to test areas of the code where serialization/deserialization is used since the compiler cannot check for problems. One example is public API where responsens could be objects that use automatic mappers. If tests are present this should be easy to catch.
Maybe there could be a way to interpret the wrapped GUID struct as a GUID when it comes to serialization? Maybe an implicit conversion could be used from GUID to the new strongly typed identifier?
What we gain from this aproach? Developer cannot assign wrong id accidentally. Which can easily be detected by unit tests... Not much benefit + more complexity...
You said it all: "... easily detected by unit tests." Compilation is the static code analysis, whereas unit tests are dynamic analysis. SCA is always preferred for the simplest of all reasons: its conclusions are universally correct. Unit tests cannot achieve that. If unit tests were the answer, we would let go of compilers long ago.
isn't there an allocation for GUID? aren't you making a complicated wrapper for what is essentially an enum (yea i know it's not the same, but it's only the same because you cant define enums at runtime which would only be useful if you dont know all the enums at compile time)?
Wouldn't the Empty property bites us later on, given it could produce an object in an invalid state. I'm thinking of one of your previous lessons on never construct an object in an invalid state.
It is not invalid for an ID. That is a regular case when you request the database to populate the ID upon insert, and it is backed by EF configuration. On the other hand, if you set the ID value in the entity, EF will use that in the insert and not change it.
It's worth mentioning that guid created from c# application and used as primary key clustered index in relational database significantly decrease performance.
public readonly record struct BookId(Guid Value): > Model bound complex types must not be abstract or value types and must have a parameterless constructor. Record types must have a single primary constructor. Alternatively, give the 'Value' parameter a non-null default value. Ok apparently entity framework doesn't do that, let's just use a record then public record BookId(Guid Value): > InvalidOperationException: The entity type 'Book' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'. .... Ok, so just ignore the whole strongly typed ID thing and use int Id, got it.
@@zoran-horvat Previous was with a Sqlite db, so now I made it as simple as I possibly could. Memory database, and copy straight from the video, no trying to work it into my existing project. #1. New WebApp, .NET 8.0 Add nuget EFCore.InMemory and dependencies #2. builder.Services.AddDbContext(options => options.UseInMemoryDatabase("Test")); #3. public class MemoryContext : DbContext { public MemoryContext(DbContextOptions options) : base(options) { } public DbSet Books { get; set; } } public class Book { public readonly record struct BookId(Guid Value); public BookId Id { get; set; } = new(Guid.Empty); public string Title { get; set; } = string.Empty; private Book() { } } #4. Index page public void OnGet() { var books = _context.Books.ToList(); } # result: InvalidOperationException: Property 'Book.Id' cannot be used as a key because it has type 'BookId' which does not implement 'IComparable', 'IComparable' or 'IStructuralComparable'. Use 'HasConversion' in 'OnModelCreating' to wrap 'BookId' with a type that can be compared. There must be steps in the video that are being left out? Such as defining keys inside OnModelCreating?
@@zoran-horvat TH-cam eats my code snips so I'll just say I fixed it by first rewriting away from sqlite to inmemory, and then figuring that inmemory has its own issues so I had to write a value converter for the key to make it work. And because that became tedious very fast I wrote a generic identity type that I can just use on all the entities and then a modelbuilder extension hack to put in a converter easily. It's ugly but it works. Sort of. Why is code just problems on top of problems on top of more problems?
Strongly typed IDs are like any other type when it comes to serialization. You have to unpack the primitive values before serialization and package them back after deserialization. The problem you are experiencing probably originates in the attempt to serialize a domain model, which is a wrong step in any settings. You should serialize DTOs instead.
Interesting. But what if I need to place Books and Publishers to different APIs? Books aggregate and Publisher aggregate will be in different projects. And then it'll be difficult to use PublisherId in Books aggregate. I need to declare that type again.
What is the advantage of using strongly typed anything in your code base other than not making an obvious error? Why use a compiled language other than letting the compiler report obvious errors?
@@zoran-horvatWe can't and shouldn't strongly type everything, only strongly type the things that matter. We use strings instead of enums in many places in our code because not all strings need to be strongly typed. We weigh the effort vs the benefit first. Saying everything should be strongly typed "just because" is not a good reason to do it. Efficiency isn't just in our code, it's the time spent doing something vs the benefit of the said thing. I was curious if there was any other reason to do this and the answer seems to be no.
@@zoran-horvatbut we do because even though a string may only contain a set of known values there's no reason to make them enums unless they need to be.
@@auronedgevicks7739 I guess you do. It's not clear who you refer to as "we", though. Those I worked with in the last 10+ years are not your "we", I know that with confidence.
How do you go about removing the exception that is thrown when you use entity.HasKey(e => e.Id); entity.HasComplexProperty(e => e.Id);? It appears EF doesn't support complex PK's although it looks like you have at least one in your example.
Nice video! However, I wonder if this is not too much overengineering for such a small problem. Extrapolating the idea, why not creating strongly types for every other property of the Book and Author objects which are primitives and where have the same risk of passing an incorrect value?
I'd say that you can push this idea to any length, and then it comes to a judgment when to stop. IDs are pervasive in entities design - every entity has one. If you knew the Web application I added side by side with this isolated demo application, you would know that turning only the three IDs into strongly typed IDs caused changes in 20-30 other places in the model and UI. Even in the smallest of all applications, with only two web pages, every ID is used in a dozen of places to render those two pages! You can imagine how many thousands of instances that would be in a large application. That makes this particular detail a highly cost-effective candidate for refactoring, a much better one than any other part of the design.
As with everything, philosophy matters. If I'm going to cut a bit of wood a saw will do, it I'm building a cabinet, a sophisticated set of precise tools is required. Such tools use the same primative ideas. However, their design trades versatility for precision. I think it's important to realise he does this for the domain model (the most important code), such an approach to all the accidental complexity within an application would lead to a frustrated customer.
I'm 100% with you on that topic. The only thing I dislike is that if you want to export a model with System.Text.Json you will get an "ugly" Json from the Json-perspective because of unnecessary nesting. Should I then make a copy of that model only for Json because of that one field?
That is a good question. I might try to come up with a good answer at some time later. Normally,, I avoid direct serialization/deserialization of domain models because that process would be ridden with issues anyway. I usually add an intermediate step, converting the model object into a DTO first, to make it serialization-friendly, among other things.
@@tobias989 It is not just the ID. Serializing a domain model, whose primary reason to exist is to model business processes, will always be connected to issues of all kinds. We normally use DTOs to support data-only operations and isolate the model from those duties.
@@zoran-horvatThat is a good point. The DTO can contain directly the GUID or a String representing the BookId. With this approach no custom JsonConverter is needed.
@@tobias989 The Json-Converter is no option for our projects because it's incompatible with source generation (trimming, aot, etc.). If our domain models are acceptable to serialize then it feels so unnecessary to introduce duplicate code only because we want to introduce strong id's
@2:47 I paused for a moment here to ponder, why wouldn't we just create an Author, Book tuple table / entity? I often get stuck on things and can't move on. So I'm posting my question now and I will try to answer it myself in a reply later.
Okay, so I think the answer is that for the purposes of this demo it doesn't matter. If you're stuck here get passed it. Yes you should probably have a Tuple table. Just keep watching and it will make sense. It's about strongly typing IDs which can help to reduce runtime issues that the compiler won't prevent.
What are your thoughts on using “real-world” IDs. e.g. ISBN* for the ID of the book entity/table. Employee number for Users (or UserName). Invoice Number for invoices, etc. I don’t know enough about these numbers to know if this would work, it is really just an example.
@@JonathanPeel Natural keys are good in communication, URLs, etc. but they are not ideal as identifiers. Most notably, they might change for an existing entity for various reasons.
I dont get it. Id mistakes happen in intermediate layer mostly. Strong id types protects from making mistakes in domain layer, but it still posible to make DomainAClass.(DomainAClassID(DtoBClass.ID)) mistake in binder )
@@zoran-horvat and what if specification of GUID changes ? whats a point to make non template identity entity. you probably want to test domain run on different identity system. ddd on c# sucks.. crappy language
Perfect example of "the juice isn't worth the squeeze" that pushed me away from OO/DDD. Yes, you are solving a real problem, but the cost of the solution is higher than the cost of the problem. Not just the cognitive load, but now i'll almost certainly need adaptors, generics, etc to use this. Just like I can pass 'first name' (a string) into a function that is meant for formatting 'last name', its not worth creating FirstName and LastName types to solve that problem. The more pragmatic solution is If you really need to restrict it to the type, pass in the whole object and not just the id.
There is nothing to add to your code base after this. The solution is complete, there is absolutely nothing to add to it. No adapters, generics, nothing. Check it out yourself.
@@andreibicu5592 What is the "whole object" and what do you mean by "pass the whole object"? If you are referring to the strongly typed ID, it is a value, implemented as a struct. It is passed around the same way as you pass a GUID or an int, and it is an object to the same extent. It is just a value.
There must be a value mapping for that to work. I will publish the next video soon, showing how strongly typed IDs work with EF Core and SQL Server. They work fine.
This approach of using strongly typed identities elevates identity management from a simple necessity to a core practice in software development, promoting higher code quality and reliability across various domains.
Why can't the MakeThisBookFamous method take a Book instead of a UUID and then we wouldn't even have that problem to solve? In your example, MakeThisBookFamous may be doing too much. I mean, it doesn't only make a book famous, but it also fetches a book from a database... which can fail... We're kinda mixing some internal business logic about how to make books famous with how books are persisted in our system... I don't understand... :(
You have already answered your question. It is one of the methods that receives the ID and must load the object from the database. Every deserialization point, endpoint, controller and whatnot is precisely in that situation. Don't forget that in any model of a significant complexity you won't be loading all the objects until it becomes clear which object is the right subject. Most of the methods in an application would instead have access to numerous IDs, only sometimes deciding to materialize an ID into an object.
Through DTOs that remove mappings induced by the model, including those on IDs. A DTO would contain a plain GUID again, the same way as the database and UI already do.
You missed the point. The number generated by the runtime is unique, and even referred to as identity. The fact it is used in GetHashCode doesn't change that.
Thanks for the video. The strongly typed Ids is a valid idea for protecting against mix of ids. But Ids in the domain entities is a design smell. They are persistence logic implementation details that have no relation to your domain. Use straight references instead. So "Book Book" instead of "Guid BookId".
You cannot use the Book at the outer parts of the system. The UI and network will only communicate the ID. Also, it is not realistic that you will have numerous entities fully populated when only working with one of them. It is reasonable to move only the references to other entities around in operations that don't deal with them directly, not to load the entire objects in every operation.
Ids are for the application layer. Not the domain model. The domain entities refer to each other by type. Regarding loading of the object graph: That is why you have "Lazy loading". That is one of the main benefits of NHibernate and EF.
@@AndersBaumann That looks like a religious view on DDD and I cannot go much into depth commenting it. As an engineer, I know that I should not be querying a dozen times via lazy loading only to obtain a dozen other entities I don't plan to apply the current command to. That makes the models and all operations on them bloated with navigation properties nobody actually uses, all in the name of a very abstract goal of not seeing an ID, like ID is a separate concern. The original works by Eric Evans had defined IDs and explained their role. I agree with that explanation, it is correct. Eric has later even explained why entities cannot implement equality and yet I see everybody around start an entity by implementing its equality, even before defining its attributes. That is so wrong, as many other practices we see today are.
Actually referencing entities that do not belong to each other is a code smell. Good luck to your code complexity and perfomance with lazy loading. Ids are fine. There are many many other "trade-offs" when it comes to DDD and micro-services communication, where certain domain classes use things, that typically belong to infrastructure. Even tho I dont like Zoran's code style as much, this video is a really really good advice that makes code easier to read/write.
There is a foreign key reference so obviously they belong to each other. "Code complexity"? With ids you cannot unit test your domain model in isolation and you will move domain logic into the application layer. Seems like something that could quickly turn ugly if you are more than a few developers.
If you have a complex system with huge number of entities heaving a complex type to represent identity for each entity separately you will come up with huge number of small classes. But again, this is something related to big monolith projects. DDD on the other side tries to force you to split things in small bounded-contexts with relatively small number of entities. @@zoran-horvat
You can't imagine how short-sighted that view is. If the compiler is already doing the static code analysis, then why not let it tell the bugs back through the same process? You sound like you lost a bit of pride if you didn't do it yourself, do you? I remember an event when 70 people died and a certain programmer expressed that same attitude towards the guy who made the critical bug. Yet, the issue could have been prevented by the .NET runtime, if only it were in place.
In my last large project we have spent a lot of time to remove this garbage and replace with generic logic in many places of code. It decrease memory consumption like 1,5 gb in every of 4 solutions and increase development speed for 3 times.
@@zoran-horvat you change int ids (or guid) to structs with values. this have it's consuption. We had for every objects(not only ids) legacy for every sub entities (some was structs, some classes). remove this approach help to decrease consuption (as i said before). on of reason was that we have very large caches (for calculation reasons). so some services decrease from 6 gb to 4.5. the main questions is that benefit of this approach is very arguable
@@1235663 Stepping from int to GUID is driven by other reasons, as int is not sufficient to index the objects we use today, nor does it work in distributed systems, unless the price is paid in redesigning the system in other ways. Therefore, int or GUID is the false dilemma. After we get to using the long key, that is it. Whether it is primitive or a strong type doesn't matter. The amount of memory used in either case is identical, to a byte. If you tell me that you have reduced the amount of memory used by 25% by only shortening the ID, then where are the data besides the ID? I mean, there must be a reference to each object as well, that is another 1.5GB. What are you telling me, that one half of your database are the keys? That is either not true or your service is one in a thousand and whatever conclusions you make from it are irrelevant in the general case. Your numbers just don't add up.
So you're writing more code because you didn't check what you were doing or name the parameter something useful like bookId? No wonder you got it wrong
@@zoran-horvat per Id per Entity. And it's a lot more than 3 lines. I know because I managed to persuade the team to not overengineer solutions because of primitive envy.
@@TheDiggidee Not per ID per entity but one per entity, located in the same file with the entity, for clarity. That does not qualify as overengineering. The solution is adapted to static code analysis like so many other parts of the model and even language syntax.
@zoran-horvat yes it is per I'd per entity. Put the damn code wherever you want. Doesn't make it cleaner or solve the problem that you put the wrong id in. Just makes it clearer that you did. This problem could be solved by writing a unit test and double checking your work. The entire problem is down to bad naming conventions and not checking. Therefore it's overengineering because you're doing more work when you could just check.
Apart from the content, which is always great, Zoran’s commentary and humour crack me up😂
Thanks for going into WHY strongly typed IDs are good
The advice bears repeating -- thanks for the continous flow of golden nuggets
Thanks!
Really enjoying your new style of videos, thanks!
What is the new style, what did I change? If it's good, I want to do more of it!
@@zoran-horvat You didn't show your face during the video 😄 But that could not be the reason^^
@@zoran-horvat I think it's just the general vibe to be honest :) it used to feel like a nice, laid-back lecture (and it was great), and now it feels smooth - the code appears swiftly and all the details and comments are displayed next to it.
Both formats are great, but this one just feels fresh
Keep posting please :)
@@MC_DarkMaster haha, cant be that ;)
Amazing story telling and practical programming techniques that work
mr. horvat your content is incredibly inspiring! i often rewatch your videos experienceing some kind of sudden ephiphany of education, enticing me to reiterate on my past solutions!!! so much that i recently became a patreon, thank you zoran
Great video as always.
This was pretty much my journey with my product.
I started off with some uint for the ID, than we decided to change it into a string and back into an int, I got fed up with changing the ID all over the project everything management decided to change things around -> So I wrapped it in a class UserIdentifier. That in time turned into a struct, and after your video about records I changed it into a record, which removed quite a bit of code from the Equality functions. It's a shame but I cannot use record structs in this project, so might have to revert it back to a struct.
I really enjoy your videos and learn a lot from them, thank you!
I know the path you walked because that happened to me, too. Some critics don't realize that everything I say, I have pushed to production first at some time during my career. All this code just works.
Really engaging video, genuinely a fun watch! Nice stuff!
Derived types (like in Ada) would be really useful for this purpose. They would be essentially just an int, only the compiler would tag
them with additional type info, treating them as totally distinct types.
A wrapper type essentially behaves like a derived type.
Nice format, and great lesson. Thank you!
Wonderful explanation, thank you kindly!
Love this idea but still can't find a good way to handle it in an application due to JSON, EF Core, API endpoints, etc. all needing converters, serialization methods, parsing, etc.
Just a UserId wrapping a Guid, for example:
- UserId must implement IParsable so it can be used in API endpoints.
- UserId needs a UserIdJsonConverter so it isn't drawn as id: { value: "guid" } in JSON responses.
- UserId needs a UserIdValueConverter so that EF Core can treat it like a Guid rather than an owned entity.
And none of this works with inheritance, so you must do this for every ID in the domain, even if you use a ValueObject or StrongId base record.
@@tuckertcs Actually, it is not the duty of the domain model to serialize/deserialize itself, just like it is not its responsibility to persist itself.
@@zoran-horvat This is true, however it's generally expected, so whether you handle it in the domain layer or the persistence/presentation layers, it's still something you generally need to do.
Additionally, many objects do handle this logic within themselves. Guid implements IParsable, so why shouldn't UserId?
Very good! I would however add to this video a section about serialization problems (Your key will be serialized as an object by default) and other reasons why you should use a library for doing this.
You should not burden the domain model with serialization. That should be the ability of the corresponding DTOs, and DTOs, conversely, should not burden with such domain problems as stringly-typed IDs. They should only contain primitive values, such as GUID, int, or long for the ID.
Hi Zoran, great content as always.
What about making it generic?
public readonly record struct StronglyTypedId(Guid Value)
where T : notnull
{
public static StronglyTypedId Empty => new(Guid.Empty);
public static StronglyTypedId New => new(Guid.NewGuid());
public override string ToString()
{
return Value.ToString();
}
}
What would T be? Any class?
I see your point with this type, but I object that it is very abstract for the purpose. Using it almost looks like Yoda speak. If it is to save three lines of code per entity class, I'm not sure it is worth the mental effort associated with its use.
Anyway, on first look, I'd say it would work fine.
@@edgeofsanitysevensix I think T is the class where the Id is for. So in the Book class you would define a StronglyTypedId Id ...
Thats what Automated Tests are for so even if you are passing the wrong Id you should have an automated test asserting the expected outcome
Two questions: 1) Who's gonna automate that? 2) Do you tend to stringify models and leave their correctness to automated tests, with respect to question 1?
1. The Developer that writes the Feature.
2. Im with you on the DDD approach of introducing specific types rather than falling victim to primitive Obsession though. Just wanted to Point Out that If there is a Bug because someone passed the wrong value that should usually be caught by a unit or Integrationtest.
@@pinguincoder That's fine, so let's focus on point 1. There is one place to define a strong type and dozens where the assignments happen. You are advocating weak assignment checks, dismissing the most powerful static code analysis tool we have - the compiler - and request, say, writing 20 unit tests, plus 2 which you forget, as a better idea? Be so kind to explain to me the economy of that, excuse my language, lunacy.
This is excellent!
Also, don’t create IMPLICIT operator overloads on Strongly Typed ID classes. If you do, have fun debugging. U can use Explicit, but I wouldn’t recommend implilcit ones.
Interesting. I was wondering what would be the difference in using a readonly struct instead of a readonly record struct
Sounds like a good-old D&D tale)
Really clever solution 👍🏻
Hello Zoran, thank you for this video. I wanted to apply this idea to a Guid based ID which would be serializable.
Do you see any pitfalls to an approach which guards against empty GUIDs by always setting them to Guid.NewGuid if empty?
I'm wondering whether this can lead to bugs in a context of JSON serialization.
My reasoning for not allowing Guid.Empty is that the IDs are then not unique anymore and thus two objects can clash.
@@juliusdeblaaij2534 If you wrapped the GUID into a record, that record could ensure that the value is never empty.
Top stuff, makes sense...
You could pass the object instead of the ID, you then don't have to overengineer a solution, as you already then have type safety.
In DDD it is common practice to pass around objects and not Ids, this way you can perform other DDD functions whereas before you are limited to an ID.
I'm not following you. What object do you mean instead of the ID?
Don't forget that any type you use as the identity must map to a primitive type supported by the database.
MakeFamous(Book book)
@@djdaveydave123 Where did you get the Book object from?
@@djdaveydave123consider a common situation where you get the id of a book from a client. you will then have to first fetch the entire book from the database only to pass it to the makeFamouse method?
How do I host a composite list of 2 Types of IDs? So the List can contain BookID and AuthorID
I understand that for different entities, I would need to create different strongly typed Ids. So it means also to declare the Empty properties and NewId(). Any way to avoid rewriting each time? I guess compiler generated codes?
I use C# for game dev and don't find this applicable to my current project. That said, I am 100% with you on this, besides the already mentioned JSON annoyance.
@@Cool-Game-Dev This is definitely not the topic that applies in game modeling.
Instead of entity framework, I’d like to see how you would make this work with cosmos db client library directly.
Hi Zoran. Great video! What about when you have a DataTransferObject, say BookDto, in your API?
The DTOs should contain primitive types, because their purpose is to transfer low-definition data. Therefore, the conversion to and from a DTO would also include unwrapping and wrapping the GUID into a strongly typed ID.
This process is the same as for all other pieces of data in the DTO and it stems from the fact that the domain model is built around processes and DTOs are built around values. Putting both aspects into the domain model would cause lots of trouble everywhere, and hence we separate those.
Interesting idea -- what I get is I need to learn about records, since they don't exist in Java or C++ and I hadn't come across them in C# before -- so until now "record" was just what Pascal call a "struct" to me. Not that I have much immediate practical use for this.
There is a previous video I made that explains C# records and their use in depth.
Suppose that you needed access to the underlying Guid, what's the best way to cast it back into a Guid? Attempting to `(Guid)stronglyTypedId` results in 'CS0030: Cannot convert type 'stronglyTypedId' into 'System.Guid'.' Converting it to a `string` then back to a Guid feels wrong.
Conversion back and forth happens at the edges of the model. That is at the endpoint or UI, where it is transformed into a primitive type, often a string, or at the persistence layer where it is transformed into a type supported by the storage.
For example, when using EF Core, you would define a value conversion that applies to a strongly typed ID.
Thx @@zoran-horvat, that's precisely where I'm having to do the conversion. It's a D365 integration piece, and I need to convert my model into an Entity, which takes a Guid as it's Id. So, right at the margin, or endpoint.
I like the idea but how on earth do you handle nullable foreign key types in your entity framework configuration class?
EF Core supports that out of the box. I did it in one of the videos, I think.
@@zoran-horvat hmmm I can't get my code to compile, I'll see if I can track the video down
Oh crikey, simple as that... countryId!.Value.Value I missed the last .Value LOL!
@@andyhb1970 Yes, I remember it was easy.
I assume that this impacts JSON serialization and could make a project upgrade from Guid to record structs a bit risky if serialization is used.
In what way does it make it more risky?
I expect certain complications in serialization, mostly coming from syntax, but not from any ambiguity that could cause risks.
@@zoran-horvat Thank you for the reply and for the videos, always apreciated. Since this is not a transparent in-place update, care should be taken to test areas of the code where serialization/deserialization is used since the compiler cannot check for problems. One example is public API where responsens could be objects that use automatic mappers. If tests are present this should be easy to catch.
@@mihaiga I see your point. I'll keep it in the back of my mind for a while. Maybe something will come out.
Maybe there could be a way to interpret the wrapped GUID struct as a GUID when it comes to serialization? Maybe an implicit conversion could be used from GUID to the new strongly typed identifier?
What we gain from this aproach?
Developer cannot assign wrong id accidentally. Which can easily be detected by unit tests... Not much benefit + more complexity...
You said it all: "... easily detected by unit tests."
Compilation is the static code analysis, whereas unit tests are dynamic analysis. SCA is always preferred for the simplest of all reasons: its conclusions are universally correct. Unit tests cannot achieve that.
If unit tests were the answer, we would let go of compilers long ago.
A readonly record struct is a thing? Hmn, that could prevent some Heap churn.
isn't there an allocation for GUID? aren't you making a complicated wrapper for what is essentially an enum (yea i know it's not the same, but it's only the same because you cant define enums at runtime which would only be useful if you dont know all the enums at compile time)?
Guid is a value type. There is no allocation for it.
@@zoran-horvat oh, ok, thats interesting
Wouldn't the Empty property bites us later on, given it could produce an object in an invalid state. I'm thinking of one of your previous lessons on never construct an object in an invalid state.
It is not invalid for an ID. That is a regular case when you request the database to populate the ID upon insert, and it is backed by EF configuration. On the other hand, if you set the ID value in the entity, EF will use that in the insert and not change it.
Love it !
It's worth mentioning that guid created from c# application and used as primary key clustered index in relational database significantly decrease performance.
Yes, that is why the index should not be clustered in that case.
Could you make a video about Rules Engine Pattern? And envolve it with Specification Pattern
Possibly yes.
public readonly record struct BookId(Guid Value):
> Model bound complex types must not be abstract or value types and must have a parameterless constructor. Record types must have a single primary constructor. Alternatively, give the 'Value' parameter a non-null default value.
Ok apparently entity framework doesn't do that, let's just use a record then
public record BookId(Guid Value):
> InvalidOperationException: The entity type 'Book' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'.
....
Ok, so just ignore the whole strongly typed ID thing and use int Id, got it.
Wrong.
@@zoran-horvat
Previous was with a Sqlite db, so now I made it as simple as I possibly could. Memory database, and copy straight from the video, no trying to work it into my existing project.
#1.
New WebApp, .NET 8.0
Add nuget EFCore.InMemory and dependencies
#2.
builder.Services.AddDbContext(options => options.UseInMemoryDatabase("Test"));
#3.
public class MemoryContext : DbContext {
public MemoryContext(DbContextOptions options) : base(options) { }
public DbSet Books { get; set; }
}
public class Book {
public readonly record struct BookId(Guid Value);
public BookId Id { get; set; } = new(Guid.Empty);
public string Title { get; set; } = string.Empty;
private Book() { }
}
#4. Index page
public void OnGet() {
var books = _context.Books.ToList();
}
# result:
InvalidOperationException: Property 'Book.Id' cannot be used as a key because it has type 'BookId' which does not implement 'IComparable', 'IComparable' or 'IStructuralComparable'. Use 'HasConversion' in 'OnModelCreating' to wrap 'BookId' with a type that can be compared.
There must be steps in the video that are being left out? Such as defining keys inside OnModelCreating?
@@zoran-horvat
TH-cam eats my code snips so I'll just say I fixed it by first rewriting away from sqlite to inmemory, and then figuring that inmemory has its own issues so I had to write a value converter for the key to make it work.
And because that became tedious very fast I wrote a generic identity type that I can just use on all the entities and then a modelbuilder extension hack to put in a converter easily.
It's ugly but it works. Sort of.
Why is code just problems on top of problems on top of more problems?
How would this mirror to the database? Wouldn't it generate a separate table for the BookIds?
No, there must be the value mapping back to plain GUID and that would be the row key in the database.
When my class is serialized to JSON I am getting Id : { 'Value' : 1} instead of Id : 1. Is there a way to fix this?
Strongly typed IDs are like any other type when it comes to serialization. You have to unpack the primitive values before serialization and package them back after deserialization.
The problem you are experiencing probably originates in the attempt to serialize a domain model, which is a wrong step in any settings. You should serialize DTOs instead.
Interesting. But what if I need to place Books and Publishers to different APIs? Books aggregate and Publisher aggregate will be in different projects. And then it'll be difficult to use PublisherId in Books aggregate. I need to declare that type again.
Both APIs should contain the types they are using.
so ultimately what's the advantage of a strongly typed guid other than not making an obvious error?
What is the advantage of using strongly typed anything in your code base other than not making an obvious error?
Why use a compiled language other than letting the compiler report obvious errors?
@@zoran-horvatWe can't and shouldn't strongly type everything, only strongly type the things that matter. We use strings instead of enums in many places in our code because not all strings need to be strongly typed. We weigh the effort vs the benefit first. Saying everything should be strongly typed "just because" is not a good reason to do it. Efficiency isn't just in our code, it's the time spent doing something vs the benefit of the said thing. I was curious if there was any other reason to do this and the answer seems to be no.
@@auronedgevicks7739 We don't use strings instead of enums, though I can imagine some programmers do.
@@zoran-horvatbut we do because even though a string may only contain a set of known values there's no reason to make them enums unless they need to be.
@@auronedgevicks7739 I guess you do. It's not clear who you refer to as "we", though. Those I worked with in the last 10+ years are not your "we", I know that with confidence.
How do you go about removing the exception that is thrown when you use entity.HasKey(e => e.Id); entity.HasComplexProperty(e => e.Id);? It appears EF doesn't support complex PK's although it looks like you have at least one in your example.
There must be the value mapping on that property. It works well with EF, I'll prepare a separate video for that.
Excellent. Many thanks.
Nice video! However, I wonder if this is not too much overengineering for such a small problem. Extrapolating the idea, why not creating strongly types for every other property of the Book and Author objects which are primitives and where have the same risk of passing an incorrect value?
why extrapolating? 😊
I'd say that you can push this idea to any length, and then it comes to a judgment when to stop. IDs are pervasive in entities design - every entity has one.
If you knew the Web application I added side by side with this isolated demo application, you would know that turning only the three IDs into strongly typed IDs caused changes in 20-30 other places in the model and UI.
Even in the smallest of all applications, with only two web pages, every ID is used in a dozen of places to render those two pages! You can imagine how many thousands of instances that would be in a large application. That makes this particular detail a highly cost-effective candidate for refactoring, a much better one than any other part of the design.
As with everything, philosophy matters. If I'm going to cut a bit of wood a saw will do, it I'm building a cabinet, a sophisticated set of precise tools is required. Such tools use the same primative ideas. However, their design trades versatility for precision. I think it's important to realise he does this for the domain model (the most important code), such an approach to all the accidental complexity within an application would lead to a frustrated customer.
I'm 100% with you on that topic. The only thing I dislike is that if you want to export a model with System.Text.Json you will get an "ugly" Json from the Json-perspective because of unnecessary nesting. Should I then make a copy of that model only for Json because of that one field?
That is a good question. I might try to come up with a good answer at some time later.
Normally,, I avoid direct serialization/deserialization of domain models because that process would be ridden with issues anyway. I usually add an intermediate step, converting the model object into a DTO first, to make it serialization-friendly, among other things.
Add a custom JsonCoverter for this Type.
@@tobias989 It is not just the ID. Serializing a domain model, whose primary reason to exist is to model business processes, will always be connected to issues of all kinds. We normally use DTOs to support data-only operations and isolate the model from those duties.
@@zoran-horvatThat is a good point. The DTO can contain directly the GUID or a String representing the BookId. With this approach no custom JsonConverter is needed.
@@tobias989 The Json-Converter is no option for our projects because it's incompatible with source generation (trimming, aot, etc.).
If our domain models are acceptable to serialize then it feels so unnecessary to introduce duplicate code only because we want to introduce strong id's
@2:47 I paused for a moment here to ponder, why wouldn't we just create an Author, Book tuple table / entity? I often get stuck on things and can't move on. So I'm posting my question now and I will try to answer it myself in a reply later.
Okay, so I think the answer is that for the purposes of this demo it doesn't matter. If you're stuck here get passed it. Yes you should probably have a Tuple table. Just keep watching and it will make sense.
It's about strongly typing IDs which can help to reduce runtime issues that the compiler won't prevent.
Right but now I'm wondering... Does Book ID become an entity? Hence have it's own table?
What are your thoughts on using “real-world” IDs.
e.g. ISBN* for the ID of the book entity/table.
Employee number for Users (or UserName).
Invoice Number for invoices, etc.
I don’t know enough about these numbers to know if this would work, it is really just an example.
@@JonathanPeel Natural keys are good in communication, URLs, etc. but they are not ideal as identifiers. Most notably, they might change for an existing entity for various reasons.
/ *reads Bertrand Meyer, the Book* /
I dont get it. Id mistakes happen in intermediate layer mostly. Strong id types protects from making mistakes in domain layer, but it still posible to make DomainAClass.(DomainAClassID(DtoBClass.ID)) mistake in binder )
The next video which is about to go live shows binding, too.
@@zoran-horvat and what if specification of GUID changes ? whats a point to make non template identity entity. you probably want to test domain run on different identity system.
ddd on c# sucks.. crappy language
@@alexeybeloushko7240 Are you more arrogant or wrong?
@@zoran-horvat about what? be more specific
Does it work with entity framework and mssql?
It works with EF Care on SQL Server. I suppose that would make it work with MySql, too.
@@zoran-horvat Gonna need to test this out! I see this as an really good solution for generic apprach to diferent types of ID's!
Perfect example of "the juice isn't worth the squeeze" that pushed me away from OO/DDD.
Yes, you are solving a real problem, but the cost of the solution is higher than the cost of the problem. Not just the cognitive load, but now i'll almost certainly need adaptors, generics, etc to use this. Just like I can pass 'first name' (a string) into a function that is meant for formatting 'last name', its not worth creating FirstName and LastName types to solve that problem.
The more pragmatic solution is If you really need to restrict it to the type, pass in the whole object and not just the id.
There is nothing to add to your code base after this. The solution is complete, there is absolutely nothing to add to it. No adapters, generics, nothing. Check it out yourself.
Why would you pass the whole object just for one property ? This is not recommended anywhere.
@@andreibicu5592 What is the "whole object" and what do you mean by "pass the whole object"? If you are referring to the strongly typed ID, it is a value, implemented as a struct. It is passed around the same way as you pass a GUID or an int, and it is an object to the same extent. It is just a value.
@@zoran-horvat My comment was a reply to what @adam said. I totally agree with you.
@@andreibicu5592 Ah, sorry, it looked to me as the same commenter after a glance... My bad.
I did a someid(int Value) and it said could not be mapped because the database provider does not support this type. (Sql Server)
Greate idea though.
There must be a value mapping for that to work. I will publish the next video soon, showing how strongly typed IDs work with EF Core and SQL Server. They work fine.
@@zoran-horvatAwesome! Looking forward to it.
This approach of using strongly typed identities elevates identity management from a simple necessity to a core practice in software development, promoting higher code quality and reliability across various domains.
gpt
Why can't the MakeThisBookFamous method take a Book instead of a UUID and then we wouldn't even have that problem to solve? In your example, MakeThisBookFamous may be doing too much. I mean, it doesn't only make a book famous, but it also fetches a book from a database... which can fail... We're kinda mixing some internal business logic about how to make books famous with how books are persisted in our system... I don't understand... :(
You have already answered your question. It is one of the methods that receives the ID and must load the object from the database. Every deserialization point, endpoint, controller and whatnot is precisely in that situation.
Don't forget that in any model of a significant complexity you won't be loading all the objects until it becomes clear which object is the right subject. Most of the methods in an application would instead have access to numerous IDs, only sometimes deciding to materialize an ID into an object.
how to push this concept down to api?
Through DTOs that remove mappings induced by the model, including those on IDs. A DTO would contain a plain GUID again, the same way as the database and UI already do.
this is exactly what I don't want. how to make request from client to not mix ids.
@@Kubkochan Invent a new JSON format. That is easy.
00:42 A hash code, as per definition, does not qualify as identifier. Uniqueness is not guaranteed.
You missed the point. The number generated by the runtime is unique, and even referred to as identity. The fact it is used in GetHashCode doesn't change that.
Thanks for the video.
The strongly typed Ids is a valid idea for protecting against mix of ids.
But Ids in the domain entities is a design smell. They are persistence logic implementation details that have no relation to your domain. Use straight references instead. So "Book Book" instead of "Guid BookId".
You cannot use the Book at the outer parts of the system. The UI and network will only communicate the ID.
Also, it is not realistic that you will have numerous entities fully populated when only working with one of them. It is reasonable to move only the references to other entities around in operations that don't deal with them directly, not to load the entire objects in every operation.
Ids are for the application layer. Not the domain model. The domain entities refer to each other by type.
Regarding loading of the object graph: That is why you have "Lazy loading". That is one of the main benefits of NHibernate and EF.
@@AndersBaumann That looks like a religious view on DDD and I cannot go much into depth commenting it. As an engineer, I know that I should not be querying a dozen times via lazy loading only to obtain a dozen other entities I don't plan to apply the current command to. That makes the models and all operations on them bloated with navigation properties nobody actually uses, all in the name of a very abstract goal of not seeing an ID, like ID is a separate concern.
The original works by Eric Evans had defined IDs and explained their role. I agree with that explanation, it is correct. Eric has later even explained why entities cannot implement equality and yet I see everybody around start an entity by implementing its equality, even before defining its attributes. That is so wrong, as many other practices we see today are.
Actually referencing entities that do not belong to each other is a code smell.
Good luck to your code complexity and perfomance with lazy loading.
Ids are fine.
There are many many other "trade-offs" when it comes to DDD and micro-services communication, where certain domain classes use things, that typically belong to infrastructure.
Even tho I dont like Zoran's code style as much, this video is a really really good advice that makes code easier to read/write.
There is a foreign key reference so obviously they belong to each other.
"Code complexity"? With ids you cannot unit test your domain model in isolation and you will move domain logic into the application layer. Seems like something that could quickly turn ugly if you are more than a few developers.
This can lead to class proliferation problem
I think it outweighs primitive obsession problem =P
How?
If you have a complex system with huge number of entities heaving a complex type to represent identity for each entity separately you will come up with huge number of small classes.
But again, this is something related to big monolith projects. DDD on the other side tries to force you to split things in small bounded-contexts with relatively small number of entities. @@zoran-horvat
This is called overengineering, ladies and gents
But of course.
Omg, write a lot of waste code for developers, who can’t read the name of method ((
You can't imagine how short-sighted that view is.
If the compiler is already doing the static code analysis, then why not let it tell the bugs back through the same process? You sound like you lost a bit of pride if you didn't do it yourself, do you?
I remember an event when 70 people died and a certain programmer expressed that same attitude towards the guy who made the critical bug. Yet, the issue could have been prevented by the .NET runtime, if only it were in place.
In my last large project we have spent a lot of time to remove this garbage and replace with generic logic in many places of code. It decrease memory consumption like 1,5 gb in every of 4 solutions and increase development speed for 3 times.
@@1235663 You didn't watch the video, did you?
What memory consumption are you talking about? It is identical before and after, to a byte.
@@zoran-horvat you change int ids (or guid) to structs with values. this have it's consuption. We had for every objects(not only ids) legacy for every sub entities (some was structs, some classes). remove this approach help to decrease consuption (as i said before). on of reason was that we have very large caches (for calculation reasons). so some services decrease from 6 gb to 4.5. the main questions is that benefit of this approach is very arguable
@@1235663 Stepping from int to GUID is driven by other reasons, as int is not sufficient to index the objects we use today, nor does it work in distributed systems, unless the price is paid in redesigning the system in other ways. Therefore, int or GUID is the false dilemma.
After we get to using the long key, that is it. Whether it is primitive or a strong type doesn't matter. The amount of memory used in either case is identical, to a byte.
If you tell me that you have reduced the amount of memory used by 25% by only shortening the ID, then where are the data besides the ID? I mean, there must be a reference to each object as well, that is another 1.5GB. What are you telling me, that one half of your database are the keys? That is either not true or your service is one in a thousand and whatever conclusions you make from it are irrelevant in the general case. Your numbers just don't add up.
So you're writing more code because you didn't check what you were doing or name the parameter something useful like bookId? No wonder you got it wrong
Three lines you count as more code but checking what you are doing is for free.
You know better than that.
@@zoran-horvat per Id per Entity. And it's a lot more than 3 lines. I know because I managed to persuade the team to not overengineer solutions because of primitive envy.
@@TheDiggidee Not per ID per entity but one per entity, located in the same file with the entity, for clarity. That does not qualify as overengineering. The solution is adapted to static code analysis like so many other parts of the model and even language syntax.
@zoran-horvat yes it is per I'd per entity. Put the damn code wherever you want. Doesn't make it cleaner or solve the problem that you put the wrong id in. Just makes it clearer that you did. This problem could be solved by writing a unit test and double checking your work. The entire problem is down to bad naming conventions and not checking. Therefore it's overengineering because you're doing more work when you could just check.