Stop Using Records As Strongly Typed IDs!

Sdílet
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

Komentáře • 71

  • @r14958
    @r14958 Před rokem +4

    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.

    • @Codewrinkles
      @Codewrinkles  Před rokem +1

      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.

    • @r14958
      @r14958 Před rokem +2

      @@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. 😉

  • @MilanJovanovicTech
    @MilanJovanovicTech Před rokem +17

    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.

    • @Codewrinkles
      @Codewrinkles  Před rokem +5

      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.

    • @kyryllvlasiuk
      @kyryllvlasiuk Před rokem +1

      @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.

    • @Codewrinkles
      @Codewrinkles  Před rokem +3

      @@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!

    • @debtpeon
      @debtpeon Před rokem +1

      A record struct does structural comparison. In other words, you don't have to implement IEquatable for record structs.

    • @kumailn7662
      @kumailn7662 Před 6 měsíci

      @@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?

  • @Codewrinkles
    @Codewrinkles  Před rokem +11

    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.

    • @Dalet_
      @Dalet_ Před rokem +3

      You cannot conclude that one is faster than the other because the error margin is higher than the difference

    • @Robert-G
      @Robert-G Před rokem

      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

    • @Robert-G
      @Robert-G Před rokem

      otoh: using record classes for this seems to be crazy

  • @19balazs86
    @19balazs86 Před rokem +15

    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.

    • @Codewrinkles
      @Codewrinkles  Před rokem +4

      Nice to hear you used readonly record structs. Most people out there seem to be using regular records.

    • @sonicjoy2002
      @sonicjoy2002 Před 3 měsíci

      this is the way.

  • @fredimachadonet
    @fredimachadonet Před rokem +8

    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!

    • @Codewrinkles
      @Codewrinkles  Před rokem +1

      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.

    • @kyryllvlasiuk
      @kyryllvlasiuk Před rokem +4

      ​@@Codewrinkles record classes are classes. Record structs are structs.

    • @normalmighty
      @normalmighty Před rokem

      @@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.

    • @ThomasLevesque
      @ThomasLevesque Před rokem +1

      ​@@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.

    • @kyryllvlasiuk
      @kyryllvlasiuk Před rokem +1

      @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.

  • @krccmsitp2884
    @krccmsitp2884 Před rokem +5

    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.

    • @Codewrinkles
      @Codewrinkles  Před rokem

      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.

  • @andreasmewald2439
    @andreasmewald2439 Před rokem +4

    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

    • @Codewrinkles
      @Codewrinkles  Před rokem

      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.

  • @torrvic1156
    @torrvic1156 Před 3 měsíci

    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?

  • @fredimachadonet
    @fredimachadonet Před rokem +4

    Nice video Dan. Have you tried record struct?

    • @Codewrinkles
      @Codewrinkles  Před rokem +3

      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.

  • @nayanchoudhary4353
    @nayanchoudhary4353 Před rokem +2

    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.

    • @Codewrinkles
      @Codewrinkles  Před rokem +1

      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.

    • @kyryllvlasiuk
      @kyryllvlasiuk Před rokem +3

      ​@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

  • @TonoNamnum
    @TonoNamnum Před 10 měsíci

    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!

    • @Codewrinkles
      @Codewrinkles  Před 10 měsíci

      That's actually also a very good point. Thank you!

  • @steve-wright-uk
    @steve-wright-uk Před rokem +5

    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)

    • @fredimachadonet
      @fredimachadonet Před rokem

      It will be possible to alias any types in C# 12

    • @fredimachadonet
      @fredimachadonet Před rokem

      I'm not sure if it will serve the purpose of strongly typed IDs though. I hope so.

    • @Codewrinkles
      @Codewrinkles  Před rokem +1

      I'm also curious how aliases will work in C# 12. But your comment is very insightful and I agree.

    • @VoroninPavel
      @VoroninPavel Před rokem +1

      @@fredimachadonet aliases do not enforce type incompatibility =(. If you have several aliases for int, they are equivalent.

    • @andreasmewald2439
      @andreasmewald2439 Před rokem

      @@fredimachadonet Thought that you already can alias a type via the using statement. What am I missing?

  • @user-bx7xi7bu9k
    @user-bx7xi7bu9k Před rokem

    Great benchmark. The problem here we are loosing options for base/generic EntityIds because of Structs, dont support inheritance 🤔

    • @Codewrinkles
      @Codewrinkles  Před rokem +1

      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.

    • @user-bx7xi7bu9k
      @user-bx7xi7bu9k Před rokem

      @@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.

  • @evertonfa7
    @evertonfa7 Před rokem +2

    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;
    }

    • @Codewrinkles
      @Codewrinkles  Před rokem +3

      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 :)

  • @debtpeon
    @debtpeon Před rokem +1

    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.

  • @AceHack00
    @AceHack00 Před rokem +1

    How fast is with without strongly typed ids?

    • @Codewrinkles
      @Codewrinkles  Před rokem +1

      I think it doesn't really make sense to compare. It's like you would compare apples to bananas. Am I missing something?

  • @theyur
    @theyur Před rokem

    What about shadow copying every time when the struct is passed as parameter into a method? It is another kind of overhead.

    • @Codewrinkles
      @Codewrinkles  Před rokem +1

      As you said, I'm not sure it's really worth the hustle. But definitely an idea worth evaluating.

    • @jelle2819
      @jelle2819 Před rokem

      Use the in keyword for parameters and ref keywords for returning if this is a real concern in certain pathd

  • @dmzone64
    @dmzone64 Před 2 měsíci

    this plus the EF problems with the record, tell me to simply sod off...

  • @spirits_
    @spirits_ Před rokem

    Why would you need a strongly typed id that is not a composite id?

    • @MuharremGorkem
      @MuharremGorkem Před rokem +1

      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

  • @OzgulEzgin
    @OzgulEzgin Před rokem +1

    thank you for the great content.

  • @andersonhansen9542
    @andersonhansen9542 Před rokem

    An amazing addition to the strongly typed IDs discussion!!

  • @VoroninPavel
    @VoroninPavel Před rokem +1

    I think struct could be readonly struct.

  • @Marfig
    @Marfig Před 7 měsíci +7

    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.

  • @RenegadeVile
    @RenegadeVile Před rokem +1

    I only ever use record for DTOs sent to and from API's.

    • @Codewrinkles
      @Codewrinkles  Před rokem +2

      That's where also I use records most of the times.

  • @thygrrr
    @thygrrr Před 3 dny

    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?