其實在Asp.Net MVC中原本就已經內建了非常好用的驗證機制,它可以透過在Model的屬性加上DataAnnotation的方式,來設定驗證Model的條件,更可以一次套用到Server端和Client端的驗證上,讓開發更加的方便迅速,此外也提供了豐富的擴充彈性,可以將自己常用的驗證方式透過撰寫CustomValidator來讓驗證的套用更加方便。
既然原生得那麼好用,那為什麼今天還要向大家介紹另外一套Validation Library呢?主要是希望可以更進一步的將驗證的邏輯和Model分離(ex.Model在BE專案,驗證在BL專案),讓驗證邏輯和Model之間的耦合性更低,又由於我們的系統已經整合了DI Framework,所以希望可以透過DI Framework來控制Model和Validator之間的關聯,甚至可以動態更換Validator等等,而FluentValidation也很好的符合了我們的需求,也提供給大家另外一個做驗證的選擇。
大家可以從Github ApiSample - Tag Day10開始練習今天的程式
※開始撰寫簡單的驗證
FluentValidation的驗證不同於Asp.Net MVC的方式,它會獨立建立一個Validator,將驗證邏輯寫在Validator裡面,而需要驗證時再使用Validator來驗證。接下來我們就要來撰寫InsertProductModel的驗證程式,InsertProductModel在輸入時必須要符合的條件是
* Name - 不得為空,長度需在1~100之間
* Price - 必須大於0,以及大於Cost
* Cost - 必須大於0,以及小於Price
* Introduction - 長度必須小於1000
* StartSellAt - 不得為空,必須早於FinishSellAt
* FinishSellAt - 不得為空,必須晚於FinishSellAt
* StartListingAt - 不得為空,必須早於StartSellAt
* FinishListingAt - 不得為空,必須晚於FinishSellAt
* CategoryId - 不得為空,資料庫必須存在該Category
在BL建立Validators,用來撰寫Model相關的驗證
使用Nuget加入FluentValidation函式庫
建立Category Repository,用來查詢Category是否存在
public interface ICategoryRepository
{
bool IsCategoryExist(int categoryId);
}
public class CategoryRepository : ICategoryRepository
{
public ShopContext ShopContext { get; set; }
public CategoryRepository(ShopContext context)
{
this.ShopContext = context;
}
public bool IsCategoryExist(int categoryId)
{
var isExist = this.ShopContext.Categories.Valids()
.Any(i => i.Id == categoryId);
return isExist;
}
}
建立CategoryService
public interface ICategoryService
{
bool IsCategoryExist(int categoryId);
}
public class CategoryService : ICategoryService
{
public ICategoryRepository CategoryRepository { get; set; }
public CategoryService(ICategoryRepository categoryRepository)
{
this.CategoryRepository = categoryRepository;
}
public bool IsCategoryExist(int categoryId)
{
return this.CategoryRepository.IsCategoryExist(categoryId);
}
}
建立InsertProductModelValidator,在建構式中撰寫驗證邏輯
public class InsertProductModelValidator : AbstractValidator<InsertProductModel>
{
public ICategoryService CategoryService { get; set; }
public InsertProductModelValidator(ICategoryService categoryService)
{
this.CategoryService = categoryService;
RuleFor(i => i.Name).NotEmpty()
.Length(1, 100);
RuleFor(i => i.Price).GreaterThan(0)
.GreaterThan(i => i.Cost);
RuleFor(i => i.Cost).GreaterThan(0)
.LessThan(i => i.Price);
RuleFor(i => i.Introduction).Length(0, 1000);
RuleFor(i => i.StartSellAt).NotEmpty()
.LessThan(i => i.FinishSellAt);
RuleFor(i => i.FinishSellAt).NotEmpty()
.GreaterThan(i => i.StartSellAt);
RuleFor(i => i.StartListingAt).NotEmpty()
.GreaterThanOrEqualTo(i => i.StartSellAt);
RuleFor(i => i.FinishListingAt).NotEmpty()
.LessThanOrEqualTo(i => i.FinishSellAt);
RuleFor(i => i.CategoryId).NotEmpty()
.Must(i => this.CategoryService.IsCategoryExist(i))
.WithMessage("Category must exist!");
}
}
這樣我們就完成驗證邏輯的撰寫了,如果想要使用Validator也很簡單,可以參考下面的程式碼
InsertProductModel product = new InsertProductModel();
InsertProductModelValidator validator = new InsertProductModelValidator();
ValidationResult results = validator.Validate(product);
if(! results.IsValid)
{
foreach(var failure in results.Errors)
{
Console.WriteLine("Property " + failure.PropertyName + " failed alidation. Error was: " + failure.ErrorMessage);
}
}
※FluentValidation提供的驗證方法
從上面的範例,我們可以看到FluentValidation提供的驗證撰寫方式也是非常直覺的,它透過在Validator中以條列式的方式,一個一個屬性的撰寫測試邏輯,不但寫起來簡單,要核對檢查條件是否完整時也相當輕鬆。
FluentValidation內建基本提供的驗證如下
除此之外,我們同樣的也可以擴充自己的Validator
public class NameMustStartWithAValidator : PropertyValidator {
public NameMustStartWithAValidator()
: base("{PropertyName} must start with A") {
}
protected override bool IsValid(PropertyValidatorContext context) {
var name = context.PropertyValue.toString();
if(name.StartWith("A")) {
return true;
}
return false;
}
}
使用上也很簡單
RuleFor(i => i.Name).SetValidator(new NameMustStartWithAValidator());
而如果想要自己定義錯誤訊息的話,也可以在設定完驗證規則後,使用WithMessage指定該驗證條件的錯誤訊息!
RuleFor(i => i.CategoryId).NotEmpty()
.Must(i => this.CategoryService.IsCategoryExist(i))
.WithMessage("Category must exist!");
延伸閱讀:
* FluentValidation
※測試驗證邏輯
相信經過上面的Sample Code以及簡單介紹之後,大家應該對於如何使用FluentValidation有了初步的認識,接下來我們就要來撰寫驗證的測試程式,來確保我們寫的驗證邏輯是沒有問題的!
在BL新增Validators.Test測試專案,並使用Nuget加入Specflow、Specrun和Rhino Mock
新增InsertProductModel驗證功能.feature
#language: zh-TW
功能: InsertProductModel驗證功能
提供給 BL層
使用者輸入InsertProductModel資料,驗證資料是否正確
撰寫測試案例
場景: 輸入資料正確,驗證成功
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 200 | 100 | | 2013-10-01 | 2014-10-01 | 2013-10-01 | 2014-10-01 | 1 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證成功
場景: 輸入姓名為空,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| | 200 | 100 | | 2013-10-01 | 2014-10-01 | 2013-10-01 | 2014-10-01 | 1 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入姓名長度超過100,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| | 200 | 100 | | 2013-10-01 | 2014-10-01 | 2013-10-01 | 2014-10-01 | 1 |
假設 資料庫存在分類序號1
並且 姓名長度超過100
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入價格為0,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 0 | 100 | | 2013-10-01 | 2014-10-01 | 2013-10-01 | 2014-10-01 | 1 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入價格小於成本,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 99 | 100 | | 2013-10-01 | 2014-10-01 | 2013-10-01 | 2014-10-01 | 1 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入成本為0,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 200 | 0 | | 2013-10-01 | 2014-10-01 | 2013-10-01 | 2014-10-01 | 1 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入成本大於售價,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 200 | 300 | | 2013-10-01 | 2014-10-01 | 2013-10-01 | 2014-10-01 | 1 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入介紹超過1000,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 200 | 100 | | 2013-10-01 | 2014-10-01 | 2013-10-01 | 2014-10-01 | 1 |
假設 資料庫存在分類序號1
並且 介紹長度超過1000
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入開賣時間為空,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 200 | 100 | | 2013-10-01 | 2014-10-01 | | 2014-10-01 | 1 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入開賣時間晚於開賣結束時間,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 200 | 100 | | 2013-10-01 | 2014-10-01 | 2015-10-01 | 2014-10-01 | 1 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入開賣結束時間為空,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 200 | 100 | | 2013-10-01 | 2014-10-01 | 2013-10-01 | | 1 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入開賣結束時間早於開賣時間,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 200 | 100 | | 2013-10-01 | 2014-10-01 | 2013-10-01 | 2012-10-01 | 1 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入上架時間為空,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 200 | 100 | | | 2014-10-01 | 2013-10-01 | 2014-10-01 | 1 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入上架時間晚於開賣時間,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 200 | 100 | | 2015-10-01 | 2014-10-01 | 2013-10-01 | 2014-10-01 | 1 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入下架時間為空,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 200 | 100 | | 2013-10-01 | | 2013-10-01 | 2014-10-01 | 1 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入下架時間早於開賣結束時間,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 200 | 100 | | 2013-10-01 | 2012-10-01 | 2013-10-01 | 2014-10-01 | 1 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入分類資料為空,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 200 | 100 | | 2013-10-01 | 2014-10-01 | 2013-10-01 | 2014-10-01 | 0 |
假設 資料庫存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
場景: 輸入分類不存在資料庫中,驗證失敗
假設 使用者輸入InsertProductModel資料
| Name | Price | Cost | Introduction | StartListingAt | FinishListingAt | StartSellAt | FinishSellAt | CategoryId |
| Test | 200 | 100 | | 2013-10-01 | 2014-10-01 | 2013-10-01 | 2014-10-01 | 1 |
假設 資料庫不存在分類序號1
當 執行InsertProductModel驗證
那麼 驗證失敗
撰寫測試程式
private InsertProductModel model;
private ICategoryService service;
private ValidationResult result;
[Given(@"使用者輸入InsertProductModel資料")]
public void 假設使用者輸入InsertProductModel資料(Table table)
{
this.model = table.CreateInstance<InsertProductModel>();
}
[Given(@"資料庫存在分類序號(.*)")]
public void 假設資料庫存在分類序號(int categoryId)
{
this.service = MockRepository.GenerateStub<ICategoryService>();
this.service.Stub(i => i.IsCategoryExist(Arg<int>.Is.Equal(categoryId)))
.Return(true);
}
[Given(@"資料庫不存在分類序號(.*)")]
public void 假設資料庫不存在分類序號(int categoryId)
{
this.service = MockRepository.GenerateStub<ICategoryService>();
this.service.Stub(i => i.IsCategoryExist(Arg<int>.Is.Equal(categoryId)))
.Return(false);
}
[Given(@"姓名長度超過(.*)")]
public void 假設姓名長度超過(int length)
{
for (int i = 0; i <= length; i++)
{
this.model.Name += "A";
}
}
[Given(@"介紹長度超過(.*)")]
public void 假設介紹長度超過(int length)
{
for (int i = 0; i <= length; i++)
{
this.model.Introduction+= "A";
}
}
[When(@"執行InsertProductModel驗證")]
public void 當執行InsertProductModel驗證()
{
InsertProductModelValidator validator = new InsertProductModelValidator(this.service);
this.result = validator.Validate(this.model);
}
[Then(@"驗證成功")]
public void 那麼驗證成功()
{
Assert.IsTrue(this.result.IsValid);
}
[Then(@"驗證失敗")]
public void 那麼驗證失敗()
{
Assert.IsFalse(this.result.IsValid);
}
執行測試,但測試失敗!?
我們可以發現,原來上架時間要早於開賣時間,和下架時間要晚於開賣結束時間的邏輯寫錯了,修正為
RuleFor(i => i.StartListingAt).NotEmpty()
.LessThanOrEqualTo(i => i.StartSellAt);
RuleFor(i => i.FinishListingAt).NotEmpty()
.GreaterThanOrEqualTo(i => i.FinishSellAt);
再次執行測試,發現測試成功
※本日小結
今天的介紹中,我們初步的認識FluentValidation,實際撰寫了驗證的邏輯,並且還可以透過測試程式碼來檢查我們的驗證邏輯有沒有錯誤,發現錯誤後也可以快速的修正,並再跑一次測試來複測,就不需要再擔心是否有在修Bug時改壞了原本好的功能。