1 前言

在程序中,需要进行数据验证的场景经常存在,且数据验证是有必要的。前端进行数据验证,主要是为了减少服务器请求压力,和提高用户体验;后端进行数据验证,主要是为了保证数据的正确性,保证系统的健壮性。

本文描述的数据验证方案,是基于官方的模型验证(Model validation),也是笔者近期面试过程中才得知的方式【之前个人混淆了:模型验证(Model validation)和 EF 模型配置的数据注释(Data annotation)方式】。

注:MVC 和 API 的模型验证有些许差异,本文主要描述的是 API 下的模型验证。

1.1 数据验证的场景

比较传统的验证方式如下:

public string TraditionValidation(TestModel model)
{
if (string.IsNullOrEmpty(model.Name))
{
return "名字不能为空!";
}
if (model.Name.Length > 10)
{
return "名字长度不能超过10!";
} return "验证通过!";
}

在函数中,对模型的各个属性分别做验证。

虽然函数能与模型配合重复使用,但是确实不够优雅。

官方提供了模型验证(Model validation)的方式,下面将会基于这种方式,提出相应的解决方案。

1.2 本文的脉络

先大概介绍一下模型验证(Model validation)的使用,随后提出两种自定义方案。

最后会大概解读一下 AspNetCore 这一块相关的源码。

2 模型验证

2.1 介绍

官方提供的模型验证(Model validation)的方式,是通过在模型属性上添加验证特性(Validation attributes),配置验证规则以及相应的错误信息(ErrorMessage)。当验证不通过时,将会返回验证不通过的错误信息。

其中,除了内置的验证特性,用户也可以自定义验证特性(本文不展开),具体请自行查看自定义特性一节。

在 MVC 中,需要通过如下代码来调用(在 action 中):

if (!ModelState.IsValid)
{
return View(movie);
}

在 API 中,只要控制器拥有 [ApiController] 特性,如果模型验证不通过,将自动返回包含错误信息的 HTTP400 相应,详细请参阅自动 HTTP 400 响应

2.2 基本使用

(1)自定义模型

如下代码中,[Required] 表示该属性为必须,ErrorMessage = "" 为该验证特性验证不通过时,返回的验证信息。

public class TestModel
{
[Required(ErrorMessage = "名字不能为空!")]
[StringLength(10, ErrorMessage = "名字长度不能超过10个字符!")]
public string? Name { get; set; } [Phone(ErrorMessage = "手机格式错误!")]
public string? Phone { get; set; }
}

(2)控制器代码

控制器上有 [ApiController] 特性即可触发:

[ApiController]
[Route("[controller]/[action]")]
public class TestController : ControllerBase
{
[HttpPost]
public TestModel ModelValidation(TestModel model)
{
return model;
}
}

(3)测试

输入不合法的数据,格式如下:

{
"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": [
"邮箱格式错误!"
]
}
}

2.3 内置特性

官方列出的一些内置特性如:

  • [ValidateNever]:指示属性或参数应从验证中排除。

  • [CreditCard]:验证属性是否具有信用卡格式。

  • [Compare]:验证模型中的两个属性是否匹配。

  • [EmailAddress]:验证属性是否具有电子邮件格式。

  • [Phone]:验证属性是否具有电话号码格式。

  • [Range]:验证属性值是否在指定的范围内。

  • [RegularExpression]:验证属性值是否与指定的正则表达式匹配。

  • [Required]:验证字段是否不为 null。

  • [StringLength]:验证字符串属性值是否不超过指定长度限制。

  • [URL]:验证属性是否具有 URL 格式。

  • [Remote]:通过在服务器上调用操作方法来验证客户端上的输入。

可以在命名空间中找到 System.ComponentModel.DataAnnotations 验证属性的完整列表。

3 自定义数据验证

3.1 介绍

由于官方模型验证返回的格式与我们程序实际需要的格式有差异,所以这一部分主要是替换模型验证的返回格式,使用的实际上还是模型验证的能力。

3.2 前置准备

准备一个统一返回格式:

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个字符!",
"邮箱格式错误!"
]
}

3.3 方案1:替换工厂

替换 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);
};
});

3.4 方案2:自定义过滤器

(1)自定义过滤器

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)
{
}
}

(2)禁用默认过滤器

