.NET之模型綁定和驗(yàn)證
介紹
模型綁定就是接收將來(lái)自HTTP請(qǐng)求的數(shù)據(jù)映射到模型的過(guò)程。如果找不到模型屬性的值,并不會(huì)報(bào)錯(cuò),而是給該屬性設(shè)置默認(rèn)值。
示例:比如我們有一個(gè)接口為
[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)
這個(gè)時(shí)候你的請(qǐng)求為:http://localhost:5000/api/pets/2?DogsOnly=true
路由系統(tǒng)選擇該Action后,模型綁定會(huì)執(zhí)行以下的步驟:
查找 GetByID的第一個(gè)參數(shù),該參數(shù)是一個(gè)名為id的整數(shù)。查找 HTTP 請(qǐng)求中的可用源,并在路由數(shù)據(jù)中查找 id=“2”。將字符串“2”轉(zhuǎn)換為整數(shù) 2。 查找 GetByID的下一個(gè)參數(shù),該參數(shù)是一個(gè)名為dogsOnly的布爾值。查找源,并在查詢字符串中查找“DogsOnly=true”。名稱匹配不區(qū)分大小寫。 將字符串“true”轉(zhuǎn)換為布爾值 true。
最后會(huì)調(diào)用GetById方法,參數(shù)Id為2,參數(shù)dogsOnly為true。
源
默認(rèn)情況下,模型綁定以鍵值對(duì)的形式從HTTP請(qǐng)求中的以下源中獲取數(shù)據(jù):
表單域 請(qǐng)求正文 路由數(shù)據(jù) 查詢字符串參數(shù) 上傳的文件
對(duì)于每個(gè)參數(shù),按照順序掃描源。也可以直接指定源
[FromQuery] - 從查詢字符串獲取值。 [FromRoute] - 從路由數(shù)據(jù)獲取值。 [FromForm] - 從發(fā)布表單字段中獲取值。 [FromBody] - 從請(qǐng)求正文獲取值。 [FromHeader] - 從 HTTP 標(biāo)頭獲取值。
示例:
[HttpGet]
public async Task<User> GetAsync([FromQuery]string id)
[HttpGet]
public async Task<User> GetAsync([FromRoute]string id)
[HttpGet]
public async Task<User> GetAsync([FromForm]string id)
[HttpPost]
public async Task<ActionResult<string>> AddAsync([FromBody]AddUserVm dto)
public void OnGet([FromHeader(Name = "Accept-Language")] string language)
也可以編寫自定義的值提供程序,比如從cookie中獲取會(huì)話狀態(tài),參考:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/model-binding?view=aspnetcore-5.0#additional-sources
模型綁定
簡(jiǎn)單模型綁定
例如:bool、byte、char、DateTime、DateTimeOffset、float、enum、guid、int、TimeSpan、Url、Version等
復(fù)雜類型
使用復(fù)雜類型必須具有要綁定的公共默認(rèn)構(gòu)造函數(shù)和公共可寫屬性。進(jìn)行模型綁定時(shí)候,將使用公共默認(rèn)構(gòu)造函數(shù)來(lái)實(shí)例化類。對(duì)于復(fù)雜類型的每個(gè)屬性,模型綁定會(huì)查找名稱模式 prefix.property_name 的源。如果未找到,它將僅查找不含前綴的 properties_name。不過(guò)一般我們使用都是進(jìn)行完全匹配,特殊需求才會(huì)做此操作。
參考資料:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/model-binding?view=aspnetcore-5.0#complex-types
內(nèi)置自定義模型綁定
通過(guò)ByteArrayModelBinder 可以實(shí)現(xiàn)將傳輸?shù)腷ase64編碼字符串轉(zhuǎn)換為字節(jié)數(shù)組。
比如:
[HttpPost]
public void Post([FromForm] byte[] file, string filename)
{
var trustedFileName = Path.GetRandomFileName();
var filePath = Path.Combine("e://", trustedFileName);
if (System.IO.File.Exists(filePath))
{
return;
}
System.IO.File.WriteAllBytes(filePath, file);
}
請(qǐng)求示例

接收結(jié)果

