ASP.NET Core MVC:移除模型绑定中未提交值的验证错误
实现仅验证提交属性的ModelState过滤
要实现仅将实际提交属性的验证错误保留在ModelState中,同时保持ModelState.IsValid为false(只要存在提交属性的错误),可以通过自定义Action过滤器结合调整IValidatableObject验证逻辑来实现,完全保留默认MVC约定,无需修改模型的验证特性。
方案一:自定义ActionFilter过滤ModelState
这个过滤器会在验证完成后,移除未提交属性对应的所有验证错误,只保留提交属性的错误。
1. 实现ActionFilter
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using System.Text.Json; public class ValidateOnlySubmittedPropertiesFilter : IActionFilter { public void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { // 获取请求中实际提交的属性列表 var submittedProperties = GetSubmittedProperties(context.HttpContext.Request); // 过滤ModelState中的错误 var keysToRemove = new List<string>(); foreach (var key in context.ModelState.Keys) { var rootPropertyName = GetRootPropertyName(key); // 如果当前属性未提交,标记为待移除 if (!submittedProperties.Contains(rootPropertyName, StringComparer.OrdinalIgnoreCase)) { keysToRemove.Add(key); } else { // 过滤自定义验证中涉及未提交属性的错误 var errors = context.ModelState[key].Errors.ToList(); foreach (var error in errors) { if (error is ModelValidationException modelValidationEx && modelValidationEx.ValidationResult != null) { // 如果错误关联的所有属性都未提交,移除该错误 if (modelValidationEx.ValidationResult.MemberNames.All(mn => !submittedProperties.Contains(mn, StringComparer.OrdinalIgnoreCase))) { context.ModelState[key].Errors.Remove(error); } } } // 如果当前属性没有剩余错误,标记为待移除 if (!context.ModelState[key].Errors.Any()) { keysToRemove.Add(key); } } } // 移除标记的键 foreach (var key in keysToRemove) { context.ModelState.Remove(key); } } } public void OnActionExecuted(ActionExecutedContext context) { } // 获取提交的所有根属性(支持JSON和表单提交) private HashSet<string> GetSubmittedProperties(HttpRequest request) { var submittedProperties = new HashSet<string>(); if (request.ContentType?.Contains("application/json") == true) { request.EnableBuffering(); using var reader = new StreamReader(request.Body, leaveOpen: true); var jsonContent = reader.ReadToEnd(); request.Body.Position = 0; if (!string.IsNullOrEmpty(jsonContent)) { using var doc = JsonDocument.Parse(jsonContent); foreach (var prop in doc.RootElement.EnumerateObject()) { submittedProperties.Add(prop.Name); } } } else if (request.HasFormContentType) { foreach (var formKey in request.Form.Keys) { submittedProperties.Add(GetRootPropertyName(formKey)); } } return submittedProperties; } // 获取根属性名(处理嵌套/数组属性,如"Hobbies[0]" → "Hobbies") private string GetRootPropertyName(string fullKey) { var separatorIndex = fullKey.IndexOfAny(new[] { '[', '.' }); return separatorIndex == -1 ? fullKey : fullKey.Substring(0, separatorIndex); } }
2. 注册过滤器
在Program.cs中注册过滤器,可选择全局注册或局部注册:
var builder = WebApplication.CreateBuilder(args); // 注册HttpContextAccessor(后续自定义验证需要) builder.Services.AddHttpContextAccessor(); // 注册过滤器 builder.Services.AddScoped<ValidateOnlySubmittedPropertiesFilter>(); builder.Services.AddControllers(options => { // 全局注册,对所有Controller生效 options.Filters.Add<ValidateOnlySubmittedPropertiesFilter>(); }); var app = builder.Build(); // ... 其他中间件配置 app.Run();
方案二:调整IValidatableObject的验证逻辑
默认的IValidatableObject.Validate会执行所有验证逻辑,需要修改为仅对提交的属性进行验证,避免生成未提交属性的错误。
修改PersonModel的Validate方法
using System.ComponentModel.DataAnnotations; public class PersonModel : IValidatableObject { public Guid? Id { get; set; } [Required] [StringLength(50)] public string FirstName { get; set; } = string.Empty; [Required] [StringLength(50)] public string LastName { get; set; } = string.Empty; public int? Age { get; set; } [Required] [EmailAddress] [StringLength(100)] public string Email { get; set; } = string.Empty; [Required] [EmailAddress] [StringLength(100)] [Compare(nameof(Email))] public string EmailConfirm { get; set; } = string.Empty; [Required] [DataType(DataType.Date)] public DateOnly BirthDate { get; set;} public List<string> Hobbies = []; public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { var httpContext = validationContext.GetService<IHttpContextAccessor>()?.HttpContext; var submittedProperties = new HashSet<string>(); // 获取提交的属性列表 if (httpContext != null) { if (httpContext.Request.ContentType?.Contains("application/json") == true) { httpContext.Request.EnableBuffering(); using var reader = new StreamReader(httpContext.Request.Body, leaveOpen: true); var jsonContent = reader.ReadToEnd(); httpContext.Request.Body.Position = 0; if (!string.IsNullOrEmpty(jsonContent)) { using var doc = JsonDocument.Parse(jsonContent); foreach (var prop in doc.RootElement.EnumerateObject()) { submittedProperties.Add(prop.Name); } } } else if (httpContext.Request.HasFormContentType) { foreach (var formKey in httpContext.Request.Form.Keys) { submittedProperties.Add(GetRootPropertyName(formKey)); } } } // 仅在Hobbies被提交时验证 if (submittedProperties.Contains(nameof(Hobbies), StringComparer.OrdinalIgnoreCase) && Hobbies.Count < 1) { yield return new ValidationResult( "At least one hobby must be selected.", [nameof(Hobbies)]); } // 仅在BirthDate被提交时验证相关逻辑 if (submittedProperties.Contains(nameof(BirthDate), StringComparer.OrdinalIgnoreCase)) { if (BirthDate == default) { yield return new ValidationResult( "Birthdate is required to cross check against age.", [nameof(BirthDate)]); } else { int yearsPassed = DateTime.Now.Year - BirthDate.Year; if (DateTime.Now.Month < BirthDate.Month || (DateTime.Now.Month == BirthDate.Month && DateTime.Now.Day < BirthDate.Day)) { yearsPassed--; } // 仅在Age也被提交时验证年龄匹配 if (submittedProperties.Contains(nameof(Age), StringComparer.OrdinalIgnoreCase) && yearsPassed != Age) { yield return new ValidationResult( "Age does not match birthdate.", [nameof(BirthDate), nameof(Age)]); } } } } private string GetRootPropertyName(string fullKey) { var separatorIndex = fullKey.IndexOfAny(new[] { '[', '.' }); return separatorIndex == -1 ? fullKey : fullKey.Substring(0, separatorIndex); } }
效果验证
当提交示例中的JSON时:
{ "Id": null, "FirstName": "Joe", "LastName": "Bob", "Age": null, "Hobbies": [] }
ModelState中只会保留Hobbies的验证错误("At least one hobby must be selected.")BirthDate、Email、EmailConfirm的Required错误会被过滤移除ModelState.IsValid仍为false,符合需求
内容的提问来源于stack exchange,提问作者SventoryMang
相关产品推荐
相关产品推荐

