46

With the new record type of C# 9, how is it possible to inject a custom parameter validation/ null check/ etc during the construction of the object without having to re-write the entire constructor?

Something similar to this:

record Person(Guid Id, string FirstName, string LastName, int Age)
{
    override void Validate()
    {
        if(FirstName == null)
            throw new ArgumentException("Argument cannot be null.", nameof(FirstName));
        if(LastName == null)
            throw new ArgumentException("Argument cannot be null.", nameof(LastName));
        if(Age < 0)
            throw new ArgumentException("Argument cannot be negative.", nameof(Age));
    }
}
Liam
  • 27,717
  • 28
  • 128
  • 190
Simon Mattes
  • 4,866
  • 2
  • 33
  • 53
  • 5
    You don't need null checks if you use nullable types. If you want to validate parameters upon construction use a custom constructor instead of the generated one. Beyond that validation works the same as any other class, eg through data annotations, validators etc – Panagiotis Kanavos Nov 11 '20 at 10:14
  • 1
    I'm not sure what this has to do with record types? – Liam Nov 11 '20 at 10:16
  • 5
    @PanagiotisKanavos You still need to add null checks when using NRT's -- people can still pass in `null`, e.g. from nullable-unaware code, or by using `!` – canton7 Nov 11 '20 at 10:18
  • 3
    Note that `with { .. }` only works for properties which are `init`, so if you define your own constructor you'll lose the ability to use withers – canton7 Nov 11 '20 at 10:20

6 Answers6

16

I'm late to the party, but this might still help someone...

There's actually a simple solution (but please read the warning below before using it). Define a base record type like this:

public abstract record RecordWithValidation
{
    protected RecordWithValidation()
    {
        Validate();
    }

    protected virtual void Validate()
    {
    }
}

And make your actual record inherit RecordWithValidation and override Validate:

record Person(Guid Id, string FirstName, string LastName, int Age) : RecordWithValidation
{
    protected override void Validate()
    {
        if (FirstName == null)
            throw new ArgumentException("Argument cannot be null.", nameof(FirstName));
        if (LastName == null)
            throw new ArgumentException("Argument cannot be null.", nameof(LastName));
        if (Age < 0)
            throw new ArgumentException("Argument cannot be negative.", nameof(Age));
    }
}

As you can see, it's almost exactly the OP's code. It's simple, and it works.

However, be very careful if you use this: it will only work with properties defined with the "positional record" syntax (a.k.a. "primary constructor").

The reason for this is that I'm doing something "bad" here: I'm calling a virtual method from the base type's constructor. This is usually discouraged, because the base type's constructor runs before the derived type's constructor, so the derived type might not be fully initialized, so the overridden method might not work correctly.

But for positional records, things don't happen in that order: positional properties are initialized first, then the base type's constructor is called. So when the Validate method is called, the properties are already initialized, so it works as expected.

If you were to change the Person record to have an explicit constructor (or init-only properties and no constructor), the call to Validate would happen before the properties are set, so it would fail.

EDIT: another annoying limitation of this approach is that it won't work with with (e.g. person with { Age = 42 }). This uses a different (generated) constructor, which doesn't call Validate...

Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • This appears to be the most-efficient "workaround" to do it for now -- thanks a lot mate! – Simon Mattes May 20 '22 at 06:29
  • 1
    This have the same issue as the one using init and initializers above, though, in that it doesn't do validation when using the `with` syntax to create a new instance. But... there is no way to do that without writing the properties full yourself anyway. – Lasse V. Karlsen May 23 '22 at 07:44
  • @LasseV.Karlsen good point, I didn't think of this! – Thomas Levesque May 24 '22 at 08:09
  • _"positional properties are initialized first, then the base type's constructor is called. So when the Validate method is called, the properties are already initialized, so it works **as expected**."_ - on the contrary, I didn't **expect** this unusual constructor behaviour at all - prior to `record` types (AFAIK) it's _always_ been the case that a derived class' constructor's parameter arguments cannot contact `this` until after the superclass constructor completes - I didn't know the CLR even let you do that, so this interests me.... – Dai Aug 03 '22 at 16:23
  • @Dai maybe I didn't express myself clearly. When I said "works as expected", I meant "the validation method works". But yes, it's a bit surprising... – Thomas Levesque Aug 04 '22 at 17:16