自定義模型綁定
示例場(chǎng)景:通過(guò)請(qǐng)求頭傳遞后端自定義的一種token,通過(guò)自定義模型綁定將token解析后綁定到請(qǐng)求模型。
參考資料:https://www.cnblogs.com/jyzhu/articles/8670536.html
請(qǐng)求接口示例
[HttpGet]
public ActionResult GetToken(TokenModel dto)
{
return Ok(dto);
}
首先定義token模型類
public class TokenModel
{
public int UserID { get; set; }
public string UserName { get; set; }
}
自定義模型綁定器
public class TokenModelBinder : IModelBinder
{
/// <summary>
/// 請(qǐng)求里傳遞參數(shù)token
/// </summary>
/// <param name="bindingContext"></param>
/// <returns></returns>
public Task BindModelAsync(ModelBindingContext bindingContext)
{
//參數(shù)必須包含token
if (!(bindingContext.ActionContext.HttpContext.Request.Headers.ContainsKey("token")))
return Task.CompletedTask;
var token = bindingContext.ActionContext.HttpContext.Request.Headers["token"];
//TODO 解析token
var result = new TokenModel()
{
UserID = 111,
UserName = "azrng",
};
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
定義token框架綁定器
public class TokenModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(TokenModel))
return new TokenModelBinder();
return null;
}
}
啟用綁定器
services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0, new TokenModelBinderProvider());
});
請(qǐng)求示例
var client = new RestClient("http://localhost:5000/api/ModelVerify/GetToken");
client.Timeout = -1;
var request = new RestRequest(Method.GET);
request.AddHeader("token", "123456");
IRestResponse response = client.Execute(request);
Console.WriteLine(response.Content);
結(jié)果就是可以在GetToken方法參數(shù)獲取到我們token的值。
模型校驗(yàn)
現(xiàn)在dotNetCore如果在控制器標(biāo)識(shí)[ApiController],那么就會(huì)在進(jìn)action前就會(huì)自動(dòng)校驗(yàn)?zāi)P皖惤壎ㄊ欠穹弦螅绻环弦笞詣?dòng)觸發(fā)HTTP400錯(cuò)誤響應(yīng)。原文
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
驗(yàn)證特性
通過(guò)驗(yàn)證特性可為屬性增加驗(yàn)證規(guī)則。不僅僅有內(nèi)置的驗(yàn)證特性,還可以實(shí)現(xiàn)自定義驗(yàn)證特性。
內(nèi)置驗(yàn)證特性
常用的有:必填、長(zhǎng)度驗(yàn)證、數(shù)值范圍、手機(jī)號(hào)碼、郵箱,還可以使用正則驗(yàn)證
public class AddModelVerify
{
[Display(Name = "名稱"), Required(ErrorMessage = "{0}不能為空")]// 非空校驗(yàn)
[MinLength(6, ErrorMessage = "名稱不能小于6位")] // 最小長(zhǎng)度校驗(yàn)
[MaxLength(10, ErrorMessage = "長(zhǎng)度不超過(guò)10個(gè)")] // 最大長(zhǎng)度校驗(yàn)
public string UserName { get; set; }
/// <summary>
/// 密碼
/// </summary>
[Display(Name = "密碼"), Required(ErrorMessage = "{0}不能為空")]
[MinLength(6, ErrorMessage = "密碼必須大于6位")]
public string PassWord { get; set; }
[Display(Name = "工號(hào)")] // 友好名稱錯(cuò)誤提示
[Required(ErrorMessage = "{0}不能為空")]
[StringLength(10, MinimumLength = 1, ErrorMessage = "{0}長(zhǎng)度是{1}")]
public string EmployeeNo { get; set; }
}
public IActionResult VerifyPhone([RegularExpression(@"^\d{3}-\d{3}-\d{4}$")] string phone)
除了上面這些還有其他內(nèi)置特性:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/validation?view=aspnetcore-5.0#built-in-attributes
請(qǐng)求地址傳入空值,輸出結(jié)果:HTTP錯(cuò)誤400
{
"errors": {
"PassWord": [
"密碼不能為空",
"密碼必須大于6位"
],
"UserName": [
"名稱不能為空",
"名稱不能小于6位"
],
"EmployeeNo": [
"工號(hào)不能為空",
"工號(hào)長(zhǎng)度是10"
]
},
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-d16b945b3e172a42bfe5b53d08f7487b-8d87c2ca238fdc4a-00"
}
還有一個(gè)Remote特性感覺挺有意思,使用場(chǎng)景是比如在ID上標(biāo)注遠(yuǎn)程特性,綁定時(shí)候自定驗(yàn)證ID是否有效
[AcceptVerbs("GET", "POST")]
public IActionResult VerifyID(string id)
{
if (!_userService.VerifyID(id))
{
return Json($"對(duì)象未找到");
}
return Json(true);
}
模型類使用指向操作方法的[Remote]特性注釋屬性
[Remote(action: "VerifyID", controller: "Users")]
public string ID { get; set; }
Remote其他用法:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/validation?view=aspnetcore-5.0#additional-fields
自定義特性
對(duì)于內(nèi)置驗(yàn)證特性無(wú)法處理的情況,我們可以創(chuàng)建自定義驗(yàn)證特性。
模擬場(chǎng)景:添加用戶時(shí)候,設(shè)置名字和工號(hào)不能一致,出生日期必須小于當(dāng)前時(shí)間
輸入模型類
public class AddUserinfoVm
{
[Display(Name = "名稱"), Required(ErrorMessage = "{0}不能為空")]
[MinLength(6, ErrorMessage = "名稱不能小于6位")]
[MaxLength(10, ErrorMessage = "長(zhǎng)度不超過(guò)10個(gè)")]
public string UserName { get; set; }
/// <summary>
/// 密碼
/// </summary>
[Display(Name = "密碼"), Required(ErrorMessage = "{0}不能為空")]
[MinLength(6, ErrorMessage = "密碼必須大于6位")]
public string PassWord { get; set; }
[Display(Name = "工號(hào)")]
[Required(ErrorMessage = "{0}不能為空")]
[StringLength(10, MinimumLength = 1, ErrorMessage = "{0}長(zhǎng)度是{1}")]
public string EmployeeNo { get; set; }
/// <summary>
/// 出生日期
/// </summary>
public DateTime Birthday { get; set; }
}
方案一:通過(guò)添加AddUserVerifyAttribute來(lái)實(shí)現(xiàn)
[AttributeUsage(AttributeTargets.All, AllowMultiple = false)]
public class AddUserVerifyAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var user = (AddUserinfoVm)validationContext.ObjectInstance;//user 變量表示 AddUserinfoVm 對(duì)象,其中包含表單提交中的數(shù)據(jù)
var date = (DateTime)value;
if (date > DateTime.Now)
{
return new ValidationResult("出生日期不能大于當(dāng)前時(shí)間");
}
if (user.UserName == user.EmployeeNo)
{
return new ValidationResult("名稱和工號(hào)不能一樣");
}
return ValidationResult.Success;
}
}
使用方法
[AddUserVerify]
public DateTime Birthday { get; set; }
方案二:模型類中繼承IValidatableObject,并實(shí)現(xiàn)Validate方法
/// <summary>
/// 屬性級(jí)別的自定義驗(yàn)證
/// </summary>
/// <param name="validationContext"></param>
/// <returns></returns>
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Birthday > DateTime.Now)
{
yield return new ValidationResult("出生日期不能大于當(dāng)前時(shí)間", new[] { nameof(Birthday) });
}
if (UserName == EmployeeNo)
{
yield return new ValidationResult("名稱和工號(hào)不能一樣", new[] { nameof(UserName), nameof(EmployeeNo) });
}
}
請(qǐng)求參數(shù):
{
"userName": "string",
"passWord": "string",
"employeeNo": "string",
"birthday": "2021-06-15T14:34:52.192Z"
}
輸出錯(cuò)誤信息
{
"errors": {
"Birthday": [
"出生日期不能大于當(dāng)前時(shí)間"
],
"UserName": [
"名稱和工號(hào)不能一樣"
],
"EmployeeNo": [
"名稱和工號(hào)不能一樣"
]
},
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-18854d59f6b6fc48b5c4c6a6dbe3802c-ba23f594f351a64d-00"
}
ModelState.IsValid
通過(guò)該方法可以實(shí)現(xiàn)對(duì)請(qǐng)求類驗(yàn)證是否滿足要求并做出相應(yīng)的響應(yīng)。
如果已經(jīng)使用[ApiController]標(biāo)識(shí),那么該方法就不在需要。
[HttpPost]
public ActionResult Add([FromBody] AddModelVerify dto)
{
//對(duì)請(qǐng)求類進(jìn)行驗(yàn)證特性
if (ModelState.IsValid)
{
//對(duì)請(qǐng)求類的值做出修改
dto.UserName = "azrng";
if (!TryValidateModel(dto))
{
//重新運(yùn)行驗(yàn)證失敗
return Ok("修改值后驗(yàn)證失敗");
}
return Ok("驗(yàn)證成功");
}
else
{
ModelState.AddModelError(string.Empty, "輸入有誤");
}
return Ok("");
}
禁用驗(yàn)證
/// <summary>
/// 創(chuàng)建不會(huì)將任何字段標(biāo)記為無(wú)效的 IObjectModelValidator 實(shí)現(xiàn)。
/// </summary>
public class NullObjectModelValidator : IObjectModelValidator
{
public void Validate(ActionContext actionContext,
ValidationStateDictionary validationState, string prefix, object model)
{
// 該方法故意為空
}
}
Startup.ConfigureServices中注入,以便替換依賴項(xiàng)注入容器中的默認(rèn) IObjectModelValidator 實(shí)現(xiàn)。
services.AddSingleton<IObjectModelValidator, NullObjectModelValidator>();
統(tǒng)一模型攔截器
增加ModelActionFiter過(guò)濾器
public class ModelActionFiter : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext context)
{
}
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
var errorResults = new List<ErrorResultDTO>();
foreach (var item in context.ModelState)
{
var result = new ErrorResultDTO
{
Field = item.Key,
};
foreach (var error in item.Value.Errors)
{
if (!string.IsNullOrEmpty(result.Message))
{
result.Message += '|';
}
result.Message += error.ErrorMessage;
}
errorResults.Add(result);
}
context.Result = new BadRequestObjectResult(new
{
Code = StatusCodes.Status400BadRequest,
Errors = errorResults
});
}
}
public class ErrorResultDTO
{
/// <summary>
/// 參數(shù)領(lǐng)域
/// </summary>
public string Field { get; set; }
/// <summary>
/// 錯(cuò)誤信息
/// </summary>
public string Message { get; set; }
}
}
參考文檔:https://www.cnblogs.com/minskiter/p/11601873.html
ConfigureServices中注冊(cè)過(guò)濾器并禁用默認(rèn)的自動(dòng)模型驗(yàn)證
services.AddControllers(options =>
{
options.Filters.Add<ModelActionFiter>(); //注冊(cè)過(guò)濾器
}).AddNewtonsoftJson().ConfigureApiBehaviorOptions(options =>
{
//[ApiController] 默認(rèn)自帶有400模型驗(yàn)證,且優(yōu)先級(jí)比較高,如果需要自定義模型驗(yàn)證,則需要先關(guān)閉默認(rèn)的模型驗(yàn)證
options.SuppressModelStateInvalidFilter = true;
});
ASP.NET Core MVC 使用 ModelStateInvalidFilter 操作篩選器來(lái)執(zhí)行自定義驗(yàn)證。
輸出結(jié)果
{
"code": 400,
"errors": [
{
"field": "PassWord",
"message": "密碼不能為空|密碼必須大于6位"
},
{
"field": "UserName",
"message": "名稱不能為空|名稱不能小于6位"
}
]
}
參考文檔
模型綁定:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/model-binding?view=aspnetcore-5.0
禁用綁定源推理:https://docs.microsoft.com/zh-cn/aspnet/core/web-api/?view=aspnetcore-5.0#disable-inference-rules
禁用驗(yàn)證:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/validation?view=aspnetcore-5.0#disable-validation
禁用自動(dòng)400響應(yīng):https://docs.microsoft.com/zh-cn/aspnet/core/web-api/?view=aspnetcore-5.0#disable-automatic-400-response
