C# 9 记录验证

C# 9 records validation

使用 C# 9 的新记录类型,如何在对象的构造过程中注入自定义参数验证/空检查/等无需重写整个构造函数?

与此类似的内容:

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

如果你可以不用位置构造函数,你可以在每个需要它的 属性 的 init 部分完成验证:

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

否则,您将需要创建自定义构造函数,如上面评论中所述。

(顺便说一句,考虑使用 ArgumentNullException and ArgumentOutOfRangeException 而不是 ArgumentException。它们继承自 ArgumentException,但对于发生的错误类型更具体。)

(Source)

以下也实现了它并且更短(而且我也更清楚):

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

要是有办法去掉虚拟变量就好了。

record Person([Required] Guid Id, [Required] string FirstName, [Required] string LastName, int Age);

您可以在初始化期间验证 属性:

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

我参加聚会迟到了,但这可能仍然对某人有所帮助...

实际上有一个简单的解决方案(但请在使用前阅读下面的警告)。像这样定义基本记录类型:

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

    protected virtual void Validate()
    {
    }
}

并使您的实际记录继承 RecordWithValidation 并覆盖 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));
    }
}

如您所见,它几乎就是 OP 的代码。简单易用。

但是,如果你使用这个要非常小心:它只适用于用“位置记录”语法定义的属性(a.k.a。“主构造函数” ).

这样做的原因是我在这里做了一些“坏事”:我从基类型的构造函数中调用虚方法。通常不鼓励这样做,因为基类型的构造函数在派生类型的构造函数之前运行,因此派生类型可能未完全初始化,因此重写的方法可能无法正常工作。

但对于位置记录,事情不会按此顺序发生:首先初始化位置属性,然后调用基类型的构造函数。所以当 Validate 方法被调用时,属性已经被初始化,所以它按预期工作。

如果您要将 Person 记录更改为具有显式构造函数(或 init-only 属性但没有构造函数),则对 Validate 的调用将在设置属性之前发生, 所以它会失败。

编辑:这种方法的另一个恼人的限制是它不适用于 with(例如 person with { Age = 42 })。这使用不同的(生成的)构造函数,它不调用 Validate...

这里的其他答案都非常棒,但是,据我所知,都没有涵盖 with 运算符。我需要确保我们不能在我们的域模型上输入无效状态,并且我们喜欢在适用的地方使用记录。我在寻找最佳解决方案时偶然发现了这个问题。我为不同的场景和解决方案创建了一堆测试,但是,大多数测试都因 with 表达式而失败。我尝试过的变体可以在这里找到:https://gist.github.com/C0DK/d9e8b99deca92a3a07b3a82ba4a6c4f8

我最终采用的解决方案是:

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

遗憾的是,您目前需要同时处理 updating/creating 一条记录的两种方式。