iT邦幫忙

DAY 11
2

使用Asp.Net MVC打造Web Api系列 第 11

使用Asp.Net MVC打造Web Api (11) - 使用FluentValidation進行驗證

  • 分享至 

  • xImage
  •  

其實在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

  1. 在BL建立Validators,用來撰寫Model相關的驗證

  2. 使用Nuget加入FluentValidation函式庫

  3. 建立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;
            }
        }
    
  4. 建立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);
            }
        }    
    
  5. 建立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有了初步的認識,接下來我們就要來撰寫驗證的測試程式,來確保我們寫的驗證邏輯是沒有問題的!

  1. 在BL新增Validators.Test測試專案,並使用Nuget加入Specflow、Specrun和Rhino Mock

  2. 新增InsertProductModel驗證功能.feature

    	#language: zh-TW
    	功能: InsertProductModel驗證功能
    		提供給 BL層
    		使用者輸入InsertProductModel資料,驗證資料是否正確
    
  3. 撰寫測試案例

    	場景: 輸入資料正確,驗證成功
    		假設 使用者輸入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驗證
    		那麼 驗證失敗
    
  4. 撰寫測試程式

        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);
        }
    
  5. 執行測試,但測試失敗!?

  6. 我們可以發現,原來上架時間要早於開賣時間,和下架時間要晚於開賣結束時間的邏輯寫錯了,修正為

        RuleFor(i => i.StartListingAt).NotEmpty()
                                      .LessThanOrEqualTo(i => i.StartSellAt);
    
        RuleFor(i => i.FinishListingAt).NotEmpty()
                                       .GreaterThanOrEqualTo(i => i.FinishSellAt);
    
  7. 再次執行測試,發現測試成功

    ※本日小結
    今天的介紹中,我們初步的認識FluentValidation,實際撰寫了驗證的邏輯,並且還可以透過測試程式碼來檢查我們的驗證邏輯有沒有錯誤,發現錯誤後也可以快速的修正,並再跑一次測試來複測,就不需要再擔心是否有在修Bug時改壞了原本好的功能。


上一篇
使用Asp.Net MVC打造Web Api (10) - 透過AutoMapper處理資料轉換
下一篇
使用Asp.Net MVC打造Web Api (12) - 整合FluentValidation到Api中
系列文
使用Asp.Net MVC打造Web Api30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言