ASP.Net Core MVC 之模型验证

在我们日常的表单中,我们总需要写很多的验证,这个是避免不了的,不写,安全上过不去,写了,又很繁琐。

我们不仅前端要写验证,后端也需要写验证,才能杜绝非法数据入侵。

例如下面的代码:在服务层的保存方法中进行参数验证

 var jsonCode = new JsonCode { code = -1, msg = "校验失败" };
            if (dtoModel.UserId.IsNullOrEmpty())
            {
                jsonCode.msg = "请填写用户ID!";
                return jsonCode;
            }

            if (string.IsNullOrWhiteSpace(dtoModel.CommitmentFileUrl))
            {
                jsonCode.msg = "请提交科研诚信承诺书!";
                return jsonCode;
            }

            if (dtoModel.CompanyName.IsNullOrEmpty())
            {
                jsonCode.msg = "请填写技术转移机构名称!";
                return jsonCode;
            }

            if (!Regex.IsMatch(dtoModel.CompanyName, @"^[\S\s]{4,20}$"))
            {
                jsonCode.msg = "请输入4-20个字的技术转移机构名称";
                return jsonCode;
            }

            if (dtoModel.CreditCode.IsNullOrEmpty())
            {
                jsonCode.msg = "请填写统一社会信用代码!";
                return jsonCode;
            }

当表单字段多的时候,这个就写得很长,而且发布时候写一次,审核的后台也需要再写一次。是不是很繁琐。

下面就进入主题,介绍模型验证的应用(微软自带):

引入命名空间:System.ComponentModel.DataAnnotations

基础验证规则,请参考官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/validation?view=aspnetcore-6.0

这里仅讲解在项目中怎么应用和自定义显示验证信息

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace 模型验证.Models
{
    [Serializable]
    public class CreateUserDto
    {
        [Required(ErrorMessage ="请填写姓名")]
        public string Name { get; set; }
    }
}

 

我们只需在实体上定义各个字段的校验规则,接下来就是在控制器中应用

 public IActionResult Index(CreateUserDto user)//参数必须以对象形式,一般是createDto,即对应表达的字段内容。
        {
           if(!ModelState.IsValid)
            {
               foreach(var item in  ModelState.Values) //循环验证每个对象
                {
                    if (item.ValidationState == ModelValidationState.Invalid) //判断是否是模型绑定
                    {
                        return new ContentResult(){Content= item.Errors.FirstOrDefault().ErrorMessage }; //返回第一个字段的错误信息
                    }
                       
                }
              
            }
            return View();
        }

运行起来:

 

显示验证信息。

 如果我们加上Name参数呢,验证通过,显示页面。

 

 

以上是一个简单的例子,讲了在哪里定义验证规则,以及怎么获取自定义的验证信息来显示。

但是在实际项目中,我们不可能在每个控制器中去写这么一串代码输出异常。

实际项目中,我们需要用到过滤器了,下面定义一个过滤器:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace 模型验证.Models
{
    public class ValidateFilter : IActionFilter
    {
        public void OnActionExecuted(ActionExecutedContext context)
        {
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            if (!context.ModelState.IsValid)
            {
                string msg = "";
                foreach (var item in context.ModelState.Values)
                {
                    if (item.ValidationState == ModelValidationState.Invalid)
                    {
                      
                        if (item.Errors != null && item.Errors.Any())
                        {
                            msg = item.Errors.FirstOrDefault().ErrorMessage;
                        }

                        break;
                    }
                }
                context.Result = new JsonResult(new { code=-1,msg=msg}); //以json形式返回,在项目中也可以根据需要处理,比如ajax请求的,返回json,否则返回文本内容
            }
        }
    }
}

 

 接着在startup.cs中应用过滤器:

 public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews(options=> {
                options.Filters.Add<ValidateFilter>();
            });
        }

这时候,控制器中的代码就可以去掉了

  public IActionResult Index(CreateUserDto user)
        {
            return View();
        }

 

运行起来,就会执行过滤器的验证规则进行验证了。

这样大大减少了我们后端做验证的复杂度。

 

有了内置的验证属性之外,我们也可以自定义验证属性,还有模型内的验证,比如判断字段之间的关系;如下代码

继承 IValidatableObject, 编写Validate方法

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace 模型验证.Models
{
    [Serializable]
    public class CreateUserDto: IValidatableObject
    {
        [Required(ErrorMessage ="请填写姓名")]
        public string Name { get; set; }

        public string Desc { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            if(Name==Desc)
            {
                yield return new ValidationResult(
                   "Name and Description can not be the same!",
                   new[] { nameof(Name), nameof(Desc) }
               );
            }
            if(Desc=="张三")
            {
                yield return new ValidationResult(
                   "Name and Description can not be the same!",
                   new[] { nameof(Name), nameof(Desc) }
               );
            }
        }
    }
}

 

 

上面界面的是全局的注入方式,但有些情况下,我们又不需要验证。

比如保存草稿的场景下,我们使用的是同一个模型。

保存草稿,必然就不需要全部必填。在点击“保存草稿”按钮时,服务端是不能去验证这个模型的。但是我们都全局注入了,那要怎么办呢?

