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!