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. 1.ArrayList和LinkedList区别

    说⼀下ArrayList和LinkedList区别 具体区别 1.1. ⾸先,他们的底层数据结构不同,ArrayList底层是基于数组实现的,LinkedList底层是基于链表实现的 1.2. 由于底 ...

  2. 【系统】Windows相关软件下载

    Windows相关软件下载 Visual Studio Windows SQL Server https://my.visualstudio.com/Downloads/Featured?mkt=zh ...

  3. Codeforces Round #793 (Div. 2)

    C. LIS or Reverse LIS? D. Circular Spanning Tree E. Unordered Swaps F MCMF?

  4. net core天马行空系列-微服务篇:全声明式http客户端feign快速接入微服务中心nacos

    1.前言 hi,大家好,我是三合,距离上一篇博客已经过去了整整两年,这两年里,博主通关了<人生>这个游戏里的两大关卡,买房和结婚.最近闲了下来,那么当然要继续写博客了,今天这篇博客的主要内 ...

  5. (干货)基于 veImageX 搭建海报生成平台 -- 附源码

    前言 618 年中促销即将来临,很多公司都会通过海报来宣传自己的促销方案,通常情况下海报由设计团队基于 PS.Sketch 等工具创作,后期若想替换海报文案.商品列表等内容则需打开原工程进行二次创作, ...

  6. 阶段性总结 GDOI 2022 PJ

    阶段性总结 GDOI 2022 PJ 比赛经过 Day ? ~ Day -1 半停课集训,补了很多东西,但是之前漏得太多了,结果是还有很多题没改 打了若干场 AtCoder ,承认自己思维的不足,训练 ...

  7. 并发bug之源(一)-可见性

    CPU三级缓存 要聊可见性,这事儿还得从计算机的组成开始说起,我们都知道,计算机由CPU.内存.磁盘.显卡.外设等几部分组成,对于我们程序员而言,写代码主要关注CPU和内存两部分.放几张马士兵老师的图 ...

  8. ExtJS 布局-VBox布局(VBox layout)

    更新记录: 2022年6月11日 优化文章结构. 2022年6月9日 发布. 2022年6月1日 开始. 1.说明 vbox布局类似auto布局,将子组件一个接一个垂直向下放置,既可以在水平方向也可以 ...

  9. 慢到不能忍?别忍了,Ubuntu 21.10 APT 源修改为华为云镜像源

    更新记录 2022年4月15日:本文迁移自Panda666原博客,原发布时间:2021年3月29日. 2022年4月15日:将源改为华为云,华为云更方便.Ubuntu从20.04更新到21.10. 切 ...

  10. 循序渐进 Redis 分布式锁(以及何时不用它)

    场景 假设我们有个批处理服务,实现逻辑大致是这样的: 用户在管理后台向批处理服务投递任务: 批处理服务将该任务写入数据库,立即返回: 批处理服务有启动单独线程定时从数据库获取一批未处理(或处理失败)的 ...