在程序中,需要进行数据验证的场景经常存在,且数据验证是有必要的。前端进行数据验证,主要是为了减少服务器请求压力,和提高用户体验;后端进行数据验证,主要是为了保证数据的正确性,保证系统的健壮性。
本文描述的数据验证方案,是基于官方的模型验证(Model validation),也是笔者近期面试过程中才得知的方式【之前个人混淆了:模型验证(Model validation)和 EF 模型配置的数据注释(Data annotation)方式】。
注:MVC 和 API 的模型验证有些许差异,本文主要描述的是 API 下的模型验证。
比较传统的验证方式如下:
public string TraditionValidation(TestModel model) { if (string.IsNullOrEmpty(model.Name)) { return "名字不能为空!"; } if (model.Name.Length > 10) { return "名字长度不能超过10!"; } return "验证通过!"; }
在函数中,对模型的各个属性分别做验证。
虽然函数能与模型配合重复使用,但是确实不够优雅。
官方提供了模型验证(Model validation)的方式,下面将会基于这种方式,提出相应的解决方案。
先大概介绍一下模型验证(Model validation)的使用,随后提出两种自定义方案。
最后会大概解读一下 AspNetCore 这一块相关的源码。
官方提供的模型验证(Model validation)的方式,是通过在模型属性上添加验证特性(Validation attributes),配置验证规则以及相应的错误信息(ErrorMessage)。当验证不通过时,将会返回验证不通过的错误信息。
其中,除了内置的验证特性,用户也可以自定义验证特性(本文不展开),具体请自行查看自定义特性一节。
在 MVC 中,需要通过如下代码来调用(在 action 中):
if (!ModelState.IsValid) { return View(movie); }
在 API 中,只要控制器拥有 [ApiController] 特性,如果模型验证不通过,将自动返回包含错误信息的 HTTP400 相应,详细请参阅自动 HTTP 400 响应。
如下代码中,[Required]
表示该属性为必须,ErrorMessage = ""
为该验证特性验证不通过时,返回的验证信息。
public class TestModel { [Required(ErrorMessage = "名字不能为空!")] [StringLength(10, ErrorMessage = "名字长度不能超过10个字符!")] public string? Name { get; set; } [Phone(ErrorMessage = "手机格式错误!")] public string? Phone { get; set; } }
控制器上有 [ApiController]
特性即可触发:
[ApiController] [Route("[controller]/[action]")] public class TestController : ControllerBase { [HttpPost] public TestModel ModelValidation(TestModel model) { return model; } }
输入不合法的数据,格式如下:
{ "name": "string string", "email": "111" }
输出信息如下:
{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "00-4d4df1b3618a97a6c50d5fe45884543d-81ac2a79523fd282-00", "errors": { "Name": [ "名字长度不能超过10个字符!" ], "Email": [ "邮箱格式错误!" ] } }
官方列出的一些内置特性如:
[ValidateNever]:指示属性或参数应从验证中排除。
[CreditCard]:验证属性是否具有信用卡格式。
[Compare]:验证模型中的两个属性是否匹配。
[EmailAddress]:验证属性是否具有电子邮件格式。
[Phone]:验证属性是否具有电话号码格式。
[Range]:验证属性值是否在指定的范围内。
[RegularExpression]:验证属性值是否与指定的正则表达式匹配。
[Required]:验证字段是否不为 null。
[StringLength]:验证字符串属性值是否不超过指定长度限制。
[URL]:验证属性是否具有 URL 格式。
[Remote]:通过在服务器上调用操作方法来验证客户端上的输入。
可以在命名空间中找到 System.ComponentModel.DataAnnotations 验证属性的完整列表。
由于官方模型验证返回的格式与我们程序实际需要的格式有差异,所以这一部分主要是替换模型验证的返回格式,使用的实际上还是模型验证的能力。
准备一个统一返回格式:
public class ApiResult { public int Code { get; set; } public string? Msg { get; set; } public object? Data { get; set; } }
当数据验证不通过时:
Code 为 400,表示请求数据存在问题。
Msg 默认为:数据验证不通过!用于前端提示。
Data 为错误信息明细,用于前端提示。
如:
{ "code": 400, "msg": "数据验证不通过!", "data": [ "名字长度不能超过10个字符!", "邮箱格式错误!" ] }
替换 ApiBehaviorOptions
中默认定义的 InvalidModelStateResponseFactory
,在 Program.cs 中:
builder.Services.Configure<ApiBehaviorOptions>(options => { options.InvalidModelStateResponseFactory = actionContext => { //获取验证失败的模型字段 var errors = actionContext.ModelState .Where(s => s.Value != null && s.Value.ValidationState == ModelValidationState.Invalid) .SelectMany(s => s.Value!.Errors.ToList()) .Select(e => e.ErrorMessage) .ToList(); // 统一返回格式 var result = new ApiResult() { Code = StatusCodes.Status400BadRequest, Msg = "数据验证不通过!", Data = errors }; return new BadRequestObjectResult(result); }; });
public class DataValidationFilter : IActionFilter { public void OnActionExecuting(ActionExecutingContext context) { // 如果其他过滤器已经设置了结果,则跳过验证 if (context.Result != null) return; // 如果验证通过,跳过后面的动作 if (context.ModelState.IsValid) return; // 获取失败的验证信息列表 var errors = context.ModelState .Where(s => s.Value != null && s.Value.ValidationState == ModelValidationState.Invalid) .SelectMany(s => s.Value!.Errors.ToList()) .Select(e => e.ErrorMessage) .ToArray(); // 统一返回格式 var result = new ApiResult() { Code = StatusCodes.Status400BadRequest, Msg = "数据验证不通过!", Data = errors }; // 设置结果 context.Result = new BadRequestObjectResult(result); } public void OnActionExecuted(ActionExecutedContext context) { } }
在 Program.cs 中:
builder.Services.Configure<ApiBehaviorOptions>(options => { // 禁用默认模型验证过滤器 options.SuppressModelStateInvalidFilter = true; });
在 Program.cs 中:
builder.Services.Configure<MvcOptions>(options => { // 全局添加自定义模型验证过滤器 options.Filters.Add<DataValidationFilter>(); });
输入不合法的数据,格式如下:
{ "name": "string string", "email": "111" }
输出信息如下:
{ "code": 400, "msg": "数据验证不通过!", "data": [ "名字长度不能超过10个字符!", "邮箱格式错误!" ] }
两种方案实际上都是差不多的(实际上都是基于过滤器 Filter 的),可以根据个人需要选择。
其中 AspNetCore 默认实现的过滤器为 ModelStateInvalidFilter
,其 Order = -2000,可以根据程序实际情况,对程序内的过滤器顺序进行编排。
AspNetCore 模型验证这一块相关的源码,主要是通过注册一个默认工厂 InvalidModelStateResponseFactory
(由 ApiBehaviorOptionsSetup
对 ApiBehaviorOptions
进行配置,实际上是一个 Func
),以及使用一个过滤器(为 ModelStateInvalidFilter
,由 ModelStateInvalidFilterFactory
生成),来控制模型验证以及返回结果(返回一个 BadRequestObjectResult
或 ObjectResult
)。
其中,最主要的是 ApiBehaviorOptions
的 SuppressModelStateInvalidFilter
和 InvalidModelStateResponseFactory
属性。这两个属性,前者控制默认过滤器是否启用,后者生成模型验证的结果。
新建的 WebAPI 模板的 Program.cs 中注册控制器的语句如下:
builder.Services.AddControllers();
调用的是源码中 MvcServiceCollectionExtensions.cs
的方法,摘出来如下:
// MvcServiceCollectionExtensions.cs public static IMvcBuilder AddControllers(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } var builder = AddControllersCore(services); return new MvcBuilder(builder.Services, builder.PartManager); }
会调用另一个方法 AddControllersCore
:
// MvcServiceCollectionExtensions.cs private static IMvcCoreBuilder AddControllersCore(IServiceCollection services) { // This method excludes all of the view-related services by default. var builder = services .AddMvcCore() .AddApiExplorer() .AddAuthorization() .AddCors() .AddDataAnnotations() .AddFormatterMappings(); if (MetadataUpdater.IsSupported) { services.TryAddEnumerable( ServiceDescriptor.Singleton<IActionDescriptorChangeProvider, HotReloadService>()); } return builder; }
其中相关的是 AddMvcCore()
:
// MvcServiceCollectionExtensions.cs public static IMvcCoreBuilder AddMvcCore(this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } var environment = GetServiceFromCollection<IWebHostEnvironment>(services); var partManager = GetApplicationPartManager(services, environment); services.TryAddSingleton(partManager); ConfigureDefaultFeatureProviders(partManager); ConfigureDefaultServices(services); AddMvcCoreServices(services); var builder = new MvcCoreBuilder(services, partManager); return builder; }
其中 AddMvcCoreServices(services)
方法会执行如下方法,由于这个方法太长,这里将与模型验证相关的一句代码摘出来:
// MvcServiceCollectionExtensions.cs internal static void AddMvcCoreServices(IServiceCollection services) { services.TryAddEnumerable( ServiceDescriptor.Transient<IConfigureOptions<ApiBehaviorOptions>, ApiBehaviorOptionsSetup>()); }
主要是配置默认的 ApiBehaviorOptions
。
主要代码如下:
internal class ApiBehaviorOptionsSetup : IConfigureOptions<ApiBehaviorOptions> { private ProblemDetailsFactory? _problemDetailsFactory; public void Configure(ApiBehaviorOptions options) { options.InvalidModelStateResponseFactory = context => { _problemDetailsFactory ??= context.HttpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>(); return ProblemDetailsInvalidModelStateResponse(_problemDetailsFactory, context); }; ConfigureClientErrorMapping(options); } }
为属性 InvalidModelStateResponseFactory
配置一个默认工厂,这个工厂在执行时,会做这些动作:
获取 ProblemDetailsFactory
(Singleton)服务实例,调用 ProblemDetailsInvalidModelStateResponse
获取一个 IActionResult
作为响应结果。
ProblemDetailsInvalidModelStateResponse
方法如下:
// ApiBehaviorOptionsSetup.cs internal static IActionResult ProblemDetailsInvalidModelStateResponse(ProblemDetailsFactory problemDetailsFactory, ActionContext context) { var problemDetails = problemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState); ObjectResult result; if (problemDetails.Status == 400) { // For compatibility with 2.x, continue producing BadRequestObjectResult instances if the status code is 400. result = new BadRequestObjectResult(problemDetails); } else { result = new ObjectResult(problemDetails) { StatusCode = problemDetails.Status, }; } result.ContentTypes.Add("application/problem+json"); result.ContentTypes.Add("application/problem+xml"); return result; }
该方法最终会返回一个 BadRequestObjectResult
或 ObjectResult
。
上面介绍完了 InvalidModelStateResponseFactory
的注册,那么是何时调用这个工厂呢?
模型验证默认的过滤器主要代码如下:
public class ModelStateInvalidFilter : IActionFilter, IOrderedFilter { internal const int FilterOrder = -2000; private readonly ApiBehaviorOptions _apiBehaviorOptions; private readonly ILogger _logger; public int Order => FilterOrder; public void OnActionExecuting(ActionExecutingContext context) { if (context.Result == null && !context.ModelState.IsValid) { _logger.ModelStateInvalidFilterExecuting(); context.Result = _apiBehaviorOptions.InvalidModelStateResponseFactory(context); } } }
可以看到,在 OnActionExecuting
中,当没有其他过滤器设置结果(context.Result == null
),且模型验证不通过(!context.ModelState.IsValid
)时,会调用 InvalidModelStateResponseFactory
工厂的验证,获取返回结果。
模型验证最主要的源码就如上所述。
(1)过滤器的执行顺序
默认过滤器的 Order 为 -2000,其触发时机一般是较早的(模型验证也是要尽可能早)。
过滤器管道的执行顺序:Order 值越小,越先执行 Executing 方法,越后执行 Executed 方法(即先进后出)。
(2)默认过滤器的创建和注册
这一部分个人没有细看,套路大概是这样的:通过过滤器提供者(DefaultFilterProvider
),获取实现 IFilterFactory
接口的实例,调用 CreateInstance
方法生成过滤器,并将过滤器添加到过滤器容器中(IFilterContainer
)。
其中模型验证的默认过滤的工厂类为:ModelStateInvalidFilterFactory
本文示例的完整代码,可以从这里获取:
Gitee:https://gitee.com/lisheng741/testnetcore/tree/master/Filter/DataAnnotationTest
Github:https://github.com/lisheng741/testnetcore/tree/master/Filter/DataAnnotationTest
AspNetCore源码
手把手教你AspNetCore WebApi:数据验证
ASP.NET Core 官方文档>>高级>>模型验证