15

You can validate the property during initialization:

record Person(Guid Id, string FirstName, string LastName, int Age)
{
    public string FirstName {get;} = FirstName ?? throw new ArgumentException("Argument cannot be null.", nameof(FirstName));
    public string LastName{get;} = LastName ?? throw new ArgumentException("Argument cannot be null.", nameof(LastName));
    public int Age{get;} = Age >= 0 ? Age : throw new ArgumentException("Argument cannot be negative.", nameof(Age));
}

https://sharplab.io/#gist:5bfbe07fd5382dc2fb38ad7f407a3836

Aik
  • 3,528
  • 3
  • 19
  • 20
  • 5
    Unfortunately this still requires duplicating the record type's members' names and types (as params and as properties), which kinda defeats one of the main points for using `record` types in the first place: succinctness. – Dai May 13 '22 at 03:42
  • 1
    @Dai, with classes, you would write more lines in the constructor to set all of the properties. With this approach, however, you only need to write extra lines for properties you want to check for null. _Btw, you can create a snippet for it._ – Artemious May 13 '22 at 16:10
  • 3
    This is not a correct answer, as the validation wont happen when utilizing the new `with` expression. See the following tests: https://gist.github.com/C0DK/1df531dc5d8bbde6faffe2dc887cd4ef – Casper Bang May 23 '22 at 06:52
  • 2
    @CasperBang Well, there is no way how to override `with` expression as it is emits simple assignment. This is why the example doesn't define setter for the property and it effectively disable `with` statement for getter only properties. – Aik May 24 '22 at 13:20
8

The following achieves it too and is much shorter (and imo also clearer):

record Person (string FirstName, string LastName, int Age, Guid Id)
{
    private bool _dummy = Check.StringArg(FirstName)
        && Check.StringArg(LastName) && Check.IntArg(Age);

    internal static class Check
    {
        static internal bool StringArg(string s) {
            if (s == "" || s == null) 
                throw new ArgumentException("Argument cannot be null or empty");
            else return true;
        }

        static internal bool IntArg(int a) {
            if (a < 0)
                throw new ArgumentException("Argument cannot be negative");
            else return true;
        }
    }
}

If only there was a way to get rid of the dummy variable.

Batox
  • 574
  • 1
  • 4
  • 16
  • 7
    See String.IsNullOrEmpty – Etienne Charland Dec 01 '21 at 12:10
  • 2
    You should also be passing the parameter name to the exceptions. For C# 10, consider using the [CallerArgumentExpressionAttribute](https://learn.microsoft.com/en-gb/dotnet/api/system.runtime.compilerservices.callerargumentexpressionattribute?view=net-5.0). – Richard Deeming Dec 01 '21 at 12:19
  • 1
    This is really clever, and avoids the need to add all those explicit properties. Thanks – Avrohom Yisroel Dec 07 '21 at 15:48
  • Also, you could actually move common validation methods into a separate `record` and have `Person` inherit from it, which would make the definition of `Person` a bit shorter. – Avrohom Yisroel Dec 07 '21 at 16:13
  • Please do not treat it as a complaint, but mimicking Kotlin features without thinking further about real life use cases makes C# more and more awful. Its like inventing the try without a catch – Michael P Mar 08 '22 at 11:09
  • 2
    @AvrohomYisroel Inheritance should not be abused as a Poor Man's substitute for mixins. Instead, use a `using static` import instead. – Dai May 13 '22 at 03:43
  • Creating a `bool _dummy` field will add to the object size (remember, every instance of the class will reserve memory for the `bool` field). So if you create many such objects, they will eat more memory than required. – Artemious May 13 '22 at 16:05
5

If you can live without the positional constructor, you can have your validation done in the init portion of each property which requires it:

record Person
{
    private readonly string _firstName;
    private readonly string _lastName;
    private readonly int _age;
    
    public Guid Id { get; init; }
    
    public string FirstName
    {
        get => _firstName;
        init => _firstName = (value ?? throw new ArgumentException("Argument cannot be null.", nameof(value)));
    }
    
