首页 > 编程语言 >ASP.NET Core 6.0 基于模型验证的数据验证

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

时间:2023-02-01 22:12:09浏览次数:60  
标签:Core ASP 验证 模型 services context 过滤器 public

https://zhuanlan.zhihu.com/p/551581094

 

1.1、数据验证的场景

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

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

    return "验证通过!";
}

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

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

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

临时加更干货分享

大家能看到这里,已是对我们的支持了。分享一组7月录制的C#零基础教程。我们喜欢做这样的分享,它足够的基础,对新手友好。如果需要的话,就来免费领取吧!

快来领取吧

资料免费自取:

由于内容过多不便呈现,需要视频教程和配套源码的小伙伴,可点击这里,添加我知乎主页个人说明处号码 免费分享

也可直接点击下方卡片:点击后可自动复制威芯号,并跳转到威芯。得辛苦大家自行搜索威芯号添加。内容已做打包,添加后直接发送注意查收!

    点击卡片,复制微信号添加 免费领取干货  

兴致上来了做了张图,也是干货清单,需要的小伙伴直接来领就是了。

包含VS2022安装包 / C#基础 .NET6/WPF/Winform零基础到各类实战!

继续正文

1.2、本文的脉络

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

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

二、模型验证

2.1、介绍

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

当验证不通过时,将会返回验证不通过的错误信息。

其中,除了内置的验证特性,用户也可以自定义验证特性(本文不展开),具体请自行查看自定义特性:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/validation?view=aspnetcore-6.0#custom-attributes一节。

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

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

在 API 中,只要控制器拥有 ApiController:https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute 特性,如果模型验证不通过,将自动返回包含错误信息的 HTTP400 相应,详细请参阅自动 HTTP 400 响应:https://docs.microsoft.com/zh-cn/aspnet/core/web-api/?view=aspnetcore-6.0#automatic-http-400-responses

2.2、基本使用

自定义模型

如下代码中,[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": [
      "邮箱格式错误!"
    ]
  }
}

2.3、内置特性

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

  • [ValidateNever]:指示属性或参数应从验证中排除。
  • [CreditCard]:验证属性是否具有信用卡格式。
  • [Compare]:验证模型中的两个属性是否匹配。
  • [EmailAddress]:验证属性是否具有电子邮件格式。
  • [Phone]:验证属性是否具有电话号码格式。
  • [Range]:验证属性值是否在指定的范围内。
  • [RegularExpression]:验证属性值是否与指定的正则表达式匹配。
  • [Required]:验证字段是否不为 null。
  • [StringLength]:验证字符串属性值是否不超过指定长度限制。
  • [URL]:验证属性是否具有 URL 格式。
  • [Remote]:通过在服务器上调用操作方法来验证客户端上的输入。

可以在命名空间中找到System.ComponentModel.DataAnnotations:https://docs.microsoft.com/zh-cn/dotnet/api/system.componentmodel.dataannotations验证属性的完整列表。

三、自定义数据验证返回结果

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自定义过滤器

自定义过滤器

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

3.5、测试

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

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

输出信息如下:

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

3.6、总结

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

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

四、源码解读

4.1、基本介绍

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

简单描述一下数据流向:

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

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

所以,本文第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<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;
}

该方法最终会返回一个 BadRequestObjectResult 或 ObjectResult

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

五、示例代码

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

Gitee:https://gitee.com/lisheng741/testnetcore/tree/master/Filter/DataAnnotationTest

Github:https://github.com/lisheng741/testnetcore/tree/master/Filter/DataAnnotationTest

转自:芦荟柚子茶
链接:http://cnblogs.com/clis/p/16515512.html

临时加更干货分享

大家能看到这里,已是对我们的支持了。分享一组7月录制的C#零基础教程。我们喜欢做这样的分享,它足够的基础,对新手友好。如果需要的话,就来免费领取吧!

快来领取吧

资料免费自取:

由于内容过多不便呈现,需要视频教程和配套源码的小伙伴,可点击这里,添加我知乎主页个人说明处号码 免费分享

也可直接点击下方卡片:点击后可自动复制威芯号,并跳转到威芯。得辛苦大家自行搜索威芯号添加。内容已做打包,添加后直接发送注意查收!

    点击卡片,复制微信号添加 免费领取干货  

兴致上来了做了张图,也是干货清单,需要的小伙伴直接来领就是了。

包含VS2022安装包 / C#基础 .NET6/WPF/Winform零基础到各类实战!

标签:Core,ASP,验证,模型,services,context,过滤器,public
From: https://www.cnblogs.com/chinasoft/p/17084290.html

相关文章

  • ElementUI 中 el-form 验证 el-upload 上传图片校验不通过
    ————————————————版权声明:本文为CSDN博主「xiaoxiaodeDream」的原创文章,遵循CC4.0BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog......
  • efcore 问题集
    一、迁移    ①、数据迁移1、错误信息(附截图): Couldnotloadassembly'xxx.EntityFramework'.Ensureitisreferencedbythestartupproject'xxx.api'.......
  • Blazor入门100天 : 身份验证和授权 (1)
    目录建立默认带身份验证Blazor程序<AuthorizeView>组件,检查登录信息,级联参数获取身份验证状态数据,基于角色或基于策略的授权,可以在Razor组件中使用[Authorize]......
  • SMSUtils阿里云短信验证码java-api
    <!--阿里云短信服务--><dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>4.5.16</version></dependency><depe......
  • java正则表达式不包含特殊字符验证
    原文链接:https://blog.csdn.net/weixin_39625782/article/details/114674258packagecom.sodii.regex.demo;importjava.util.regex.Matcher;importjava.util.regex.Pa......
  • 项目运行报错:目标进程已退出,但未引发 CoreCLR 启动事件。
      项目运行报错:目标进程已退出,但未引发CoreCLR启动事件。请确保将目标进程配置为使用.NETCore。如果目标进程未运行.NETCore,则发生这种情况并不意外。 解决方......
  • ASP.NET 前端大文件上传
    ​ 以ASP.NETCoreWebAPI 作后端 API ,用 Vue 构建前端页面,用 Axios 从前端访问后端 API,包括文件的上传和下载。 准备文件上传的API #region 文件上传......
  • tinycore树莓派蓝牙模块的相关操作
    修改config.txt成下面的样子:[PI0]dtoverlay=miniuart-btenable_uart=1core_freq=250然后:tc@box:~$sudomodprobehci_uartmodprobe:can'tloadmoduleecc(kernel.tc......
  • Jenkins和Gitlab做完key验证依旧报错
    报错内容无法连接仓库:Command"[email protected]:gitlab-instance-edd44d36/music.gitHEAD"returnedstatuscode128:stdout:stderr:......
  • netcore之异步并不是多线程!
    1、遇到await,线程的变化遇到await会把当前线程返回且返回值就是await后面的Task,再从线程池随机取一个线程往下执行代码。我们使用封装好的异步方法模拟写入大量字符串的......