在 Program.cs 中:

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
// 禁用默认模型验证过滤器
options.SuppressModelStateInvalidFilter = true;
});

(3)启用自定义过滤器

在 Program.cs 中:

builder.Services.Configure<MvcOptions>(options =>
{
// 全局添加自定义模型验证过滤器
options.Filters.Add<DataValidationFilter>();
});

3.5 测试

输入不合法的数据,格式如下:

{
"name": "string string",
"email": "111"
}

输出信息如下:

{
"code": 400,
"msg": "数据验证不通过!",
"data": [
"名字长度不能超过10个字符!",
"邮箱格式错误!"
]
}

3.6 总结

两种方案实际上都是差不多的(实际上都是基于过滤器 Filter 的),可以根据个人需要选择。

其中 AspNetCore 默认实现的过滤器为 ModelStateInvalidFilter ,其 Order = -2000,可以根据程序实际情况,对程序内的过滤器顺序进行编排。

4 源码解读

4.1 基本介绍

AspNetCore 模型验证这一块相关的源码,主要是通过注册一个默认工厂 InvalidModelStateResponseFactory(由 ApiBehaviorOptionsSetupApiBehaviorOptions 进行配置,实际上是一个 Func),以及使用一个过滤器(为 ModelStateInvalidFilter,由 ModelStateInvalidFilterFactory 生成),来控制模型验证以及返回结果(返回一个 BadRequestObjectResultObjectResult)。

其中,最主要的是 ApiBehaviorOptionsSuppressModelStateInvalidFilterInvalidModelStateResponseFactory 属性。这两个属性,前者控制默认过滤器是否启用,后者生成模型验证的结果。

4.2 MvcServiceCollectionExtensions

新建的 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

4.3 ApiBehaviorOptionsSetup

主要代码如下:

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;
}

该方法最终会返回一个 BadRequestObjectResultObjectResult

4.4 ModelStateInvalidFilter

上面介绍完了 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 工厂的验证,获取返回结果。

模型验证最主要的源码就如上所述。

4.5 其他补充

(1)过滤器的执行顺序

默认过滤器的 Order 为 -2000,其触发时机一般是较早的(模型验证也是要尽可能早)。

过滤器管道的执行顺序:Order 值越小,越先执行 Executing 方法,越后执行 Executed 方法(即先进后出)。

(2)默认过滤器的创建和注册

这一部分个人没有细看,套路大概是这样的:通过过滤器提供者(DefaultFilterProvider),获取实现 IFilterFactory 接口的实例,调用 CreateInstance 方法生成过滤器,并将过滤器添加到过滤器容器中(IFilterContainer)。

其中模型验证的默认过滤的工厂类为:ModelStateInvalidFilterFactory

5 示例代码

本文示例的完整代码,可以从这里获取:

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 官方文档>>高级>>模型验证

