如何让FluentValidation验证字典时用键而非数组索引作属性名
这是针对.NET 4、ASP.NET 5与FluentValidation 8.5旧问题的后续提问。尽管可能被视为重复,但FluentValidation多年来变化较大,且本文有更简单的复现方式。另可参考GitHub上已有8年历史的相关议题,该议题提及要“更好地支持字典”。
我正在尝试使用FluentValidation 11.9.1验证字典。.NET中的字典虽实现了IEnumerable<KeyValuePair<T1, T2>>,但理应通过索引符号(如items["foo"])访问。以下是复现问题的最简代码,后续会说明实际应用中必须验证字典的原因。
环境信息
-.NET Framework 8
-Visual Studio 2022中的MS Test项目
-FluentValidation 11.9.1
模型代码
public class OrderForm { public Dictionary<string, OrderItem> Items { get; set; } = new Dictionary<string, OrderItem>(); } public class OrderItem { public string? Name { get; set; } public decimal? Price { get; set; } }
Items字典的string类型键是必需的,因为最终要用于单体MVC应用,JavaScript会动态向列表中添加项(后续详述)。
验证器代码
public class OrderFormValidator : AbstractValidator<OrderForm> { public OrderFormValidator() { RuleFor(model => model.Items) .NotEmpty(); RuleForEach(model => model.Items) .SetValidator(new OrderItemValidator()); } } public class OrderItemValidator : AbstractValidator<KeyValuePair<string, OrderItem>> { public OrderItemValidator() { RuleFor(model => model.Value.Name) .NotEmpty(); RuleFor(model => model.Value.Price) .NotNull() .GreaterThan(0.0m); } }
失败的测试用例
以下测试展示了预期结果:
[TestClass] public class OrderFormValidationTests { [TestMethod] public void Given_empty_order_item_When_validating_Then_order_item_is_invalid() { var model = new OrderForm() { Items = new Dictionary<string, OrderItem>() { ["foo"] = new OrderItem() } }; var validator = new OrderFormValidator(); var results = validator.Validate(model); // 此断言通过 results.Errors.Count.ShouldBeEqualTo(2); // 此断言失败 results.Errors[0].PropertyName.ShouldBeEqualTo(@"Items[foo].Price"); } }
测试失败信息:
Given_empty_order_item_When_validating_Then_order_item_is_invalid Source: UnitTest1.cs line 10 Duration: 349 ms Message: Test method OrderFormValidationTests.Given_empty_order_item_When_validating_Then_order_item_is_invalid threw exception: FluentAssert.Exceptions.ShouldBeEqualAssertionException: Expected string length 18 but was 19. Strings differ at index 6. Expected: "Items[foo].Price" But was: "Items[0].Value.Name"
为何在模型中使用字典
Visual Studio智能提示显示RuleForEach(model => model.Items)期望接收IEnumerable<KeyValuePair<string, OrderItem>>,这在技术上是正确的,因为Dictionary<T1, T2>实现了IEnumerable<T>。但FluentValidation为字典项生成的属性名会破坏ASP.NET MVC模型与Razor视图中的验证消息绑定。Razor视图中调用@Html.EditorFor(model => model.Items["foo"].Name)会生成格式为Items[foo].Name的表单字段名,ValidationMessageFor助手期望MVC ModelState中存在同名的验证错误,但FluentValidation却使用数字索引(如同List或数组)而非字典键。
遗憾的是,我的应用视图层必须使用字典。这是一个老旧的MVC 5单体应用,旧版FluentValidation验证器特性可正确关联字典模型的验证消息,因此在.NET 4/MVC 5/FluentValidation 8.5环境下可正常工作。客户端有JavaScript会在页面加载后动态添加项,我需要将HTML模板中的特殊令牌替换为Date.now()以生成字典的新键,确保表单完整回传。由于表单庞大复杂,无法完全重构为其他数据结构,必须使用字典。
我的问题
如何让FluentValidation验证字典时,属性名格式为Items[foo].Name而非Items[0].Name?
理想的解决方案优先级如下:
1.(最优)提供可实现该效果的API调用序列;
2.编写自定义验证器或重写FluentValidation的部分逻辑以生成所需属性名;
3.重构C#验证器结构,但需保留字典的使用;
4.说明该功能暂不支持,需提交PR实现。
内容的提问来源于stack exchange,提问作者Greg Burghardt

