• ASP.NET Core 6.0 基于模型验证的数据验证


    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(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(options =>
    {
        // 禁用默认模型验证过滤器
        options.SuppressModelStateInvalidFilter = true;
    });
    

    (3)启用自定义过滤器

    在 Program.cs 中:

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

    3.5 测试

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

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

    输出信息如下:

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

    3.6 总结

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

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


    4 源码解读

    4.1 基本介绍

    AspNetCore 模型验证这一块相关的源码,主要是使用一个默认过滤器(为 ModelStateInvalidFilter,由 ModelStateInvalidFilterFactory 生成),在经过默认过滤器,判定是否模型验证不通过,若验证不通过,将会调用一个默认工厂 InvalidModelStateResponseFactory(由 ApiBehaviorOptionsSetupApiBehaviorOptions 进行配置,实际上是一个 Func),来产生模型验证的返回结果(返回一个 BadRequestObjectResultObjectResult)。

    简单描述一下数据流向:

    用户请求 >> ModelStateInvalidFilter >> InvalidModelStateResponseFactory >> BadRequestObjectResult

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

    所以,本文第3部门提及的两种自定义返回结果的方案,要么是自定义一个新的过滤器并禁用默认的过滤器,要么是替换生成模型验证结果的工厂。

    下面将相关的源码贴出。

    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());
        }
    
        return builder;
    }
    

    其中相关的是 AddMvcCore()

    // MvcServiceCollectionExtensions.cs 
    public static IMvcCoreBuilder AddMvcCore(this IServiceCollection services)
    {
        if (services == null)
        {
            throw new ArgumentNullException(nameof(services));
        }
    
        var environment = GetServiceFromCollection(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, 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();
                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 官方文档>>高级>>模型验证

  • 相关阅读:
    SpringBoot_RestTemplate使用总结
    swagger stub https无法访问
    别说你不会网络请求了,6000字入门Axios框架
    基础SQL 函数
    Anaconda安装+Tensorflow2.0安装配置+Pycharm安装+GCN调试(Window 10)
    c语言练习73:统计位数为偶数的数字
    mysql 高性能搭建2-2: mysql5.7.29 主主复制+keepalived实现高可用
    python-web开发[16-18]之Django开发
    C/C++的刷题练习之牛客网,一个友好的网站
    C++编程练习系列(1)C++和标准库速成
  • 原文地址:https://www.cnblogs.com/clis/p/16515512.html