ASP.NET Core 6.0 基于模型验证的数据验证的更多相关文章

  1. 在ASP.NET Core 2.0中使用Facebook进行身份验证

    已经很久没有更新自己的技术博客了,自从上个月末来到天津之后把家安顿好,这个月月初开始找工作,由于以前是做.NET开发的,所以找的还是.NET工作,但是天津这边大多还是针对to B(企业)进行定制开发的 ...

  2. Asp.net core通过自定义特性实现双端数据验证的一些想法

    asp.net core集成了非常方便的数据绑定和数据校验机制,配合操作各种easy的vs,效率直接高到飞起. 通过自定义验证特性(Custom Validation Attribute)可以实现对于 ...

  3. asp.net core mvc视频A:笔记4-1.数据验证

    开发建议:永远不要相信客户端提交过来的数据!!! 前端数据验证定位:提高用户体验,仅此而已! 后端数据验证定位:保证系统安全与数据完整!!! 实例:用户登录验证 定义一个用户登录类 在用户登录类基础上 ...

  4. ASP.NET Core 1.0 静态文件、路由、自定义中间件、身份验证简介

    概述 ASP.NET Core 1.0是ASP.NET的一个重要的重新设计. 例如,在ASP.NET Core中,使用Middleware编写请求管道. ASP.NET Core中间件对HttpCon ...

  5. Asp.Net Core 2.0 项目实战(11) 基于OnActionExecuting全局过滤器,页面操作权限过滤控制到按钮级

    1.权限管理 权限管理的基本定义:百度百科. 基于<Asp.Net Core 2.0 项目实战(10) 基于cookie登录授权认证并实现前台会员.后台管理员同时登录>我们做过了登录认证, ...

  6. asp.net core 2.0 web api基于JWT自定义策略授权

    JWT(json web token)是一种基于json的身份验证机制,流程如下: 通过登录,来获取Token,再在之后每次请求的Header中追加Authorization为Token的凭据,服务端 ...

  7. 用VSCode开发一个基于asp.net core 2.0/sql server linux(docker)/ng5/bs4的项目(1)

    最近使用vscode比较多. 学习了一下如何在mac上使用vscode开发asp.netcore项目. 这里是我写的关于vscode的一篇文章: https://www.cnblogs.com/cgz ...

  8. Asp.Net Core 2.0 项目实战(10) 基于cookie登录授权认证并实现前台会员、后台管理员同时登录

    1.登录的实现 登录功能实现起来有哪些常用的方式,大家首先想到的肯定是cookie或session或cookie+session,当然还有其他模式,今天主要探讨一下在Asp.net core 2.0下 ...

  9. Asp.Net Core 2.0 项目实战(9) 日志记录,基于Nlog或Microsoft.Extensions.Logging的实现及调用实例

    本文目录 1. Net下日志记录 2. NLog的使用     2.1 添加nuget引用NLog.Web.AspNetCore     2.2 配置文件设置     2.3 依赖配置及调用     ...

随机推荐

  1. Java学习笔记-学生管理系统

    Java学习笔记 一个Student类 public class Student { private String sid; private String name; private String a ...

  2. 一个关于 useState 的误解

    一个关于 useState 的误解 本文写于 2020 年 11 月 17 日 前两天有人问了我一个问题,他有一段这样的代码: function App() { const [n, setN] = u ...

  3. 141. Linked List Cycle - LeetCode

    Question 141. Linked List Cycle Solution 题目大意:给一个链表,判断是否存在循环,最好不要使用额外空间 思路:定义一个假节点fakeNext,遍历这个链表,判断 ...

  4. 好客租房46-react组件进阶目标

    1能够使用props接收数据 2能够使用父子组件之间的通讯 3能够实现兄弟组件之间的通讯 4能够给组件添加props校验 5能够说出生命周期常用的钩子函数 6能够知道高阶组件的作用 组件通讯介绍 组件 ...

  5. v87.01 鸿蒙内核源码分析 (内核启动篇) | 从汇编到 main () | 百篇博客分析 OpenHarmony 源码

    本篇关键词:内核重定位.MMU.SVC栈.热启动.内核映射表 内核汇编相关篇为: v74.01 鸿蒙内核源码分析(编码方式) | 机器指令是如何编码的 v75.03 鸿蒙内核源码分析(汇编基础) | ...

  6. 看Spring源码不得不会的@Enable模块驱动实现原理讲解

    这篇文章我想和你聊一聊 spring的@Enable模块驱动的实现原理. 在我们平时使用spring的过程中,如果想要加个定时任务的功能,那么就需要加注解@EnableScheduling,如果想使用 ...

  7. Linux 中递归删除文件

    递归删除当前目录下以 .json 结尾的文件 find . -name "*.json" | xargs rm -f find . -name "*.json" ...

  8. jenkins 流水线自动化部署 手动下载安装插件包

    如果有些插件不能通过可选插件安装,可以进行选择高级并上传插件包,插件包链接地址为:http://updates.jenkins-ci.org/download/plugins/ 同时在高级中可以更换下 ...

  9. 16.Nginx优化与防盗链

    Nginx优化与防盗链 目录 Nginx优化与防盗链 隐藏版本号 修改用户与组 缓存时间 日志切割 小知识 连接超时 更改进程数 配置网页压缩 配置防盗链 配置防盗链 隐藏版本号 可以使用 Fiddl ...

  10. 7.脚本三剑客之awk

    脚本三剑客之awk 目录 脚本三剑客之awk awk介绍 awk工作原理 awk命令格式 awk基础用法 awk命令高级用法 date命令使用 awk介绍 AWK 是一种处理文本文件的语言,是一个强大 ...