在程式中,需要進行資料驗證的場景經常存在,且資料驗證是有必要的。前端進行資料驗證,主要是為了減少伺服器請求壓力,和提高使用者體驗;後端進行資料驗證,主要是為了保證資料的正確性,保證系統的健壯性。
本文描述的資料驗證方案,是基於官方的模型驗證(Model validation),也是筆者近期面試過程中才得知的方式【之前個人混淆了:模型驗證(Model validation)和 EF 模型設定的資料註釋(Data annotation)方式】。
注:MVC 和 API 的模型驗證有些許差異,本文主要描述的是 API 下的模型驗證。
比較傳統的驗證方式如下:
public string TraditionValidation(TestModel model)
{
if (string.IsNullOrEmpty(model.Name))
{
return "名字不能為空!";
}
if (model.Name.Length > 10)
{
return "名字長度不能超過10!";
}
return "驗證通過!";
}
在函數中,對模型的各個屬性分別做驗證。
雖然函數能與模型配合重複使用,但是確實不夠優雅。
官方提供了模型驗證(Model validation)的方式,下面將會基於這種方式,提出相應的解決方案。
先大概介紹一下模型驗證(Model validation)的使用,隨後提出兩種自定義方案。
最後會大概解讀一下 AspNetCore 這一塊相關的原始碼。
官方提供的模型驗證(Model validation)的方式,是通過在模型屬性上新增驗證特性(Validation attributes),設定驗證規則以及相應的錯誤資訊(ErrorMessage)。當驗證不通過時,將會返回驗證不通過的錯誤資訊。
其中,除了內建的驗證特性,使用者也可以自定義驗證特性(本文不展開),具體請自行檢視自定義特性一節。
在 MVC 中,需要通過如下程式碼來呼叫(在 action 中):
if (!ModelState.IsValid)
{
return View(movie);
}
在 API 中,只要控制器擁有 [ApiController] 特性,如果模型驗證不通過,將自動返回包含錯誤資訊的 HTTP400 相應,詳細請參閱自動 HTTP 400 響應。
如下程式碼中,[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": [
"郵箱格式錯誤!"
]
}
}
官方列出的一些內建特性如:
[ValidateNever]:指示屬性或引數應從驗證中排除。
[CreditCard]:驗證屬性是否具有信用卡格式。
[Compare]:驗證模型中的兩個屬性是否匹配。
[EmailAddress]:驗證屬性是否具有電子郵件格式。
[Phone]:驗證屬性是否具有電話號碼格式。
[Range]:驗證屬性值是否在指定的範圍內。
[RegularExpression]:驗證屬性值是否與指定的正規表示式匹配。
[Required]:驗證欄位是否不為 null。
[StringLength]:驗證字串屬性值是否不超過指定長度限制。
[URL]:驗證屬性是否具有 URL 格式。
[Remote]:通過在伺服器上呼叫操作方法來驗證使用者端上的輸入。
可以在名稱空間中找到 System.ComponentModel.DataAnnotations 驗證屬性的完整列表。
由於官方模型驗證返回的格式與我們程式實際需要的格式有差異,所以這一部分主要是替換模型驗證的返回格式,使用的實際上還是模型驗證的能力。
準備一個統一返回格式:
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個字元!",
"郵箱格式錯誤!"
]
}
替換 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);
};
});
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>();
});
輸入不合法的資料,格式如下:
{
"name": "string string",
"email": "111"
}
輸出資訊如下:
{
"code": 400,
"msg": "資料驗證不通過!",
"data": [
"名字長度不能超過10個字元!",
"郵箱格式錯誤!"
]
}
兩種方案實際上都是差不多的(實際上都是基於過濾器 Filter 的),可以根據個人需要選擇。
其中 AspNetCore 預設實現的過濾器為 ModelStateInvalidFilter
,其 Order = -2000,可以根據程式實際情況,對程式內的過濾器順序進行編排。
AspNetCore 模型驗證這一塊相關的原始碼,主要是通過註冊一個預設工廠 InvalidModelStateResponseFactory
(由 ApiBehaviorOptionsSetup
對 ApiBehaviorOptions
進行設定,實際上是一個 Func
),以及使用一個過濾器(為 ModelStateInvalidFilter
,由 ModelStateInvalidFilterFactory
生成),來控制模型驗證以及返回結果(返回一個 BadRequestObjectResult
或 ObjectResult
)。
其中,最主要的是 ApiBehaviorOptions
的 SuppressModelStateInvalidFilter
和 InvalidModelStateResponseFactory
屬性。這兩個屬性,前者控制預設過濾器是否啟用,後者生成模型驗證的結果。
新建的 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
。
主要程式碼如下:
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
。
上面介紹完了 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
工廠的驗證,獲取返回結果。
模型驗證最主要的原始碼就如上所述。
(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
AspNetCore原始碼