C# 9 records validation

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

Leave a Comment