我们修改验证的代码,改成带参数的。通过参数来判断是否验证。如下:

public class ValidateFilter : Attribute, IActionFilter
    {
        private  bool _skipValidation; //是否跳过验证

        public ValidateFilter(bool skipValidation=false)
        {
            _skipValidation = skipValidation;
        }

        public void OnActionExecuted(ActionExecutedContext context)
        {
           
        }

        public void OnActionExecuting(ActionExecutingContext context)
        {
            _skipValidation = context.ActionDescriptor.EndpointMetadata.Any(m => m is ValidateFilter validateModel && validateModel._skipValidation); //验证action中是否有过滤掉
            if (!_skipValidation)
            {
                if (!context.ModelState.IsValid)
                {
                    JsonCode jc = new JsonCode();
                    foreach (var item in context.ModelState.Values)
                    {
                        if (item.ValidationState == ModelValidationState.Invalid)
                        {
                            jc.code = -1000;//当模型验证失败后,返回-1000,方便前端拦截处理
                            if (item.Errors != null && item.Errors.Any())
                            {
                                jc.msg = item.Errors?.FirstOrDefault()?.ErrorMessage;
                            }
                            break;
                        }
                    }
                    context.Result = new JsonResult(jc);
                }
            }
          
        }
    }

 

上面代码中,我们定义了一个_skipValidation的参数来处理是否验证。,默认为false,即不跳过验证。

全局注入照常,我们只需要在控制器中再次引用验证属性即可。

比如在对应的不需要验证的方法上

 [ValidateFilter(skipValidation: true)]
        public async Task<JsonResult> Save(string id, CreateRenCaiDto inputDto)

那假如,该方法体中,我们是动态判断是否要验证的。比如根据前端传递过来的参数,判断是否要验证。

我们可以在代码中这么写

  if (!inputDto.IsDraft) //非草稿,验证模型
            {
                if(!ModelState.IsValid)
                {
                    return BadValidateResult(ModelState);
                }
            }

 

这样,即会再次验证。

 /// <summary>
        /// 手动模型验证返回的信息
        /// </summary>
        /// <param name="ModelState"></param>
        /// <returns></returns>
        public JsonResult BadValidateResult(ModelStateDictionary ModelState)
        {
            JsonCode jc = new JsonCode();
            foreach (var item in ModelState.Values)
            {
                if (item.ValidationState == ModelValidationState.Invalid)
                {
                    jc.code = -1000;//当模型验证失败后,返回-1000,方便前端拦截处理
                    if (item.Errors != null && item.Errors.Any())
                    {
                        jc.msg = item.Errors?.FirstOrDefault()?.ErrorMessage;
                    }
                    break;
                }
            }

          return  new JsonResult(jc);
        }
JsonCode是自己定义的一个json返回的对象。

有几点需要注意:

1:对于验证规则不支持的,那只能在业务端自行判断,判断后输出。

2:该种方式,实现了业务端在表单验证中的无感,不需要去关注验证过程,大大提升这方面的效率。

3:在dto上,建议不用共用dto,表单输入创建一个CreateXXXDto,输出创建一个XXXDto。 哪怕一模一样也不要去共用,后期应对变化会灵活些。

对于CreateDto中一般不定义主键Id,而是跟表单UI中内容一致的。利用automapper进行实体间映射到数据库实体。

 

 

2023年04月20日补充

若是在代码中,我们想验证某一个对象

可以如下这么用:

public async Task<IActionResult> MyAction(MyModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    // To manually validate a specific model: 
   //重新验证,验证是自动的,但您可能希望手动重复验证。例如,您可能会计算某个属性的值,并希望在将属性设置为计算值后重新运行验证。要重新运行验证,请调用ModelStateDictionary.ClearValidationState以清除特定于正在验证的模型的验证,然后TryValidateModel
ModelState.ClearValidationState(nameof(MyModel));
if (!TryValidateModel(model, nameof(MyModel)))
{
 return BadRequest();
}
// Do something with the valid model... }
 private JsonResult BadRequest()
        {
            JsonCode jc = new JsonCode();
            foreach (var item in ModelState.Values)
            {
                if (item.ValidationState == ModelValidationState.Invalid)
                {
                    jc.code = -1000;//当模型验证失败后,返回-1000,方便前端拦截处理
                    if (item.Errors != null && item.Errors.Any())
                    {
                        jc.msg = item.Errors?.FirstOrDefault()?.ErrorMessage;
                    }
                    break;
                }
            }
            return Json(jc);
            
        }
TryValidateModel 是控制器基类自带的方法,不在控制器中的,调用不到这个方法。

若是在abp 框架的创建的webapi项目,要使得模型验证有效,需要在 ConfigureServices  方法中添加以下代码
 context.Services.Configure<ApiBehaviorOptions>(options => { 
             options.SuppressModelStateInvalidFilter = true;
            });

 

.net模型验证文档
https://learn.microsoft.com/zh-cn/aspnet/core/mvc/models/validation?view=aspnetcore-6.0 

 

更多分享,请大家关注我的个人公众号:

posted @ 2022-07-13 14:28  黄明辉  阅读(438)  评论(0编辑  收藏  举报