You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 17:25:55