C# WebApi如何为DTO内自定义类型属性实现模型绑定验证
问题原因
在Password类上标注[ModelBinder]不生效的核心原因:当接口标记[ApiController]且接收application/json类型请求体时,ASP.NET Core 默认使用JSON格式化器(InputFormatter)直接反序列化整个UserRegisterDto对象,该过程不会为单个嵌套属性触发独立模型绑定流程,类上标注的模型绑定器特性不会被执行。
可行实现方案
方案1:自定义JSON转换器(适配JSON请求场景,改动最小)
JSON请求走InputFormatter反序列化流程,直接为Password类型编写自定义JsonConverter,即可在反序列化Password属性时自动触发校验逻辑,无需为整个DTO添加绑定器,也无需修改Controller代码。
- 编写Password对应的JsonConverter,复用已有的校验逻辑:
using System.Text.Json; using System.Text.Json.Serialization; namespace UserService.Models { public class PasswordJsonConverter : JsonConverter<Password> { public override Password? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.String) { throw new JsonException("密码字段必须为字符串类型"); } string? passwordStr = reader.GetString(); if (Password.TryParse(passwordStr.AsSpan(), out Password? result)) { return result; } throw new JsonException("密码强度不符合要求:需至少8位,包含大小写字母、数字和特殊字符"); } public override void Write(Utf8JsonWriter writer, Password value, JsonSerializerOptions options) { writer.WriteStringValue(value.Value); } } }
- 为
Password类添加转换器特性,原有类内部代码无需修改:
namespace UserService.Models { [JsonConverter(typeof(PasswordJsonConverter))] public class Password { private const string PasswordRegex = "(?=^.{8,}$)(?=.*\\d)(?=.*[!@#$%&?*\\\\\"§$\\/()=~]+)(?![.\\n])(?=.*[A-Z])(?=.*[a-z]).*$"; public string Value { get; set; } public override string ToString() => Value; public static implicit operator string(Password e) => e.Value; public static bool TryParse(ReadOnlySpan<char> s, out Password? result) { result = null; if (string.IsNullOrWhiteSpace(s.ToString())) return false; if (!Regex.IsMatch(s.ToString(), PasswordRegex)) return false; result = new Password() { Value = s.ToString(), }; return true; } } }
方案生效后,前端传入JSON请求体时,反序列化到Password属性会自动执行正则校验,校验失败直接返回400错误,无需修改Controller和DTO其他代码。
方案2:兼容非JSON场景的模型绑定器实现
如果需要支持form-data、x-www-form-urlencoded提交,或直接将Password作为路由、查询参数传值,可补充实现模型绑定器,与上述JsonConverter互不冲突:
- 实现
PasswordEntityBinder:
using Microsoft.AspNetCore.Mvc.ModelBinding; namespace UserService.Models { public class PasswordEntityBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext)); ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (valueProviderResult == ValueProviderResult.None) return Task.CompletedTask; string? value = valueProviderResult.FirstValue; if (Password.TryParse(value.AsSpan(), out Password? result)) { bindingContext.Result = ModelBindingResult.Success(result); } else { bindingContext.ModelState.TryAddModelError(bindingContext.ModelName, "密码强度不符合要求"); bindingContext.Result = ModelBindingResult.Failed(); } return Task.CompletedTask; } } }
- 将绑定器特性添加到
Password类上,与[JsonConverter]共存:
[JsonConverter(typeof(PasswordJsonConverter))] [ModelBinder(typeof(PasswordEntityBinder))] public class Password { // 内部代码保持不变 }
注意事项
- 不要为整个DTO添加模型绑定器处理JSON请求,该方式会绕过默认JSON反序列化逻辑,增加额外维护成本
- 已实现的
TryParse方法覆盖了核心校验逻辑,JSON转换器、模型绑定器均可直接复用,无需重复编写校验代码 - 校验失败抛出的JsonException、添加的ModelState错误,都会被
[ApiController]的自动400响应机制捕获,无需在Controller中手动处理错误
内容的提问来源于stack exchange,提问作者user9573909
相关产品推荐
相关产品推荐

