前言
之前写过一点关于自定义模型校验的,此处 ,但是里面的方案一、方案二只是局限于具体的一个类,不具有通用性,那么如果针对比较通用的模型校验,该如何处理?.
操作
示例环境:vs2022、.Net6
列举场景:当前我们要添加用户考试科目的成绩,所以我们有了下面的接口。
入参
/// <summary>
/// 添加用户成绩请求类
/// </summary>
public class AddUserGradeVm
{
/// <summary>
/// 用户ID
/// </summary>
[Required]
public int UserId { get; set; }
/// <summary>
/// 科目ID
/// </summary>
[Required]
public int Subject { get; set; }
/// <summary>
/// 成绩
/// </summary>
[Required]
public double Grade { get; set; }
}
接口实现
[HttpPost]
public int AddUserGrade(AddUserGradeVm request)
{
_logger.LogInformation($"用户ID:{request.UserId} 科目{request.Subject} 成绩是:{request.Grade}");
return 1;
}
看咱们的入参,都是值类型,虽然我们对属性标注了Required特性,但是值类型默认值为0,所以当你不传的时候就直接取了默认值。输出结果为
用户ID:0 科目0 成绩是:0
这个时候我们可以写if判断来限制值必须大于0等,那么如果项目里面这种场景比较多,就有点繁琐了。
方案一
我们可以模仿着MaxLengthAttribute去继承ValidationAttribute编写一个ValidMinValueAttribute
/// <summary>
///验证最小值
/// </summary>
[AttributeUsage(AttributeTargets.All, AllowMultiple = false)]
public class ValidMinValueAttribute : ValidationAttribute
{
private readonly int _minValue;
public ValidMinValueAttribute(int minValue)
{
_minValue = minValue;
}
public override bool IsValid(object? value)
{
if (value == null)
{
return false;
}
if (value is not int valueAsInt)
{
return false;
}
if (valueAsInt <= _minValue)
{
return false;
}
return true;
}
}
使用方法
[ValidMinValue(1, ErrorMessage = "值必须大于等于1")]
[Required]
public int UserId { get; set; }
当我们请求接口参数参数都是0的时候,返回结果
{
"errors": {
"UserId": [
"值必须大于等于1"
]
},
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-869a6b371a821f168a11eb06085950b6-9c9e21e35a8614c5-00"
}
方案二
ASP.NET Core默认包含了内置模型的校验,也就是说当你传递校验不通过的值时候,会在未到达Action之前已经帮你过滤处理,这样子的返回格式和我们封装的公共返回类不一样,所以就是需要禁用默认的校验,然后自己写过滤器校验,如果是自定义(自己编写校验特性,而不模仿官方的)的那么就只能处理了。
小疑问:那么框架是在什么地方帮我们做的校验那?是否可以从那个地方入手处理?回头再研究一下。
思路:自己编写特定校验搭配过滤器使用。
准备
示例中使用的返回类格式是
/// <summary>
/// 返回类模型
/// </summary>
public class ResultModel : IResultModel
{
private string _message;
private string _code;
private IEnumerable<ErrorInfo> _error;
/// <summary>
/// 是否成功
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
///状态码
/// </summary>
public virtual string Code
{
get => string.IsNullOrWhiteSpace(_code) ? string.Empty : _code;
set => _code = value;
}
/// <summary>
/// 消息
/// </summary>
public virtual string Message
{
get => string.IsNullOrEmpty(_message) ? string.Empty : _message;
set => _message = value;
}
/// <summary>
/// 错误信息 model验证错误时候输入
/// </summary>
public IEnumerable<ErrorInfo> Errors
{
get => _error is null ? Enumerable.Empty<ErrorInfo>() : _error;
set => _error = value;
}
}
增加校验特性以及一个实现类(仅做演示使用的)
/// <summary>
/// 抽象根特性
/// </summary>
[AttributeUsage(AttributeTargets.All, AllowMultiple = false)]
public abstract class AbstractAttribute : Attribute
{
/// <summary>
/// 抽象验证方法
/// </summary>
/// <param name="oValue"></param>
/// <returns></returns>
public abstract IResultModel Validate(object oValue);
}
/// <summary>
/// 最小值校验特性 继承抽象特性,并重写Validate方法
/// </summary>
[AttributeUsage(AttributeTargets.All, AllowMultiple = false)]
public class ValidMinValue2Attribute : AbstractAttribute
{
private readonly string _errorMessage;
private readonly int _minValue;
public ValidMinValue2Attribute(int minValue, string errorMessage = "最小值不符合要求")
{
_errorMessage = errorMessage;
_minValue = minValue;
}
public override IResultModel Validate(object oValue)
{
if (oValue == null)
{
return new ResultModel
{
Code = "400",
IsSuccess = false,
Message = "参数无效"
};
}
if (oValue is not int valueAsInt)
{
return new ResultModel
{
Code = "400",
IsSuccess = false,
Message = "参数格式不对"
};
}
if (valueAsInt <= _minValue)
{
return new ResultModel
{
Code = "400",
IsSuccess = false,
Message = _errorMessage
};
}
return new ResultModel
{
Code = "200",
IsSuccess = true,
Message = string.Empty
};
}
}
修改之前的Action过滤器
/// <summary>
/// 模型验证Action过滤器
/// </summary>
public class ModelVerifyFilter : ActionFilterAttribute
{
// 当不关闭默认的框架验证的时候,进不来这点都已经校验返回了
//services.Configure<ApiBehaviorOptions>(options => options.SuppressModelStateInvalidFilter = true);
public override void OnActionExecuting(ActionExecutingContext context)
{
//因为我们还包含自定义的校验处理,所以这需要注释
//if (context.ModelState.IsValid)
//return;
var errorResults = new List<ErrorInfo>();
//默认的模型校验
foreach (var (key, value) in context.ModelState)
{
var result = new ErrorInfo
{
Field = key,
};
foreach (var error in value.Errors)
{
if (!string.IsNullOrEmpty(result.Message))
{
result.Message += '|';
}
result.Message += error.ErrorMessage;
}
errorResults.Add(result);
}
//自定义的模型校验处理
foreach (var item in context.ActionArguments)
{
var type = item.Value?.GetType();
if (type?.IsClass != true)
{
continue;
}
foreach (var property in type.GetProperties().Where(t => t.IsDefined(typeof(AbstractAttribute), true)))
{
foreach (AbstractAttribute attribute in property.GetCustomAttributes<AbstractAttribute>())
{
var value = property.GetValue(item.Value);
if (value == null)
continue;
var info = attribute.Validate(value);
if (info.IsSuccess)
continue;
var result = new ErrorInfo
{
Field = property.Name,
Message = info.Message
};
errorResults.Add(result);
}
}
}
if (errorResults.Count == 0)
return;
context.Result = new BadRequestObjectResult(new ResultModel
{
Code = StatusCodes.Status400BadRequest.ToString(),
Errors = errorResults,
Message = "参数格式不正确"
});
}
public override void OnActionExecuted(ActionExecutedContext context)
{
}
}
当然这里别忘了关闭默认的模型校验
//关闭模型校验
services.Configure<ApiBehaviorOptions>(options => options.SuppressModelStateInvalidFilter = true);
上面的准备工作结束,下面正式开始
编写接口测试
我们有一个接口AddUserGrade2,该接口标注了过滤器ModelVerifyFilter,当然你如果想可以全局配置。
/// <summary>
/// 考虑通过过滤器的方式增加自定义校验
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
[HttpPost]
[ModelVerifyFilter]
public IResultModel<int> AddUserGrade2(AddUserGradeVm2 request)
{
_logger.LogInformation($"用户ID:{request.UserId} 科目{request.Subject} 成绩是:{request.Grade}");
return ResultModel<int>.Success(1);
}
我们的入参AddUserGradeVm2如下
/// <summary>
/// 添加用户成绩请求类
/// </summary>
public class AddUserGradeVm2
{
/// <summary>
/// 用户ID
/// </summary>
[ValidMinValue2(1, "用户ID必须大于等于1")]
[Required]
public int UserId { get; set; }
/// <summary>
/// 科目ID
/// </summary>
[Required]
[ValidMinValue2(1, "科目ID必须大于等于1")]
public int Subject { get; set; }
/// <summary>
/// 成绩
/// </summary>
[Required]
public double Grade { get; set; }
/// <summary>
/// 备注
/// </summary>
[MinLength(2)]
[Required]
public string Remark { get; set; }
}
这个里面包含了内置的模型校验MinLength以及我们自定义的ValidMinValue2,下面开始演示
示例1:
当我们入参为
{
"userId": 0,
"subject": 0,
"grade": 0,
"remark": ""
}
返回值为
{
"isSuccess": false,
"code": "400",
"message": "参数格式不正确",
"errors": [
{
"field": "Remark",
"message": "The Remark field is required.|The field Remark must be a string or array type with a minimum length of '2'."
},
{
"field": "UserId",
"message": "用户ID必须大于等于1"
},
{
"field": "Subject",
"message": "科目ID必须大于等于1"
}
]
}
示例2:
当我们入参为
{
"userId": 0,
"subject": 0,
"grade": 0,
"remark": "123"
}
返回值为
{
"isSuccess": false,
"code": "400",
"message": "参数格式不正确",
"errors": [
{
"field": "UserId",
"message": "用户ID必须大于等于1"
},
{
"field": "Subject",
"message": "科目ID必须大于等于1"
}
]
}
示例3:
当我们入参为:
{
"userId": 716,
"subject": 10,
"grade": 99.9,
"remark": "123"
}
返回值为
{
"data": 1,
"isSuccess": true,
"code": "200",
"message": "success",
"errors": []
}
上面已经实现了我们的需要,自定义模型校验,并且兼具通用性。
总结
这两个方法都可以实现做自定义模型校验,第一种直接继承ValidationAttribute实现起来更快,第二种的话感觉可扩展性更高。具体选择根据实际情况选择。
最后感谢歪老师和二胖老师提供的思路。