5

ASP.NET Core 6.0 基于模型验证的数据验证 - 芦荟柚子茶

 2 years ago
source link: https://www.cnblogs.com/clis/p/16515512.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

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

本文描述的数据验证方案,是基于官方的模型验证(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 官方文档>>高级>>模型验证


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK