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
,但对于发生的错误类型更具体。)
以下也实现了它并且更短(而且我也更清楚):
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));
}
我参加聚会迟到了,但这可能仍然对某人有所帮助...
实际上有一个简单的解决方案(但请在使用前阅读下面的警告)。像这样定义基本记录类型:
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 一条记录的两种方式。
使用 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
,但对于发生的错误类型更具体。)
以下也实现了它并且更短(而且我也更清楚):
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));
}
我参加聚会迟到了,但这可能仍然对某人有所帮助...
实际上有一个简单的解决方案(但请在使用前阅读下面的警告)。像这样定义基本记录类型:
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 一条记录的两种方式。