我正在努力为一个类实现验证器,其中只应设置一个属性。
假设我们有以下课程:
public class SomeClass
{
public DateTime SomeDate {get; set;}
public IEnumerable<int> FirstOptionalProperty {get; set;}
public IEnumerable<int> SecondOptionalProperty {get; set;}
public IEnumerable<int> ThirdOptionalProperty {get; set;}
}
该类有一个强制属性 -
SomeDate
。其他属性是可选的,只能设置一个,例如,如果设置了 FirstOptionalProperty
- SecondOptionalProperty
和 ThirdOptionalProperty
应为空,如果设置了 SecondOptionalProperty
- FirstOptionalProperty
和 ThirdOptionalProperty
应为空,等等。
换句话说:如果设置了 IEnumerable 属性之一 - 其他 IEnumerable 应该为 null。
关于为此类类型实现验证器有什么技巧/想法吗?我唯一想到的就是编写大量
When
规则,但这种编写代码的方式很容易出错,而且结果看起来很难看。
您可以利用
Must
重载来访问整个类对象,以便您可以针对其他属性进行属性验证。有关更多详细信息,请参阅多个属性的 FluentValidation 规则。
public class SomeClassValidator : AbstractValidator<SomeClass>
{
private const string OneOptionalPropertyMessage = "Only one of FirstOptionalProperty, SecondOptionalProperty, or ThirdOptionalProperty can be set.";
public SomeClassValidator()
{
RuleFor(x => x.FirstOptionalProperty)
.Must(OptionalPropertiesAreValid)
.WithMessage(OneOptionalPropertyMessage);
RuleFor(x => x.SecondOptionalProperty)
.Must(OptionalPropertiesAreValid)
.WithMessage(OneOptionalPropertyMessage);
RuleFor(x => x.ThirdOptionalProperty)
.Must(OptionalPropertiesAreValid)
.WithMessage(OneOptionalPropertyMessage);
}
// this "break out" method only works because all of the optional properties
// in the class are of the same type. You'll need to move the logic back
// inline in the Must if that's not the case.
private bool OptionalPropertiesAreValid(SomeClass obj, IEnumerable<int> prop)
{
// "obj" is the important parameter here - it's the class instance.
// not going to use "prop" parameter.
// if they are all null, that's fine
if (obj.FirstOptionalProperty is null &&
obj.SecondOptionalProperty is null &&
obj.ThirdOptionalProperty is null)
{
return true;
}
// else, check that exactly 1 of them is not null
return new []
{
obj.FirstOptionalProperty is not null,
obj.SecondOptionalProperty is not null,
obj.ThirdOptionalProperty is not null
}
.Count(x => x == true) == 1;
// yes, the "== true" is not needed, I think it looks better
}
}
您可以调整检查功能。就目前情况而言,如果您设置 2 个或更多可选属性,所有这些属性都会引发错误。这可能适合也可能不适合您的需求。
您还可以仅针对第一个可选属性而不是所有属性创建
RuleFor
,因为所有属性都将执行相同的 IsValid 代码并返回相同的消息,如果您的用户得到,他们可能会有点困惑一条有关OptionalProperty1的错误消息,但他们没有提供该消息。
这种方法的缺点是,您需要在编译时知道所有属性是什么(以便您可以为其编写代码),并且如果添加/删除可选条目,则需要维护此验证器。这个缺点对您来说可能重要也可能不重要。
我想到的一件事是在这里使用反射:
SomeClass someClass = new SomeClass
{
SomeDate = DateTime.Now,
FirstOptionalProperty = new List<int>(),
//SecondOptionalProperty = new List<int>() // releasing this breakes the test
};
var info = typeof(SomeClass).GetProperties()
.SingleOrDefault(x =>
x.PropertyType != typeof(DateTime) &&
x.GetValue(someClass) != null);
基本上,如果
info
是 null
,则实例化了超过 1 个可选属性
我会为此使用辅助函数。
private static bool OnlyOneNotNull(params object[] properties) =>
properties.Count(p => p is not null) == 1;
你会像这样使用它。
SomeClass s = new SomeClass();
/* ... */
if(!OnlyOneNotNull(s.FirstOptionalProperty, s.SecondOptionalProperty, s.ThirdOptionalProperty))
{
/* handle error case */
}
这是@gunr2171解决方案的修改示例
它避免了他们提到的缺点,同时仍然使用 FluentValidation。
public class SomeClassValidator : AbstractValidator<SomeClass>
{
private const string OneOptionalPropertyMessage = "Only one of FirstOptionalProperty, SecondOptionalProperty, or ThirdOptionalProperty can be set.";
public SomeClassValidator()
{
// This ends up passing the entire SomeClass object to the function twice...
// It's kind of a hack but it works.
RuleFor(x => x)
.Must(OptionalPropertiesAreValid)
.WithName(nameof(SomeClass))
.WithErrorCode($"{nameof(OptionalPropertiesAreValid)}Validator")
.WithMessage(OneOptionalPropertyMessage);
}
// this "break out" method only works because all of the optional properties
// in the class are of the OptionalPropertiesAreValidsame type. You'll need to move the logic back
// inline in the Must if that's not the case.
private bool OptionalPropertiesAreValid(SomeClass obj, SomeClass redundantObj)
{
// "obj" is the important parameter here - it's the class instance.
// not going to use "prop" parameter.
// if they are all null, that's fine
if (obj.FirstOptionalProperty is null &&
obj.SecondOptionalProperty is null &&
obj.ThirdOptionalProperty is null)
{
return true;
}
// else, check that exactly 1 of them is not null
return new []
{
obj.FirstOptionalProperty is not null,
obj.SecondOptionalProperty is not null,
obj.ThirdOptionalProperty is not null
}
.Count(x => x == true) == 1;
// yes, the "== true" is not needed, I think it looks better
}
}