Validating Records in C#: Everything You Ever Wanted To Know but Had No One To Ask

แชร์
ฝัง
  • เผยแพร่เมื่อ 30 พ.ย. 2024

ความคิดเห็น • 62

  • @Mig440
    @Mig440 8 หลายเดือนก่อน +8

    I think there is one escape hatch for bugs to come in with your example of NonEmptyString. The copy constructor automatically generated by the compiler allows for an expression like the following to both compile and run without exceptions thrown:
    var invalidNonEmptyString = nonEmptyString with { Value = ""}
    This should perhaps be discussed how to proceed in this case to prevent such programming errors from propagating.

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน +4

      You are right. I might add an analysis of this case in a later video. It is very interesting.

    • @goldmund67
      @goldmund67 8 หลายเดือนก่อน

      I think you already addressed that scenario in your recent video about records by making the Value a read only property set to the Value from the positional constructor parameter?

    • @Mig440
      @Mig440 8 หลายเดือนก่อน +1

      @@goldmund67
      That is exactly what zoran did here, but that wont resolve the problem because the copy constructor does not use the property initializer as shown in the video.

    • @Mig440
      @Mig440 8 หลายเดือนก่อน

      The fundamental issue seems to be that because it is impossible to get at the new value of the encapsulated string

  • @nocturne6320
    @nocturne6320 8 หลายเดือนก่อน +7

    Great and helpful video, especially the idea of NonEmptyStrings :)
    One thing to note is that at 13:50 the PositiveMoney example could've used a private static method for validation of the parameter, it would keep the code cleaner by not cluttering the primary constructor with validation one-liners and it would also allow for more complex validation to take place, if it had to span multiple lines

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน +4

      That is a good observation.

  • @jonobaker
    @jonobaker 8 หลายเดือนก่อน +3

    Really appreciate your videos, and I find them really handy for passing to colleagues, thank you.

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน

      Glad you like them! I appreciate your support.

  • @emadabushofa2379
    @emadabushofa2379 8 หลายเดือนก่อน +1

    I hope that there is an easy way to tell the json serializers how to serialize these custom types

  • @Buutyful
    @Buutyful 8 หลายเดือนก่อน +2

    hey Zoran, i was looking on youtube for a good source of truth about how to implement ef relationships but i didnt find much! i'd love some content on that and i think many other could benefit from it :D !

  • @ahjsbkdjhavkcjhvac
    @ahjsbkdjhavkcjhvac 8 หลายเดือนก่อน +2

    amazing video Mr. Zoran!! very insightful!!

  • @johncerpa3782
    @johncerpa3782 8 หลายเดือนก่อน +7

    It will hurt but you asked for it 😂😂

  • @kostasgkoutis8534
    @kostasgkoutis8534 8 หลายเดือนก่อน +2

    Wow, the Currency record struct was a landmine 💣.

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน

      Yes, it is :)

  • @matheosmattsson2811
    @matheosmattsson2811 8 หลายเดือนก่อน

    7:06 row 3-7 had me confused for a bit. Primary constructors... :P The use of the variable "Value" was ambiguous to my eyes, but not to the compiler. It is used both in the primary constructor and as a parameter name, in the same exact form. I prefer using lowercase parameter names in the primary constructor to avoid using the exact same name and capitalization as in the corresponding Property name. This only though if the Record has a body, if it lacks a body I would opt for capitalized parameter names.
    Good video!

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน +1

      It is standard to capitalize parameters in the record's primary constructors because they are also properties. But I agree that it is confusing.

  • @gavincampbell1061
    @gavincampbell1061 8 หลายเดือนก่อน +6

    Having init on the Value of the NonEmptyString record allows the validation to be bypassed.
    new NonEmptyString("valid") { Value = string.Empty }; will not cause the exception to be thrown and the Value will be empty.

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน

      Yes, you are right. There is also the impact on with expressions that should be evaluated.

    • @gavincampbell1061
      @gavincampbell1061 8 หลายเดือนก่อน +1

      @@zoran-horvat If init is removed, that prevents with being used as Value is now read only, and prevents Value being set in the initializer list. It's a possible fix.

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน +5

      That is what I had in mind. I might spare part of some future video to analyse and demonstrate those effects. Thank you for the reminder!

    • @franciscorusso8062
      @franciscorusso8062 8 หลายเดือนก่อน

      @@gavincampbell1061 I would do it this way. It would enforce the validation on every initialization case and avoid nondestructive mutation that could bypass the validation. From my point of view, nondestructive mutation in a value doesn't make any sense, if you want a new value, just create a new one from scratch

  • @MGinx
    @MGinx 8 หลายเดือนก่อน

    Very informative videos, thank you.
    I would be curious as to your thoughts on using records in option pattern and if you would even consider using it (given the numerous ways options can be binded and configured) and if so, would you still take the same approach to apply validation?

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน

      Records are useful in options, though I would refrain from letting the records validate the options.

  • @ghevisartor6005
    @ghevisartor6005 6 หลายเดือนก่อน

    Sorry but i lack a bit of experience on this topic. Are the models like Person and Tag supposed to be on their own or can they be used as complex types for EF core?
    I guess they are the model that is core tho the app and it is then used to populate entities?
    For example if i try to use them and migrate EF fail since it cant bind Name and Surname to properties on the Person type, i think there are workarounds for it but is logically wrong?

    • @zoran-horvat
      @zoran-horvat  6 หลายเดือนก่อน

      Records are not of great use with EF Core because they are immutable by default. Making them mutable requires the same level of effort as writing a common class, which defeats the idea of records.
      Records are primarily used in functional designs, where they become the principal design element together with delegates and static methods.

    • @ghevisartor6005
      @ghevisartor6005 6 หลายเดือนก่อน +1

      @@zoran-horvat thanks! i'm dealing with Blazor and making a non trivial model, thanks to some of your courses i realized that much of my code is procedural rather than oop or functional, i'm slowly trying to move to away from it, but it's kinda hard to fit it all together

  • @Alguem387
    @Alguem387 8 หลายเดือนก่อน

    besides default there is the empty struct constructor witch is always public, you can only get rid of it with an attribute [Obsolete(true, "There be dragons here")]

  • @szymonlisiecki4452
    @szymonlisiecki4452 8 หลายเดือนก่อน

    Great video!
    How would you handle the situation when the validity of the record depends on the relationship within its fields? Example:
    record SchoolStage(SchoolType schoolType, short gradeNumber);
    A school stage representing the 6th grade of primary school is valid, but the 6th grade of high school shouldn't be. Should this validation with all potential cases verification be implemented in the constructor?

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน +2

      Why not have a discriminated union with one type per school type?

  • @Cool-Game-Dev
    @Cool-Game-Dev 7 หลายเดือนก่อน

    Another way you could turn strings to non empty strings could be an extension method. Like Name.NonEmptyString();

    • @zoran-horvat
      @zoran-horvat  7 หลายเดือนก่อน

      You cannot turn a null or empty string to a non-empty one, unless the caller supplies the non-empty value with the call.

  • @dave_s_vids
    @dave_s_vids 8 หลายเดือนก่อน +1

    The first rule of Record validation club is don’t

  • @AndersBaumann
    @AndersBaumann 8 หลายเดือนก่อน

    Using exceptions in validation code will throw a lot of exceptions in a large system. They will clutter the application monitoring and hide the actual problems. In addition, exceptions are expensive.
    Invalid user input is expected.

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน

      The constructor cannot return any results, hence exceptions and assertions are the only way to indicate error. Error returns are possible from smart constructors and factory methods.

    • @AndersBaumann
      @AndersBaumann 8 หลายเดือนก่อน +2

      Make the constructor private and add a static factory method that returns a Result.

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน

      @@AndersBaumann That's what I said. However, that would make 90% of the audience leave this video before the end, which, in turn, is why I have made the video in smart constructors separately.

    • @AndersBaumann
      @AndersBaumann 8 หลายเดือนก่อน

      @@zoran-horvat OK. But it would be good to mention the tradeoffs.

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน

      @@AndersBaumann Sorry, not in this video. The audience is too picky and I'm not making videos to scare them away...

  • @alzaimar
    @alzaimar 2 หลายเดือนก่อน

    This doesn't apply to value objects in DDD which are usually declared as records.

    • @zoran-horvat
      @zoran-horvat  2 หลายเดือนก่อน

      @@alzaimar What do you mean it doesn't apply?

  • @cyrilmorcrette
    @cyrilmorcrette 2 หลายเดือนก่อน

    After months I finally came to understand what a 'Record' is. An ubuesque horror for the average C# programmer-with-their-feet, the kind that overrides == and .Equals() in CLASSES to make them behave like structs ... and far less performant than the default 64-bits reference check. The kind that overrides == in a class Line (holding an array of vectors) so that it checks if the two entire vector arrays of two Line instances are identical one vertex at a time instead of thinking about other, much cheaper means to check for equality. (Many ways to do that, that usually demand just a little thoughtful preliminary data structuring.)

    • @zoran-horvat
      @zoran-horvat  2 หลายเดือนก่อน

      @@cyrilmorcrette That is not how Equals is implemented. Arrays are compared by reference, not by content.

    • @cyrilmorcrette
      @cyrilmorcrette 2 หลายเดือนก่อน

      ​@@zoran-horvat This was attempted humor. What I meant is overriding == with a more expensive equality check is the king's way to performance issues. As opposed to overriding it after thoughtful data (re-)structuring to perform cheaper uint32 or even uint16 index checks. Not saying it's never needed, but most of the time this looks like bad coding to me and 'records' like the 'institutionalization' of that bad coding.

    • @zoran-horvat
      @zoran-horvat  2 หลายเดือนก่อน

      @@cyrilmorcrette Equality by content, a.k.a. value-typed semantics, must compare content. There is no shortcut to that.
      Indexing you mention is usually based on calculating a hash code - again from content. But indexing invariably ends up in a full comparison of content. How else can you confirm that two objects are equal?

    • @cyrilmorcrette
      @cyrilmorcrette 2 หลายเดือนก่อน

      ​@@zoran-horvat Example: are two Line instances equal by content? public static bool operator ==(Line a, Line b) => a.vertices[0] == b.vertices[0] && a.vertices[1] == b.vertices[1]; There is just two tacit requirements here : 1. there is no existing segment duplicate anywhere (and there won't be), 2. a line has its vertices always ordered one of the two possible ways (by the extremity smaller by x then by y then by z, for instance). Then one can go further and index all existing lines' first segments somewhere and have an extremely cheap uint32 index-based equality check, even cheaper than the default ReferenceEquals sometimes when the compiler likes you. Of course this requires preliminary data (re-structuring), but better make your data consistent and not fucked up than define some complicated and very expensive equality checks so your software is still running in spite of it.

    • @cyrilmorcrette
      @cyrilmorcrette 2 หลายเดือนก่อน

      ​@@zoran-horvat NB : More precisely regarding indexation of Line instances based on content, in the Line class : private static Dictionary _indexer = new(); private uint _index; public Vector[] vertices; public Line(IList iList) { vertices = iList[0].x >= iList[^1].x && iList[0].y >= iList[^1].y && iList[0].z >= iList[^1].z ? Enumerable.Reverse(iList).ToArray() : iList; _indexer.TryAdd((vertices[0], vertices[1]), _indexer.Count); _index = _indexer((vertices[0], vertices[1])); } public static bool ==(Line a, Line b) => a._index == b._index; Now, I'm not sure why you would check for equality by content between class instances in the first place. If they're equal by content, why do you allow for both of them to exist at the same time? I use the preceding kind of code very often for in procedural geometry to determine if something exists by content ... to avoid a duplicate or conflicting instanciation.) Each time I tried using records since I watched your video a few months ago, I ended up concluding that it would make the processes both running slower and less stable.

  • @johncerpa3782
    @johncerpa3782 8 หลายเดือนก่อน +1

    Great video

  • @7th_CAV_Trooper
    @7th_CAV_Trooper 8 หลายเดือนก่อน

    Maybe I don't get the point of record then. I think you're saying they're value objects. But why would I model value in a garbage collect heap object?
    Love that behavior by inheritance idea.

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน +1

      I think the confusion is between value typed and value objects. A record can either be a value type (record struct) or a reference type (record class). When you only state 'record' it is a record class, i.e. a reference type.
      Value object is a pattern regarding a type which implements value-typed semantics (by overriding Equals and GetHashCode in C#). A value object can either be a value type or a reference type.

  • @unisonrul1171
    @unisonrul1171 8 หลายเดือนก่อน +1

    Very good

  • @Buutyful
    @Buutyful 8 หลายเดือนก่อน

    should you have the same approach with classes also?

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน +1

      Classes tend to model larger parts of the domain and hence we often rely on heavy constructor validation and even on external validators.
      However, as more and more of the functional programming practices are finding their place in C#, the impact of small, composable models, such as those represented with records, becomes more visible and more widely applied in domain modeling.

  • @MasterKrzychuu
    @MasterKrzychuu 7 หลายเดือนก่อน

    I think this short's title is misleading. The with keyword didn't evade validation, because there is nothing to evade.
    The only validation is on the property initializer, none on the setter. You can "evade" it the same way by writing new NonEmptyString("Hi") { Value = "" };, even if it's a normal class - nothing to do with record or the with keyword.
    You exposed an init setter with no validation, so you predictably get undesirable results when you use it. (You correctly explain the solution in the video, but I don't see why you point out records or with in particular, when it's not caused by either thing).

    • @zoran-horvat
      @zoran-horvat  7 หลายเดือนก่อน

      The short video was about with expressions specifically, not about classes with init setters. The rest is with expression's implementation imposed by the compiler.
      It is certainly possible to introduce validation in the init setter, including the explicit backing field but... will anybody ever do that? In a record - no.
      Hence the conclusion - with expressions evade the primary constructor and therefore evade validation; disable with expressions for properties of primitive types and force all changes through the primary constructor.

  • @lazarnikolic8161
    @lazarnikolic8161 8 หลายเดือนก่อน

    What are your thoughts on using records in tandem with other immutable structures for modeling the entire domain, if the goal is a fully immutable model? Would you say it is still inadvisable? Achieving this with "standard" language features is, of course, painful. @zoran-horvat

    • @zoran-horvat
      @zoran-horvat  8 หลายเดือนก่อน +3

      There are usually no obstacles to modelling an entire business domain using only immutable structures and types. That is what we normally do in any pure functional language.
      There are two areas that require special attention. CPU-bound algorithms often require mutability, or otherwise their runtime performance may be prohibitively low. Take sorting as an example: There is no functional sorting algorithm, only the procedural ones. We address this problem by wrapping mutable/procedural algorithms into functions that satisfy the norms of functional programming on the outside. The other problem is persistence, where we must approach the mutable database differently compared to what we do in object-oriented code based on mutable entities.
      However, these issues do not make functional programming any less potent than object-oriented. They make it different, but that alone is not an issue.

    • @lazarnikolic8161
      @lazarnikolic8161 8 หลายเดือนก่อน +2

      ​@@zoran-horvatThank you for a detailed reply. Yes, the reason I ask is precisely because I'm actively trying to apply a more functional approach to C# programming, and although I found that domain modeling using records does get a bit awkward, it is far superior to class-based immutability.
      I'm obviously talking about business applications, where performance isn't as critical of a factor, LINQ does the sorting, and persistence is usually delegated to a dedicated class-based model anyway.