    public string LastName
    {
        get => _lastName;
        init => _lastName = (value ?? throw new ArgumentException("Argument cannot be null.", nameof(value)));
    }
    
    public int Age
    {
        get => _age;
        init =>
        {
            if (value < 0)
            {
                throw new ArgumentException("Argument cannot be negative.", nameof(value));
            }
            _age = value;
        }
    }
}

Otherwise, you will need to create a custom constructor as mentioned in a comment above.

(As an aside, consider using ArgumentNullException and ArgumentOutOfRangeException instead of ArgumentException. These inherit from ArgumentException, but are more specific as to the type of error which occurred.)

(Source)

Knowledge Cube
  • 990
  • 12
  • 35
  • 2
    *"If you can live without the positional constructor"* ... and also the deconstructor. – Pang Feb 05 '21 at 01:28
  • @Pang Of course you can also add the Constructor and the Deconstructor, you still have the added Benefit of 'with' Syntax. – Spoc Mar 17 '21 at 08:03
  • 2
    This doesn't replace the constructor: the `FirstName` can be null in the constructed object if it doesn't get set, where the constructor precludes that possibility. Is there a way to force the record to run the validators (logically) (eg., by explicitly assigning `default` to any properties that the calling code doesn't assign)? – minnmass Aug 20 '21 at 19:55
3

The other answers here are all very awesome, however, neither cover the with operator, as far as I can tell. I need to make sure we cannot enter an invalid state on our domain model, and we like to use the records where applicable. I stumbled upon this question while looking for the best solution for that. I've created a bunch of tests for different scenarios and solutions, however, most fail with the with expression. The variants I tried can be found here: https://gist.github.com/C0DK/d9e8b99deca92a3a07b3a82ba4a6c4f8

The solution I ended up going with was:

public record Foo(int Value)
{
    private readonly int _value = GetValidatedValue(Value);

    public int Value
    {
        get => _value;
        init => _value = GetValidatedValue(value);
    }

    private static int GetValidatedValue(int value)
    {
        if (value < 0) throw new Exception();
        return value;
    }
}

You sadly currently need both to handle both ways of updating/creating a record.

Casper Bang
  • 793
  • 2
  • 6
  • 24
  • Wow, this is about as painful as working with `struct` types (where `this == default(T)` can happen at any moment, so all non-nullable reference-type fields _can still_ be `null`), except we're told `record` types are meant to be the future... aieeee – Dai Aug 03 '22 at 16:36
  • I was looking at the tests you wrote (and got it working in Linqpad [with a quick-fix](https://stackoverflow.com/a/53753215/159145)) and thinking that the behaviour of the `with` operator feels like a bug - have you considered filing an issue about it in the C# language GitHub repo? – Dai Aug 03 '22 at 16:42
  • 1
    @Dai no, as far as I know records are still early, and they are working on the init operator in the next release. Records do seem poorly implemented, which is sad. The idea is super great. I can recommend switching to F# where possible instead. There, you'll get most of the great features out of the box :) (also I'm sad my answer isn't further up, as AFAIK it's "more correct") – Casper Bang Oct 03 '22 at 18:14
  • 1
    Yeah, every new C# feature that feels exciting turns out like a big disappointment in the end. So many hacks required to make something work in this language. – Konrad Oct 28 '22 at 09:46
1
record Person([Required] Guid Id, [Required] string FirstName, [Required] string LastName, int Age);
Ran
  • 27
  • 2
  • 4
    Only very specific circumstances (such as ASP.NET Core model binding) utilize the `[Required]` attribute. Nothing in .NET guarantees that records instantiated this way will validate the parameters/properties. This is fine if your record is e.g. a binding model used for an ASP.NET Core action, but probably not suitable for most other cases. – Jeremy Caney Mar 17 '22 at 00:46
  • @JeremyCaney It only takes a line of code to run validations on an object, and it doesn't require asp.net. This seems like it would be the best approach. – Kelly Elton Oct 07 '22 at 15:51
  • 1
    @KellyElton: It's definitely an approach that I've utilized, and I'm certainly not recommending against it. But it's also important that anyone thinking about utilizing it know that the validation isn't enforced of implicit in most situations. – Jeremy Caney Oct 07 '22 at 21:08