ASP.NET MVC Create视图中Fluent Validation实体级验证消息放置位置
问题说明
在ASP.NET MVC项目里用Fluent Validation写了DocTrailListValidator验证器,ProjectID、DocumentType、IsThisCurrent这些字段级的验证规则能在Create视图正常显示错误消息,但针对整个实体的重复项验证规则(RuleFor(x => x).Must(...))的“Duplicate entry found.”错误始终显示不出来,试过关联到ProjectID的验证消息也没用。
原代码参考
验证器代码
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Web; using DocumentFormat.OpenXml.Linq; using FluentValidation; using FluentValidation.AspNetCore; using FluentValidation.Results; using Microsoft.Ajax.Utilities; using OpenXmlPowerTools; using ValidatorAttribute = ServiceStack.FluentValidation.Attributes.ValidatorAttribute; namespace Research.Models { public class DocTrailListValidator : AbstractValidator<DocTrailList> { public DocTrailListValidator() { RuleFor(x => x.ProjectID).NotNull().WithMessage("A required field"); RuleFor(x => x.DocumentType).NotNull().WithMessage("A required field"); RuleFor(x => x.IsThisCurrent).NotNull().WithMessage("A required field"); RuleFor(x => x).Must(x => !IsDuplicate(x)).WithMessage("Duplicate entry found."); } private bool IsDuplicate(DocTrailList c) { // Example list of existing values for validation var existingValues = new List<DocTrailList>(); return existingValues.Any(x => x.ProjectID == c.ProjectID && x.DocumentType == c.DocumentType && x.IsThisCurrent == c.IsThisCurrent); } } }
Create视图代码片段
<div class="col"> <div class="form-group"> <label asp-for="IsThisCurrent" class="control-label"></label> @Html.DropDownList("IsThisCurrent", new SelectList(ViewBag.current,"YesNo","YesNo"),string.Empty) <div class="col-md-10"> @Html.ValidationMessageFor(model => model.IsThisCurrent, null, new { @class = "text-danger" }) </div> </div> </div> <div class="col"> <div class="form-group"> List<DocTrailList> @Html.ValidationMessageFor(model => model.ProjectID, null, new { @class = "text-danger" }) </div>
Create控制器代码
// GET: DocTrailLists/Create public IActionResult Create() { ViewData["ProjectID"] = new SelectList(_context.PIF, "ProjectID", "ProjectID"); var dtname = _context.DocType.ToList(); ViewBag.dtname = dtname; var current = _context.YesNoList.ToList(); ViewBag.current = current; return View(); } // POST: DocTrailLists/Create [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Create([Bind("DetailID,ProjectID,DocumentType,Version,Dated,DateReceived,IsThisCurrent")] DocTrailList docTrailList) { DocTrailListValidator doctraillistsvalidator = new DocTrailListValidator(); ValidationResult result = doctraillistsvalidator.Validate(docTrailList); if (result.IsValid) { if (ModelState.IsValid) { _context.Add(docTrailList); await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } } else { foreach (var failure in result.Errors) { ModelState.AddModelError("", failure.ErrorMessage); } } ViewData["ProjectID"] = new SelectList(_context.PIF, "ProjectID", "ProjectID", docTrailList.ProjectID); return View(docTrailList); }
解决办法
核心问题
你在控制器里把实体级验证错误加到了空键的ModelState里(ModelState.AddModelError("", failure.ErrorMessage);),但视图里没有对应显示空键错误的控件;另外Fluent Validation的实体级规则默认不会绑定到单个字段,得手动指定或者在视图加全局错误显示。
方案一:把重复错误绑定到指定字段
修改验证器,把实体级的重复验证规则关联到某个字段(比如ProjectID),这样就能用该字段的ValidationMessageFor显示错误:
// 替换原验证器中的实体级规则 RuleFor(x => x.ProjectID) .Must((entity, _) => !IsDuplicate(entity)) // 第一个参数是完整实体,第二个是ProjectID的值 .WithMessage("已存在重复记录");
这样验证错误会被标记到ProjectID字段,视图里@Html.ValidationMessageFor(model => model.ProjectID)就能同时显示必填和重复的错误消息。
方案二:在视图中添加全局错误显示
不想绑定到单个字段的话,直接在表单顶部加全局错误显示控件:
<!-- 放在表单最上方,显示所有错误(包括实体级) --> <div class="text-danger"> @Html.ValidationSummary(false, "", new { @class = "text-danger" }) </div>
或者专门显示空键的错误:
@foreach (var error in ViewData.ModelState[""].Errors) { <div class="text-danger">@error.ErrorMessage</div> }
这样控制器里加的空键错误就能正常显示。
额外优化
- 修复重复验证逻辑:原代码里
existingValues是空列表,永远不会检测到重复,要改成从数据库查真实数据,记得注入DbContext:
private readonly YourDbContext _context; // 构造函数注入DbContext public DocTrailListValidator(YourDbContext context) { _context = context; // ...其他验证规则 } private bool IsDuplicate(DocTrailList c) { // 排除当前编辑的记录(如果是编辑场景) return _context.DocTrailList.Any(x => x.ProjectID == c.ProjectID && x.DocumentType == c.DocumentType && x.IsThisCurrent == c.IsThisCurrent && x.DetailID != c.DetailID); }
- 启用Fluent Validation自动集成:不用手动实例化验证器,在Startup.cs里配置:
services.AddControllersWithViews() .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<DocTrailListValidator>());
这样控制器里只要检查ModelState.IsValid就行,不用手动调用验证器:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Create([Bind("DetailID,ProjectID,DocumentType,Version,Dated,DateReceived,IsThisCurrent")] DocTrailList docTrailList) { if (ModelState.IsValid) { _context.Add(docTrailList); await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } // 重新绑定下拉框数据 ViewData["ProjectID"] = new SelectList(_context.PIF, "ProjectID", "ProjectID", docTrailList.ProjectID); ViewBag.dtname = _context.DocType.ToList(); ViewBag.current = _context.YesNoList.ToList(); return View(docTrailList); }
内容的提问来源于stack exchange,提问作者Sally Parkes

