前情提要 現在開發網頁幾乎都切換成前後端分離的開發方式,前端採用 Angular
, Vue
, React
三大框架之一,後端則只要撰寫API服務即可,一來職責分離,前端適合處理畫面的互動效果,後端搭配資料庫處理商業邏輯的運算,使用json
格式傳送結果至前端,多完美的協作阿。
通常簡單驗證邏輯會放在前端做處理,畢竟不用花費傳送成本,例如必填、長度等簡單邏輯驗證,直接在前端實作即可,但畢竟後端需要將資料儲存至資料庫,若沒有再做進一步驗證則資料儲存失敗的風險,以及日後資料運算使用時的困難。又或者系統本身需要透過http
與其他系統介接,其他系統直接打我方API,這種情境就不需多說,需要在後端做驗證,連必填、長度等簡單邏輯驗證也必須做在後端。
內容 基於各種理由,我們必須在後端實作資料驗證這個部份,筆者心中想法是這種邏輯驗證必須做在進到action
之前,需於Model Binding
時期,若驗證不過則由統一格式回傳,既不佔用到運算資源,類似像Dotnet Core
中的Middleware
設計,責任界線劃分清楚,日後維護或擴充起來會是好的體驗。
跟著筆者一步一步做吧,大概會分為這幾步驟
安裝相關Nuget套件
實作Validator
於StartUp載入使用
設定驗證不過則統一格式回復
安裝相關Nuget套件-FluentValidation 依照專案的角色不同,安裝不同套件,若設定Validator的專案則是安裝 FluentValidation
,若最終使用端API專案則需要使用到FluentValidation.AspNetCore
,跟著筆者安裝吧
1 2 dotnet add package FluentValidation dotnet add package FluentValidation.AspNetCore
實作Validator 主要是繼承AbstractValidator
,於Constructor
宣告驗證的Rule,直接來看Code吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class MyApiRegisterRequestModel { public string Idn { get ; set ; } } public class MyApiRequestModelValidator : AbstractValidator <MyApiRegisterRequestModel >{ public SKBApiRequestModelValidator () { RuleFor(x => x.Idn).NotEmpty() .WithMessage("身分證或居留證卡號不得為空" ); } }
主要有哪些驗證邏輯,參考官網會有諸多範例可參考,筆者不在這邊多加贅述,直接參考官網說明即可。
Built-in Validators - FluentValidation documentation
Startup載入宣告 前提需要安裝Nuget套件: FluentValidation.AspNetCore
,筆者這邊的作法是會把View Model會獨立於一個專案中宣告,然後相對應的Validators
也宣告於此專案,所以筆者利用套件提供的FromAssembly
的方式來做註冊,直接看Code吧。
1 2 3 4 5 services.AddControllers() .AddFluentValidation(opt => { opt.RegisterValidatorsFromAssembly(AppDomain.CurrentDomain.Load("ModelsProjName" )); });
設定驗證不過則統一格式回復 配合上一個步驟,只要宣告AddFluentValidation
則FluentValidation會在Model Binding時期就會做驗證並將ModelState改變,會去改變ModelState.IsValid的值,所以我們只要Configure ApiBehaviorOptions
中的InvalidModelStateResponseFactory
做設定,直接回傳結果。
筆者這邊有定義統一回傳格式,依照各位的需求,可以再調整其格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 [XmlRoot(ElementName = "result" ) ] public class Result { [XmlElement(ElementName = "status" ) ] public ResponseStatus Status { get ; set ; } = ResponseStatus.Failure; [XmlElement(ElementName = "message" ) ] public string Message { get ; set ; } = string .Empty; public override string ToString () { return JsonConvert.SerializeObject(this , new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }); } } public class Result <T > : Result { public T ResultObj { get ; set ; } } public static class ResultExtension { public static Result Success (this Result helper ) { return helper.Success("" ); } public static Result Success (this Result helper, string message ) { helper.Status = ResponseStatus.Success; helper.Message = message; return helper; } public static Result Fail (this Result helper, string resultMsg ) { helper.Status = ResponseStatus.Failure; helper.Message = resultMsg; return helper; } public static Result DbNotFound (this Result helper ) { helper.Status = ResponseStatus.NotFound; helper.Message = "查無資料" ; return helper; } public static Result DbException (this Result helper ) { helper.Status = ResponseStatus.DatabaseError; helper.Message = "資料庫異常" ; return helper; } }
就在Startup設定統一回傳固定的Result,將錯誤訊息打包到一個ResultObj
中,型別為List<string>
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 services.Configure<ApiBehaviorOptions>(options => { options.InvalidModelStateResponseFactory = (context) => { var errors = context.ModelState .Values .SelectMany(x => x.Errors .Select(p => p.ErrorMessage)) .ToList(); var result = new Result<List<string >>() { ResultObj = errors }; result.Fail(errors.FirstOrDefault()); return new OkObjectResult(result); }; });
跟著筆者來看一下效果吧,就使用Postman來試打一下API,故意傳空值,效果如下:
複雜物件類別驗證設定 實際專案往往會遇到不是單純的單層結構,一次接主要資料含明細資料,跟著筆者一起來研究一下遇到這種比較複雜類型要怎麼設定吧,筆者也是跟著官網教學做,因此就follow官網提供的範例了,這邊主要分為明細為單筆或明細為多筆,依照這兩種狀況不同而設定方式有所不同。
單筆明細 先來看看主要及明細的j物件類別、屬性:
1 2 3 4 5 6 7 8 9 10 11 12 public class Customer { public string Name { get ; set ; } public Address Address { get ; set ; } } public class Address { public string Line1 { get ; set ; } public string Line2 { get ; set ; } public string Town { get ; set ; } public string County { get ; set ; } public string Postcode { get ; set ; } }
針對Address
新增一個Validator
:
1 2 3 4 5 6 public class AddressValidator : AbstractValidator <Address > { public AddressValidator () { RuleFor(address => address.Postcode).NotNull(); } }
接下來於Customer
新增一個Validator
,並宣告針對Address
屬性的驗證類別:
1 2 3 4 5 6 public class CustomerValidator : AbstractValidator <Customer > { public CustomerValidator () { RuleFor(customer => customer.Name).NotNull(); RuleFor(customer => customer.Address).SetValidator(new AddressValidator()); } }
多筆明細 筆者這邊就拿官網上的Customer與Order的例子來說明,一個客戶雍有多筆訂單,這個時候需要使用到RuleForEach
,一般單個屬性設定為RuleFor
,先宣告對應的物件類別:
1 2 3 4 5 6 7 public class Customer { public List<Order> Orders { get ; set ; } = new List<Order>(); } public class Order { public double Total { get ; set ; } }
接下來要針對Order
先宣告其Validator
:
1 2 3 4 5 public class OrderValidator : AbstractValidator <Order > { public OrderValidator () { RuleFor(x => x.Total).GreaterThan(0 ); } }
最後針對Customer
設定其Validator
時使用RuleForEach
設定其擁有的Order
屬性使用OrderValidator
:
1 2 3 4 5 public class CustomerValidator : AbstractValidator <Customer > { public CustomerValidator () { RuleForEach(x => x.Orders).SetValidator(new OrderValidator()); } }
Custom Validator-進階邏輯驗證 筆者會用到的另一個情境是,有些識別序號欄位就錯了,其實根本不需要進到邏輯層去處理相關邏輯運算,這種牽扯自身邏輯相關的必須要實作Custom Validator
來完成。如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class ChargeListValidator : AbstractValidator <List <ChargeRequestModel >>{ private readonly MyDBContext _db; public ChargeListValidator (MyDBContext db ) { _db = db; RuleFor(x => x) .Must(ValidateCaseNo).WithMessage("契約編號有誤,請重新輸入" ) .DependentRules(() => { RuleFor(x => x) .Must(ValidateTicketNo).WithMessage("票券編號有誤,請重新輸入" ); }); RuleForEach(x => x).SetValidator(new ChargeValidator()); } private bool ValidateCaseNo (List<ChargeRequestModel> chargeRequestModelList , List<ChargeRequestModel> targetModels ) { var distinctCaseNoList = targetModels.Select(x => x.CaseNo).Distinct().ToList(); var existCount = _db.AuthMainDts.Count(x => distinctCaseNoList.Contains(x.CaseNo)); return distinctCaseNoList.Count == existCount; } private bool ValidateTicketNo (List<ChargeRequestModel> chargeRequestModelList , List<ChargeRequestModel> targetModels ) { var existCount = _db.Tkdetails.Count(x => targetModels.Select(t => t.TicketNo).ToList().Contains(x.Tkno)); return targetModels.Count == existCount; } }
以上,筆者需要解說一下,由註解可以看到,我這邊舉的例子使用EF Core
操作資料庫,把相對應的檢查邏輯寫在Custom Validator
。
結論 筆者在ASP.NET MVC 5
時期就開始使用Fluent Validation
,覺得很靈活且方便的設定及使用,內建Validation就已足夠應付一般驗證需求,基本上也可以完全走自定義的Validator,換到ASP.NET CORE
,將使用及設定方式更為彈性,尤其搭配ApiBehaviorOption
,可以完美地統一處理Response,是一件值得花時間研究一下的套件。
參考