Skip to content
\n

Motivation

\n

To avoid Primitive Obsession.

\n

In my domain, I want to make 100% sure that the domain ideas that I use are valid instances, so I use strongly-typed Value Objects, such as CustomerId instead of int.

\n

I would like the language to allow me to express these constraints and I would like to prohibit any mechanism that could evade validation, e.g. through the explicit use of default(CustomerId).

\n

Proposal

\n\n

Some examples:

\n

These are OK:

\n

a matching validate method with the same name and type of an invariant parameter

\n
public record struct Celsius(invariant float Value)\n{\n    private static void Validate(float Value) => Invariant.Ensure(Value >= -273, \"cannot be less than absolute zero\");\n}
\n
public record struct Celsius(invariant float Value)\n{\n    private static void Validate(float Value) => Invariant.Ensure(Value >= -273, \"cannot be less than absolute zero\");\n}
\n
public record struct WorkingAge(invariant int Value)\n{\n    private static void Validate(int Value) => Invariant.Ensure(Value >= 14 && Value <= 66);\n}
\n

These are not OK - resulting in compilation errors

\n

no corresponding Validate method found

\n
public record struct Celsius(invariant float Value);
\n

validate method matches on name, but has a different type

\n
public record struct Celsius(invariant float Value)\n{\n    private static void Validate(int Value) => Invariant.Ensure(Value >= -273, \"cannot be less than absolute zero\");\n}
\n

validate method matches no invariant parameter

\n
public record struct Celsius(invariant float Value)\n{\n    private static void Validate(int Age) => Invariant.Ensure(Age >0);\n}
\n

I imagine invariant records to be very simple types containing just one parameter, and will primarily be used for wrapping primitives.
\nIf it gets more complicated than that, then the Validation idea falls down when multiple parameters are accepted:

\n

compilation errors for methods with same signatures

\n
public record class Person(invariant string FirstName, string MiddleName, invariant string LastName)\n{\n    private static void Validate(string FirstName) => Invariant.Ensure(!string.IsNullOrEmpty(FirstName), \"must have a value\");\n    private static void Validate(string LastName) => Invariant.Ensure(!string.IsNullOrEmpty(LastName), \"must have a value\");\n}
\n

We could get around this by changing the Validate method to be an instance method which has access to the contents of the record:

\n
public record class Person(invariant string FirstName, string MiddleName, invariant string LastName)\n{\n    private void Validate() => \n            Invariant.Ensure(!string.IsNullOrEmpty(FirstName), \"must have a value\")\n            .Ensure(!string.IsNullOrEmpty(LastName), \"must have a value\");\n}
\n

But I would prefer to restrict it to just one parameter; composite types could be made up from individual invariant records, e.g.

\n
public record Employee(Name firstName, Name lastName, WorkingAge age);
\n

I've used Invariant.Ensure in this discussion. Here's what I imagine it might look like:

\n
public readonly struct Invariant\n{\n    public Invariant And(bool condition, [CallerArgumentExpression(\"condition\")] string? expression = null) =>\n        Ensure(condition, expression);\n    \n    private static Invariant Ensure(bool condition, [CallerArgumentExpression(\"condition\")]string? expression = null)\n    {\n        if (!condition) throw new Exception($\"Invariant failed, expected: {expression}\");\n\n        return default;\n    }\n}
\n

I envisage this to be used simply as a means to wrap primitives. I'm aware that using records might encourage or infer more complex usage, but I'm also aware of the complexities of introducing a new concept into the language, such as primitive, e.g.:

\n
public primitive CustomerId(int Value) ...
\n

... where a primitive is like a record but with 1, and only 1, parameter in the primary constructor and with a corresponding Validate method.

\n

I think anything that can be added to the language to make Value Objects easier to use and to steer people away from Primitive Obsession would be a good thing.

","upvoteCount":8,"answerCount":3,"acceptedAnswer":{"@type":"Answer","text":"

Support for validation is missing in records because the language team decided that such policy should not be built into the language, but provided through external means like source generators. This is why all of the members otherwise auto-generated by records can also be provided through explicit code in a partial class. I'd suggest that your source generator would remain the correct avenue to implement that logic.

","upvoteCount":0,"url":"https://github.com/dotnet/csharplang/discussions/5574#discussioncomment-1892488"}}}
Discussion options

You must be logged in to vote

Support for validation is missing in records because the language team decided that such policy should not be built into the language, but provided through external means like source generators. This is why all of the members otherwise auto-generated by records can also be provided through explicit code in a partial class. I'd suggest that your source generator would remain the correct avenue to implement that logic.

Replies: 3 comments 8 replies

Comment options

You must be logged in to vote
0 replies
Comment options

You must be logged in to vote
0 replies
Comment options

You must be logged in to vote
8 replies
@SteveDunn
Comment options

@HaloFour
Comment options

@SteveDunn
Comment options

@HaloFour
Comment options

Answer selected by SteveDunn
@mwisnicki
Comment options

@SteveDunn
Comment options

@mwisnicki
Comment options

@AroglDarthu
Comment options

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
6 participants