1

record is a new object type in C# 9. It saves quite a lot of typing when immutable objects are needed. In some situations it may become necessary to validate the Properties of a record. In this question, a pattern is proposed. That solution works, but I think I found a more elegant way (see my reply there). I propose this:

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

The idea is to use the Properties generated by the compiler, and have some validation function throw an exception if something's not right. Unfortunately there's the pointless _dummy variable. Is there a way to get rid of it?

Batox
  • 574
  • 1
  • 4
  • 16
  • 3
    Place the cursor at the left-most position on the line just below the first `{`, then hold down shift and press down-arrow twice, and then finally hit delete or backspace on your keyboard – Mathias R. Jessen Dec 01 '21 at 12:42
  • 1
    Short of writing an explicit constructor, no. (Ab)using an initializer to get some code injected into a generated constructor necessarily requires a field to be initialized. – Jeroen Mostert Dec 01 '21 at 13:06
  • Also, it is a private property, so whatever uses that record will not be aware of its existance. – Cleptus Dec 01 '21 at 13:50
  • @MathiasR.Jessen - very funny. And now please tell me how to get the validation done? – Batox Dec 01 '21 at 14:01
  • @Batox Your question boils down to "I think this is elegant, but I also don't think it's elegant, help?" - what are you expecting? Why is a private `bool` inelegant? What is your definition of elegance? – Mathias R. Jessen Dec 01 '21 at 14:03
  • Your code does already the validation, as long as no exceptions are raised your record has valid data. If your variable is `private`, no one outside the record Person scope would see it, so it is no that "dirty". Gets the job done? Yes. Others can mess with it? No.... Conclusion: Looks fine to me. – Cleptus Dec 01 '21 at 15:04
  • 1
    There is of course an observable difference between a class with a private field named `_dummy` and one without, even if only to reflection and (to a small extent) the garbage collector -- as opposed to rewriting this to use an explicit constructor. Whether it's worth being bothered about is another matter. I would say records are by no means a finished feature yet; it's conceivable future versions of C# will make this scenario easier without requiring hacks. – Jeroen Mostert Dec 01 '21 at 17:05
  • @Batox How can you guarantee the _dummy field is not set before the LastName, FirstName, Age and Id properties? – Yet Another Code Maker Feb 10 '22 at 12:32
  • Don't quite understand that comment. Record fields cannot be set outside their constructor, _dummy is private, and even if it would be possible to set it some way it wouldn't matter. It's just used as a trick to inject code in the constructor - see Jerone Mosterts comment a few lines up. – Batox Feb 11 '22 at 13:04
  • This is a very nice way of doing it I think, it would be nice if some standard validation feature is added, but for now I want to thank you for providing me a very nice way of doing this. I love the new record the only part that screwed things up was parameter validation. – Adriaan Mar 15 '22 at 10:07

1 Answers1

1

This is an excellent idea and it has inspired me to implement a type of validation framework around the idea based on extension methods. I created the following validation class (removed other validations for brevity):

public static class Validation
{
    public static bool IsValid<T>(this T _)
    {
        return true;
    }
    public static T NotNull<T>(T @value, [CallerArgumentExpression("value")] string? thisExpression = default)
    {
        if (value == null) throw new ArgumentNullException(thisExpression);
        return value;
    }

    public static string LengthBetween(this string @value, int min, int max, [CallerArgumentExpression("value")] string? thisExpression = default)
    {
        if (value.Length < min) throw new ArgumentOutOfRangeException(thisExpression, $"Can't have less than {min} items");
        if (value.Length > max) throw new ArgumentOutOfRangeException(thisExpression, $"Can't have more than {max} items");
        return value;
    }

    public static IComparable<T> RangeWithin<T>(this IComparable<T> @value, T min, T max, [CallerArgumentExpression("value")] string? thisExpression = default)
    {
        if (value.CompareTo(min) < 0) throw new ArgumentOutOfRangeException(thisExpression, $"Can't have less than {min} items");
        if (value.CompareTo(max) > 0) throw new ArgumentOutOfRangeException(thisExpression, $"Can't have more than {max} items");
        return value;
    }
}

Then you can use it with the following:

// FirstName may not be null and must be between 1 and 5
// LastName may be null, but when it is defined it must be between 3 and 10
// Age must be positive and below 200
record Person(string FirstName, string? LastName, int Age, Guid Id)
{
    private readonly bool _valid = Validation.NotNull(FirstName).LengthBetween(1, 5).IsValid() &&
        (LastName?.LengthBetween(2, 10).IsValid() ?? true) &&
        Age.RangeWithin(0, 200).IsValid();
        
}

The ?? true is VERY important, it is to ensure validation continues in case the nullable LastName was indeed null, otherwise it would short-circuit. Perhaps it would be better (safer) to use another static AllowNull method to wrap the entire validation of that variable in, like so:

public static class Validation
{
    public static bool IsValid<T>(this T _)
    {
        return true;
    }
    public static bool AllowNull<T>(T? _)
    {
        return true;
    }
    public static T NotNull<T>(T @value, [CallerArgumentExpression("value")] string? thisExpression = default)
    {
        if (value == null) throw new ArgumentNullException(thisExpression);
        return value;
    }

    public static string LengthBetween(this string @value, int min, int max, [CallerArgumentExpression("value")] string? thisExpression = default)
    {
        if (value.Length < min) throw new ArgumentOutOfRangeException(thisExpression, $"Can't have less than {min} items");
        if (value.Length > max) throw new ArgumentOutOfRangeException(thisExpression, $"Can't have more than {max} items");
        return value;
    }

    public static IComparable<T> RangeWithin<T>(this IComparable<T> @value, T min, T max, [CallerArgumentExpression("value")] string? thisExpression = default)
    {
        if (value.CompareTo(min) < 0) throw new ArgumentOutOfRangeException(thisExpression, $"Can't have less than {min} items");
        if (value.CompareTo(max) > 0) throw new ArgumentOutOfRangeException(thisExpression, $"Can't have more than {max} items");
        return value;
    }
}

record Person(string FirstName, string? LastName, int Age, Guid Id)
{
    private readonly bool _valid = Validation.NotNull(FirstName).LengthBetween(1, 5).IsValid() &&
        Validation.AllowNull(LastName?.LengthBetween(2, 10)) &&
        Age.RangeWithin(0, 200).IsValid();
}

Still don't like that part too much, but other than that it's pretty cool I think! Haven't actually tested it though :) So beware!

Adriaan
  • 144
  • 13
  • Any future visitor that might be interested - the final implementation I ended up doing for this is here: https://codereview.stackexchange.com/a/275089/256609 – Adriaan Mar 19 '22 at 19:36