Stop Using Records As Strongly Typed IDs!
Vložit
- čas přidán 19. 06. 2024
- Strongly typed IDs is a very important concept, inspired from DDD. The main purpose is to eliminate primitives obsession, to give our code a better intent and to avoid misplacing identifiers. But what about performance? Virtually everybody talks about C# records as ideal for this purpose. But can we do better in terms of performance?
#dotnet #softwarearchitecture #ddd #software
Join this channel to get source code access and other perks:
/ @codewrinkles
Also follow me here (especially if you are a self taught developer):
✅My other channel: / @danpatrascutech
✅Facebook: / danpatrascutech
✅Instagram: / danpatrascutech
✅TikTok: / danpatrascutech
✅Newsletter: www.danpatrascu.tech/
Milan's video: • How Strongly Typed IDs...
Amichai's video: • The Identity Paradox |...
Content:
1. Intro: 00:00
2. Implementing strongly typed IDs: 01:21
3. Initial benchmark: 03:00
4. What if we use structs? 04:50
5. What if we use regular class? 07:21
6. How we explain this? 08:23
7. Don't use records! 09:24
My setup:
Camera - Canon EOS M50 Mark II: amzn.to/3SJxS4d
Lav mic - Rode Lavalier GO Professional: amzn.to/3mmZS1B
Condenser mic - Shure SM7B: amzn.to/3JaqjQN
Audio console - Rodecaster PRO II: amzn.to/3KTVMIg
Laptop - Dell Latitude: amzn.to/3KV4SEW
Monitors - Benq 27 inch: amzn.to/3JbM6aU
Lights - 2x Godox SL-60W: amzn.to/3KV3qCj - Věda a technologie
One thing that everyone seems to be overlooking is that one reason you use strongly typed classes (or record classes) for IDs is so that you can inherit from an abstract base generic class like EntityId. That way, it makes it much easier to switch ID types from Guids to longs or strings, even within the same domain. Structs and record structs can only inherit interfaces.
That's a good reason indeed. However, look at some earlier comments where I was backlashed drastically because I even dared to bring this into discussion.
@@Codewrinkles Yes, like all internet meeting places, CZcams certainly attracts opinionated people. Of course, if you entitle your piece using a directive with an exclamation mark, you better be prepared for some spirited rebuttals. 😉
Great comparison, and I really apprecaited the benchmarks. Having said that, I'd say records still beat structs in terms of simplicity. Having to implement IEquatable every time is annoying.
You also noted that the performance is on the level of microseconds. I have a lot of doubts that this will be the thing making or breaking your application. 😅
P.S. Would be great if you can also run the same benchmark for `readonly record struct` and post the results in a pinned comment? Many people mentioned that on my channel, and I see the same trend in your comments.
Thank you, @MilanJovanovicTech for taking your time and commenting. I appreciate that. However, I'm not sure exactly how, but i think there's a BIG misunderstanding. Where exactly did I say that structs beat records in terms of simplicity? OMG. Here's the quote from my video: "I know, there’s the downside that we need to implement structural equality, we need to make sure the strongly typed Ids are immutable and so on and so forth. But we’re engineers! We’d for sure abstract this in a base class, so we’d write this functionality once and it would be a fair trade."
I also don't agree with the statement in your second paragraph. You can get better performance (even if just slightly) by just using record struct or struct instead of record. You basically get some performance free of charge. I'm not sure that your argument really stands.
I have also updated the pinned comment with benchmark results.
@Codewrinkles wow, this is bs. You recommend to use structs over records for performance, and as answer to maintainability concern, you ask to use base class... for a struct... ok - do so if you are able.
If we go for record structs, we get the best of both approaches. If we go for base class, we will get the worst of both approaches. Welcome to virtual dispatch benchmarking. Records (struct or classes) are much easier to maintain, period.
@@kyryllvlasiuk I don't understand why you are so triggered. Sorry, but your comments are just hate with no valid arguments. IN my reply here I mentioned record struct first and struct second. And if you are so triggered agains base classes, then don't even create a ValueObject or Entity class.
Honestly, your attitude doesn't do you any good and it certainly doesn't prove your point! You just rage agains common programming practices because you just want to yell at me? Take it easy!
A record struct does structural comparison. In other words, you don't have to implement IEquatable for record structs.
@@Codewrinkles its a huge difference in term of performance a good video, performance matters if the numbers are that huge. But I would like compare on more features Records vs Struct. Here is a question where you suggest Records should be used? Its something .Net introduces for no reason?
There are also record structs that I didn't include in the video. Here are benchmarks on struct vs record struct:
| Method | Mean | Error | StdDev | Allocated |
|------------- |---------:|---------:|---------:|----------:|
| Struct | 18.73 us | 0.369 us | 0.395 us | 64 B |
| RecordStruct | 18.86 us | 0.374 us | 0.843 us | 64 B |
Just as it happened when we compared record (class) and regular class and the class was a tiny bit faster, it happens also in the case of struct vs record struct. The difference is negligalbe, yet in 10 runs, struct came always on top by a few fractions.
Also I was a little bit misleading but with no intention. It's not reflection in the sense that it iterates through fields and compares them. However, during the lowering process a lot of code gets created by the compiler and structural equality is implemented based on equality contract and comparers. I apologize for this error.
You cannot conclude that one is faster than the other because the error margin is higher than the difference
yeah, I think record struct, or a custom generic type with overridden GetHaschCode/Equals would be the obvious choice. Seeing record struct in code, vs the custom one would never be a deal breaker, IMO
otoh: using record classes for this seems to be crazy
Hi Dan, nice catch about the performance. Last time I used "readonly record struct" for strongly-typed IDs, but I didn't think about the performance implications. As you mentioned, it will perform better since it is a struct.
Nice to hear you used readonly record structs. Most people out there seem to be using regular records.
this is the way.
I actually just ran benchmarks similar to yours, and record struct ended up having the exact same performance as structs. I kind of prefer using record struct since it has the value equality built in, but as you mentioned, if we want to use struct we would probably have a base class anyway. Matter of preference I guess. Thanks for your content!
Yeah, I kind of lost sight of the record struct. That would be probably the best trade off. I'll run the tests later today also myself. I somehow struggle to believe that record structs perform exactly the same as structs. I'd expect a minor difference to exist, but I'd expect regular structs to always be just a tiny bit faster.
@@Codewrinkles record classes are classes. Record structs are structs.
@@kyryllvlasiuk But as we saw in the video, records had an extra overhead over normal classes. I personally would have expected there to be a similar minor cost added to record structs vs structs. Not enough to matter in 99% of cases, but I still would have expected it to show up on a benchmark.
@@Codewrinkles The generated constructor for a record struct is identical to the one that you would write yourself for a normal struct (just set the field). Since your benchmark doesn't actually cover anything other than the constructor, you should get the same results.
@normalmighty Yeah, I do have a question to his benchmark implementation. In many cases, IL for usage of classes and records is the same. He also did not implement all the functionality that records have. This has nothing to do with records being different from classes cause they are not. But they are more robust than his implementation. They are much easier to maintain.
The title is somewhat misleading, since the problem is not "record", but in being a reference type: "record" ist the same as "record class", whereas "record struct" would be more practical here.
Thank you for the feedback. I did think about your comment but I don't think it's misleading since most people tend to use regular records for this purpose.
Can you explain where the record uses reflection? I don't get it ...
The record class supports inheritance, therefore it includes a type equality check. The record struct doesn't generate this type of code, since a struct is sealed by default.
[CompilerGenerated]
protected virtual Type EqualityContract
{
[CompilerGenerated] get
{
return typeof (TestRecord);
}
}
And maybe I don't get the point, but aren't you benchmarking memory allocation? Is there a benchmark for equality? Of course a (record) struct should be faster, compared to a (record) class, if we're benchmarking on memory allocation.
Thx
Ok, probably I was a little bit misleading but with no intention. It's not reflection in the sense that it iterates through fields and compares them. However, during the lowering process a lot of code gets created by the compiler and structural equality is implemented based on equality contract and comparers. This process is still a tiny bit slower than what you implement directly. The benchmarks show this small difference between e class we implement and using a record.
Thanks for the info but what is the point of creating a separate class for Id property of the class? It complicates things and what it gives?
Nice video Dan. Have you tried record struct?
Actually, I forgot about that. Nice catch. However, I'm pretty sure that structs would still perform slightly better. The performance order would then be struct, record struct, class, record. Thanks for pointing this out.
You said that the record compares equality using reflection. In my experience, if you decompile the assembly which has your record, you should be able to see the generated code for equality check. I've seen it in VS 2022. So, I'm not convinced with your explanation for performance difference.
Appreciate you took your time to write this comment. I wrote about this in the pinned comment a few minutes after the video was published.
@Codewrinkles Did you remove it? I have seen no pinned comment. With so many flaws, it might be a good idea to re-upload video with corrections
Another important question is how fast you will need to generate ids. If you generate a lot of ids then it is good to keep this in mind. Thanks for the video!
That's actually also a very good point. Thank you!
I think the C# language is missing a trick. What we need is the ability to inherit from a primitive. For example:
alias int : productId
The compilor could then do the strongly typed checks, but the compiled code would fallback to being the "int". That way we get the best of both worlds, strongly typed checks in the code and the performance of primtives in the compiled code. (I'm sure somebody could come up with a better syntax)
It will be possible to alias any types in C# 12
I'm not sure if it will serve the purpose of strongly typed IDs though. I hope so.
I'm also curious how aliases will work in C# 12. But your comment is very insightful and I agree.
@@fredimachadonet aliases do not enforce type incompatibility =(. If you have several aliases for int, they are equivalent.
@@fredimachadonet Thought that you already can alias a type via the using statement. What am I missing?
Great benchmark. The problem here we are loosing options for base/generic EntityIds because of Structs, dont support inheritance 🤔
I am generall against the typed identifiers as they cause a lot of had aches and the need for weird workarounds when modeling complex relationships and when it comes to persistence. I know, when we design our domain according to DDD we shouldn't care about this. But we also need make practical choices. Please note, however, this doesn't mean that I say using typed identifiers is bad.
@@Codewrinkles Totally agree. Everything is relative and depends mainly on the context of the project. We just have to keep some openness in the design because the perfect model for all cases does not exist. Thanks again for the benchmark, it gives some ideas.
Nice video. In my projects I like to use the following approach:
public readonly record struct CustomerId(Guid Value)
{
public static implicit operator CustomerId(Guid value) => new(value);
public static implicit operator Guid(CustomerId customerId) => customerId.Value;
}
That's for sure a nice approach and I talked about implicit and explicit operators in some other video. I generally think they are cool, though in production code I'm a little bit reluctant to use them as they basically promote under the hood magic and in bigger and ever changing teams that might cause some problems in terms of udnerstanding what the code does. It's just an opinion/personal preference, not an objective truthe :)
Why didn't you implement a record struct? You don't have to implement IEquatable for record struct. A struct is a value type and with a record struct you are not allocating on the heap. It's on the stack and record struct does structural comparison.
How fast is with without strongly typed ids?
I think it doesn't really make sense to compare. It's like you would compare apples to bananas. Am I missing something?
What about shadow copying every time when the struct is passed as parameter into a method? It is another kind of overhead.
As you said, I'm not sure it's really worth the hustle. But definitely an idea worth evaluating.
Use the in keyword for parameters and ref keywords for returning if this is a real concern in certain pathd
this plus the EF problems with the record, tell me to simply sod off...
Why would you need a strongly typed id that is not a composite id?
Let's assume we use int (or long etc) for such IDs for multiple distinct entities. You can easily ask for Entity-Type-A from a repo etc. with Enty-Type-B id and you only catch this error at run-time (if you are lucky). What you want is compiler prevent you from making such mistakes at compile-time
thank you for the great content.
My pleasure!
An amazing addition to the strongly typed IDs discussion!!
I'm glad you think so. Thank you.
I think struct could be readonly struct.
Definitely!
Performance based decisions should always be contextual. The differences in speed that you show are meaningless in a lot of real production environments. I wish performance was better discussed. It pains me the care-free and unfocused attitude almost everyone seems to take about it. Do not just compare numbers to write catchy CZcams titles that have almost no value on their own and can in fact contribute to bad choices or useless conversations. Put those numbers in the context their are actually used. Then bring the discussion.
I only ever use record for DTOs sent to and from API's.
That's where also I use records most of the times.
Your test has NO workload to compare. Fwiw, rhe records/record structs are a tiny, insignificant bit of your programs runtime.
If 0.2% of your runtime becomes 1% of your runtime, then you will still have 99x times the positive effect spending your time optimizing literally anytning else.
Also, equality members are up to 10x faster for records/record structs, or so i hear. Maybe something is fishy here?