2016年10月16日 星期日

【Unit Test】Fake DLL發生does not exist in the namespace 'System.Diagnostics.Tracing'


上一篇【Unit Test】Fake System.DateTime 寫到如何Fake DLL,在本機建置與進行測試都沒問題,唯獨放到CICD機器去做自動化部屬時,一直鬼擋牆的跳建置方案錯誤,錯誤內容如下

The type or namespace name 'EventSourceCreatedEventArgs' does not exist in the namespace 'System.Diagnostics.Tracing' (are you missing an assembly reference?) 

上網查了一下找到以下解法


  • 打開mscorlib.fakes,改成以下內容
<Fakes xmlns="http://schemas.microsoft.com/fakes/2011/">
  <Assembly Name="mscorlib" Version="4.0.0.0"/>
  <StubGeneration>
    <Remove FullName="System.Diagnostics.Tracing"/>
    <Remove FullName="System.Text.Encoding"/>
    <Remove FullName="System.Security.Cryptography" />
  </StubGeneration>
</Fakes>


自動化佈署機器這樣去建置佈署就過關了!!!!

【Unit Test】Fake System.DateTime


在寫程式的時後,常常會用到DateTime.Now來判斷目前時間,依照時間邏輯去撈取不同的資料,但碰到單元測試要驗證的時候就是個大麻煩,因為DateTime.Now每次執行的時候時間都會改變,所以以前的做法都是這樣


public static class SystemTime
{
 //Internal For UnitTest
 internal static Func<DateTime> SetCurrentTime = () => DateTime.Now;

 public static DateTime Now
 {
  get
  {
   return SetCurrentTime();
  }
 }
}

然後在在程式碼裡面使用的時候不直接使用DateTime.Now,改用SystemTime.Now
void Main()
{
 if (SystemTime.Now == new DateTime(2016,10,17,12,0,0))
 {
  "時間到了".Dump();
 } 
 else
 {
  "時間不對".Dump();
 }
}

之後單元測試驗證時,動態去改寫SetCurrentTime,就能確保拿到自己要的時間
SystemTime.SetCurrentTime = () => new DateTime(2016,10,17,12,0,0);



雖然這樣可以解決問題沒錯,但實在太麻煩,為了單元測試要多寫一堆Code之外,System底下很多東西有用到時,都要因為可以單元測試的關係而擴充出來。

還好之後有找到方法可以針對DLL做Fake,讓我們可以繼續安心使用DateTime.Now之餘,單元測試也能指定時間,接下來就來實作一下這個步驟


  • 首先在單元測試的專案對參考的組件System按下右鍵,新增Fake物件




  • 接著就會跑出Fake資料夾





  • 接著只要在單元測試的地方寫如此寫,就能設定DateTime.Now應該回傳的時間了
        [TestMethod]
        public void 取得現在時間()
        {
            using (ShimsContext.Create())
            {
                System.Fakes.ShimDateTime.NowGet= () =>
                {
                    return new DateTime(2016, 9, 25);
                };


                 //arrange
                var expected = new DateTime(2016, 9, 25);

                //act
                var actual = DateTime.Now;


                //assert
                Assert.AreEqual(expected, actual);
            }
        }


補充2017/01/24

單元測試Fake的功能,以Visual Studio 2015來說只有Enterprise版本才有支援,所以使用的時候請特別小心,像今天公司因為授權費的關係,要求調降成Professional,之前有用到Fake的地方就都測不過了,還請特別注意

各版本比較 : Compare Visual Studio 2015 Offerings

參考文章

2016年10月13日 星期四

【Swagger】客製化可以輸入Header的欄位


上一篇【Swagger】活著的API規格書 提到如何使用Swagger來產生規格與測試API,但遇到一個問題是,很多API會把驗證的Key放到Header傳遞,但Swagger產出來的頁面並沒有設定Header的地方,這時候就要來小調整一下已符合需求。

首先我們先把原本的API改成要吃Header的Key值才算驗證通過

    public class TestSwaggerController : ApiController
    {
        /// <summary>
        /// 測試Swagger的API
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <returns></returns>
        [HttpGet]
        [Route("api/testSwagger")]
        public HttpResponseMessage Get([FromUri]testSwaggerGetParameter parameter)
        {

            //沒有帶Appkey在Header
            if (!Request.Headers.Contains("X-Key"))
            {
                return Request.CreateResponse(HttpStatusCode.NotAcceptable, "必須輸入AppKey");
                
            }

            var AppKey =  Request.Headers.GetValues("X-Key").FirstOrDefault();
            if (AppKey != "MyKey")
            {
                return Request.CreateResponse(HttpStatusCode.NotAcceptable, "AppKey不正確!!!");
            }


            if (parameter != null &&
                parameter.ID == "toyo" && 
                parameter.PassWord == "123456")
            {
                return Request.CreateResponse(HttpStatusCode.OK, "帳號密碼正確");
            }

            return Request.CreateResponse(HttpStatusCode.OK, "帳號密碼錯誤摟!!!!!");
        }
    }


這時候測試一下原本的API,會發現因為吃不到Header裡面的X-Key,導致輸出驗證失敗的錯誤



Header中也再次確認沒有帶X-key在其中





開始客製化吧


  • 首先先建立一個JS檔,讓Swagger的View能引用這支JS,然後透過這支JS與Jquery去改View,我將這支JS就命名為AppKey.js




  • 接著請在這支JS檔案寫下以下JS


$(function () {
    $('#input_apiKey').hide();

    //加上Header區塊
    var HeaderBar = $('<div id="headerbar">' +
        '<h2 class="heading">Header</h2>' +
        '<table style="background-color:#E8E8D0"><thead><tr>' +
            '<th style="width: 100px; max-width: 100px" data-sw-translate="">Parameter</th>' +
            '<th style="width: 310px; max-width: 310px" data-sw-translate="">Value</th>' +
            '<th style="width: 200px; max-width: 200px" data-sw-translate="">Description</th>' +
        '</tr></thead>' +
        '<tbody class="operation-params">' +
        '<tr>' +
            '<td class="code required"><label for="custom_appkey">appkey</label></td>' +
            '<td><input class="parameter required" minlength="1" name="custom_appkey" placeholder="(required)" id="custom_appkey" type="text" value=""></td>' +
            '<td class="markdown"><p>AppKey</p></td>' +
        '</tr>' +
        '</tbody></table>' +
    '</div>');
    $('#resources_container').before(HeaderBar);

    //把值加到Header
    $('#custom_appkey').on('change', function () {
        var key = this.value;
        if (key && key.trim() !== '') {
            swaggerUi.api.clientAuthorizations.add("key", new SwaggerClient.ApiKeyAuthorization("X-Key", key, "header"));
        }
    });

});


主要內容就是抓到特定區塊,然後組Html用Jquery的方式放上去,然後透過Swagger開放的API塞到Header裡面送出




  • 接著請將這支JS修改屬性 > 建置動作 > 內嵌資源






  • 打開SwaggerConfig找到188~189行的地方把註解拿掉改成如下


c.InjectJavaScript(thisAssembly, "WebApplication1.CustomSwagger.AppKey.js");




  • 接著再次執行專案

Header欄位出現了

試試看欄位是否有效,因為剛剛程式是改成要帶MyKey,所以來測試看看

可以抓到X-Key了



完成!!!! 因為是透過Jquery的方式去改變欄位,所以只要透過上述的方式去載入JS,要怎麼改View應該都不是問題了,以上。


參考文章

Customize Authentication Header in SwaggerUI using Swashbuckle

【Swagger】活著的API規格書


所謂的工程師就是,【接手別人專案時,總是問怎麼沒有規格書? 自己開發專案時卻又不喜歡寫規格書】的一群人,本人也是一個極度不喜歡寫規格書的人,簡單說就是懶到極致,懶到深處無怨尤。而且常常改版時來匆匆忙忙,寫程式的時間都不夠了,誰還管你規格書有沒有更新,就這樣恍神個兩三次忘記回去更新規格,這本規格書就光榮列入公司十大(搞不好百大?)不可思議天書,可謂極度麻煩費時討厭.....

還好同事介紹了Swagger的用法,讓你邊開發程式時,規格就產生書產生出來了,而且還是本可以使用的規格書,讓你從今以後再也不用擔心規格書與實際規格脫鉤的問題,以下就筆記一下如何使用

Swagger-當開發完API時,規格書也就完成了!!
輸入的參數與說明也寫得清清楚楚
按下Try it Out後,可以馬上測試API跟觀看結果!!


使用步驟

  • 首先我先開一個WebAPI專案,然後寫一支簡單的API讓他可以運作
開一個WebAPI專案

新增一個新的TestSwaggerController




  • 開始撰寫幾個簡單的API跟輸入輸出參數


    public class testSwaggerGetParameter
    {
        /// <summary>
        /// 帳號
        /// </summary>
        /// <value>
        /// The identifier.
        /// </value>
        public string ID { get; set; }

        /// <summary>
        /// 密碼
        /// </summary>
        /// <value>
        /// The passWord.
        /// </value>
        public string PassWord { get; set; }
    }

   
    public class TestSwaggerController : ApiController
    {
        /// <summary>
        /// 測試Swagger的API
        /// </summary>
        /// <param name="parameter">The parameter.</param>
        /// <returns></returns>
        [HttpGet]
        [Route("api/testSwagger")]
        public HttpResponseMessage Get([FromUri]testSwaggerGetParameter parameter)
        {
            if (parameter != null &&
                parameter.ID == "toyo" && 
                parameter.PassWord == "123456")
            {
                return Request.CreateResponse(HttpStatusCode.OK, "帳號密碼正確");
            }

            return Request.CreateResponse(HttpStatusCode.OK, "帳號密碼錯誤摟!!!!!");
        }
    }



  • 接著先來測試API的運作是否正常

輸入正確的測試

輸入錯誤的測試



  • API都準備就緒後,來安裝Swagger吧

首先打開Nuget搜尋Swashbuckle並安裝



接著打開專案屬性,設定輸出XML說明格式



打開SwaggerConfig做些設定


在第一百行的地方把註解拿掉


在最下面補上以下程式碼



  • 重新執行WebAPI,並且在網址列輸入......./Swagger

看到API被輸出成規格了,參數的註解是跟著Summary的



來測試看看!!!





最後,Swagger雖然已經非常好用了,也能符合大部分的情境運用,但還是有些美中不足的地方,例如如果今天API部分資訊要帶在Header裡面傳給API,在預設產生的地方是沒有提供可以輸入的地方。

但慶幸的是Swagger可以很彈性的去改寫這張最後會產生的View,可以自己擴充需要的欄位,下一篇在來介紹如何客製化Swagger頁面!!!




參考文章



2016年10月12日 星期三

用 FluentValidation 驗證參數


FluentValidation是個很不錯的套件,且擴充性也高,解決了一些以前常常要寫很多遍的驗證邏輯。


以前常常驗證參數時都會有很多的If..Else,搞得程式碼很長很醜之外,閱讀性不佳。自從公司同事推薦了這個套件後,用了兩三個專案發現程式碼變得簡潔易懂之外,寫了一些擴充方法也可以重複使用,不像以前常常重複造輪子


void Main()
{
 var Parameter = new APIInputParameter 
 {
  ID = "123",
  Name = "Toyo"
 };
 
 Guid _ID;
 //驗證邏輯
 if (string.IsNullOrWhiteSpace(Parameter.ID) ||
           !Guid.TryParse(Parameter.ID,out _ID) ||
     string.IsNullOrWhiteSpace(Parameter.Name) )
 {
  "參數錯誤".Dump();
 }
 else
 {
  "參數正確".Dump();
 }
  
  
}

public class APIInputParameter 
{
 //此參數應該為Guid,但為了能Log下來所以接的時候要先接成String
 //否則輸入端不是傳入GUID就記錄不到了
 public string ID { get; set; }
 public string Name {get;set;}
}


上面的範例是以前的寫法,常常欄位多,各個欄位又有不同的要求時,總是把驗證的邏輯寫得又臭又長。

加上公司要求所有輸入輸出的欄位都要被Log下來,所以基本上參數都要是String型別,否則如果ID寫成GUID,因為傳入的時候不是GUID會接不到,自然無法被Log到,但也因此衍生了驗證的複雜度提高的問題。

想想如果各個參數錯誤要回傳的訊息會不同時,又該寫的多複雜才做得到呢......



那讓來看看如何透FluentValidation 驗證參數,

  1. 首先先把驗證邏輯寫成一個Class
    public class APIInputParameterValidator : AbstractValidator<APIInputParameter> 
    {
     public APIInputParameterValidator()
     {
      //ID - 必填,應為GUID
      this.RuleFor(x => x.ID)
         .NotEmpty()
         .WithErrorCode("X400")
         .WithMessage("ID不得為空字串")
         .NotNull()
         .WithErrorCode("X400")
         .WithMessage("ID不得為Null");
    
      this.RuleFor(x => x.Name)
       .NotEmpty()
       .WithErrorCode("X401")
       .WithMessage("Name不得為空字串")
       .NotNull()
       .WithErrorCode("X401")
       .WithMessage("Name不得為Null");
     }
    }
    

  2. 自己寫一個HasError的Extension
    /// <summary>
    /// FluentValidation 自訂驗證擴充方法.
    /// </summary>
    public static class FluentValidationExtensions
    {
     /// <summary>
     /// 驗證結果是否有 Error.
     /// </summary>
     /// <param name="validationFailure"></param>
     /// <returns></returns>
     public static bool HasError(this ValidationFailure validationFailure)
     {
      return validationFailure != null &&
        !string.IsNullOrWhiteSpace(validationFailure.ErrorMessage);
     }
    }
    

  3. 驗證的地方改成如下
     
     var Parameter = new APIInputParameter 
     {
      ID = "123",
      Name = "Toyo"
     };
     
     // 檢查輸入參數
     var validator = new APIInputParameterValidator();
    
     var error = validator.Validate(Parameter).Errors.FirstOrDefault();
     if (error.HasError())
     {
       string.Format("{0}-{1}",error.ErrorCode,error.ErrorMessage).Dump();
     }
     else
     {
       "驗證成功".Dump();
     }
    



這樣只要有參數帶入錯誤,他就會依照你要求的帶回ErrorCode跟ErrorMessage,那可能各位會發現,阿驗證是否為GUID的地方怎麼不見了?? 因為套件並沒有提供,所以這邊要自己擴充


  1. 先寫一個驗證String是否為Guid的方法
    /// <summary>
        /// 驗證是否為GUID
        /// </summary>
        /// <seealso cref="FluentValidation.Validators.PropertyValidator" />
        public class GUIDValidator : PropertyValidator
        {
            /// <summary>
            /// 是否允許字串參數為空白.
            /// </summary>
            /// <value><c>true</c> if [allow empty]; otherwise, <c>false</c>.</value>
            private bool AllowEmpty { get; set; }
    
            /// <summary>
            /// Initializes a new instance of the <see cref="GUIDValidator"/> class.
            /// </summary>
            /// <param name="allowEmpty">if set to <c>true</c> [allow empty].</param>
            public GUIDValidator(
                bool allowEmpty = false) : base("傳入參數錯誤。")
            {
                this.AllowEmpty = allowEmpty;
            }
    
            /// <summary>
            /// Returns true if ... is valid.
            /// </summary>
            /// <param name="context">The context.</param>
            /// <returns>
            ///   <c>true</c> if the specified context is valid; otherwise, <c>false</c>.
            /// </returns>
            protected override bool IsValid(PropertyValidatorContext context)
            {
                var propertyValue = context.PropertyValue as string;
    
                if (AllowEmpty &&
                    string.IsNullOrWhiteSpace(propertyValue))
                {
                    return true;
                }
    
                Guid guid;
                return Guid.TryParse(propertyValue, out guid);
            }
        }
    

  2. 接著在Extension的地方補上兩個擴充方法,分別是【應該是GUID】、【應該是GUID但允許其為空字串或Null】
    /// <summary>
    /// FluentValidation 自訂驗證擴充方法.
    /// </summary>
    public static class FluentValidationExtensions
    {
     /// <summary>
     /// 驗證結果是否有 Error.
     /// </summary>
     /// <param name="validationFailure"></param>
     /// <returns></returns>
     public static bool HasError(this ValidationFailure validationFailure)
     {
      return validationFailure != null &&
        !string.IsNullOrWhiteSpace(validationFailure.ErrorMessage);
     }
    
     /// <summary>
     /// 應該是 GUID 型別.
     /// </summary>
     /// <typeparam name="T"></typeparam>
     /// <typeparam name="TProperty">The type of the t property.</typeparam>
     /// <param name="ruleBuilder">The rule builder.</param>
     /// <returns>IRuleBuilderOptions&lt;T, TProperty&gt;.</returns>
     public static IRuleBuilderOptions<T, TProperty> IsGUID<T, TProperty>(
      this IRuleBuilder<T, TProperty> ruleBuilder)
     {
      return ruleBuilder.SetValidator(new GUIDValidator(allowEmpty: false));
     }
    
     /// <summary>
     /// 應該是 GUID 型別, 但允許 String.Empty.
     /// </summary>
     /// <typeparam name="T"></typeparam>
     /// <typeparam name="TProperty">The type of the t property.</typeparam>
     /// <param name="ruleBuilder">The rule builder.</param>
     /// <returns>IRuleBuilderOptions&lt;T, TProperty&gt;.</returns>
     public static IRuleBuilderOptions<T, TProperty> IsGUIDAllowEmpty<T, TProperty>(
      this IRuleBuilder<T, TProperty> ruleBuilder)
     {
      return ruleBuilder.SetValidator(new GUIDValidator(allowEmpty: true));
     }
    }
    
  3. 接著在原本驗證的地方補上
            
    //ID - 必填,應為GUID
    this.RuleFor(x => x.ID)
      .NotEmpty()
      .WithErrorCode("X400")
      .WithMessage("ID不得為空字串")
      .NotNull()
      .WithErrorCode("X400")
      .WithMessage("ID不得為Null")
      .IsGUID()
      .WithErrorCode("X400")
      .WithMessage("ID應為GUID");
    

再執行原本的驗證就會得到錯誤訊息 X400-ID應為GUID
對我來說不止讓程式可讀性增加之外,也讓驗證的地方被分離出來,做到所謂的關注點分離

以下補上幾個我常常用到的驗證擴充出來的方法供各位參考,再強調一次,因為公司要求輸入輸出都要被Log下來,所以所有參數都是從String出發去驗證



  • 驗證是否為DateTime or TimeStamp

    /// <summary>
        /// 驗證是否為DateTime
        /// </summary>
        /// <seealso cref="FluentValidation.Validators.PropertyValidator" />
        public class DateTimeValidator : PropertyValidator
        {
            /// <summary>
            /// 是否允許參數為空白.
            /// </summary>
            /// <value><c>true</c> if [allow empty]; otherwise, <c>false</c>.</value>
            private bool AllowEmpty { get; set; }
    
            /// <summary>
            /// Initializes a new instance of the <see cref="DateTimeValidator"/> class.
            /// </summary>
            /// <param name="allowEmpty">if set to <c>true</c> [allow empty].</param>
            public DateTimeValidator(bool allowEmpty) : base("型別錯誤")
            {
                this.AllowEmpty = allowEmpty;
            }
    
            /// <summary>
            /// Returns true if ... is valid.
            /// </summary>
            /// <param name="context">The context.</param>
            /// <returns>
            ///   <c>true</c> if the specified context is valid; otherwise, <c>false</c>.
            /// </returns>
            protected override bool IsValid(PropertyValidatorContext context)
            {
                var propertyValue = context.PropertyValue as string;
    
                if (this.AllowEmpty &&
                    string.IsNullOrWhiteSpace(propertyValue))
                {
                    return true;
                }
    
                int value;
                bool result = int.TryParse(propertyValue, out value);
                //TimeStamp
                if (result && value > 0)
                {
                    return true;
                }
    
                DateTime dateTimeValue;
                return DateTime.TryParse(propertyValue, out dateTimeValue);
            }
        }
    

    擴充方法
            /// <summary>
            /// 是 DateTime 型別 or TimeStamp .
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <typeparam name="TProperty">The type of the t property.</typeparam>
            /// <param name="ruleBuilder">The rule builder.</param>
            /// <returns>IRuleBuilderOptions&lt;T, TProperty&gt;.</returns>
            public static IRuleBuilderOptions<T, TProperty> IsDateTimeOrTimeStamp<T, TProperty>(
                this IRuleBuilder<T, TProperty> ruleBuilder)
            {
                return ruleBuilder.SetValidator(new DateTimeValidator(allowEmpty: false));
            }
    
            /// <summary>
            /// 是 DateTime 型別 or TimeStamp, 但允許 String.Empty.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <typeparam name="TProperty">The type of the t property.</typeparam>
            /// <param name="ruleBuilder">The rule builder.</param>
            /// <returns>IRuleBuilderOptions&lt;T, TProperty&gt;.</returns>
            public static IRuleBuilderOptions<T, TProperty> IsDateTimeOrTimeStampAllowEmpty<T, TProperty>(
                this IRuleBuilder<T, TProperty> ruleBuilder)
            {
                return ruleBuilder.SetValidator(new DateTimeValidator(allowEmpty: true));
            }
    


  • 驗證是否為GUID Array

    /// <summary>
        /// 驗證是否為GUID Array
        /// </summary>
        public class GUIDArrayValidator : PropertyValidator
        {
            /// <summary>
            /// 是否允許字串參數為空白.
            /// </summary>
            /// <value><c>true</c> if [allow empty]; otherwise, <c>false</c>.</value>
            private bool AllowEmpty { get; set; }
    
            /// <summary>
            /// Initializes a new instance of the <see cref="GUIDArrayValidator"/> class.
            /// </summary>
            /// <param name="allowEmpty">if set to <c>true</c> [allow empty].</param>
            public GUIDArrayValidator(
                bool allowEmpty = false) :base("傳入參數錯誤。")
            {
                this.AllowEmpty = allowEmpty;
            }
    
            /// <summary>
            /// Returns true if ... is valid.
            /// </summary>
            /// <param name="context">The context.</param>
            /// <returns>
            ///   <c>true</c> if the specified context is valid; otherwise, <c>false</c>.
            /// </returns>
            protected override bool IsValid(PropertyValidatorContext context)
            {
                var propertyValue = context.PropertyValue as List<string>;
                if (AllowEmpty &&
                    (propertyValue == null || propertyValue.Count == 0))
                {
                    return true;
                }
    
                if (!AllowEmpty &&
                    (propertyValue == null || propertyValue.Count == 0))
                {
                    return false;
                }
    
                Guid guid;
                foreach (var item in propertyValue)
                {
                    if (!Guid.TryParse(item, out guid))
                    {
                        return false;
                    }
                }
    
                return true;
            }
        }
    

    擴充方法
    /// <summary>
            /// 是 Guid Array, 但允許空集合.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <typeparam name="TProperty">The type of the t property.</typeparam>
            /// <param name="ruleBuilder">The rule builder.</param>
            /// <returns>IRuleBuilderOptions&lt;T, TProperty&gt;.</returns>
            public static IRuleBuilderOptions<T, TProperty> IsGUIDArrayAllowEmpty<T, TProperty>(
                this IRuleBuilder<T, TProperty> ruleBuilder)
            {
                return ruleBuilder.SetValidator(new GUIDArrayValidator(allowEmpty: true));
            }
    
            /// <summary>
            /// 是 Guid Array.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <typeparam name="TProperty">The type of the t property.</typeparam>
            /// <param name="ruleBuilder">The rule builder.</param>
            /// <returns>IRuleBuilderOptions&lt;T, TProperty&gt;.</returns>
            public static IRuleBuilderOptions<T, TProperty> IsGUIDArray<T, TProperty>(
                this IRuleBuilder<T, TProperty> ruleBuilder)
            {
                return ruleBuilder.SetValidator(new GUIDArrayValidator(allowEmpty: false));
            }
    
    

  • 驗證是否為數字

    /// <summary>
        /// 驗證是否為Integer
        /// </summary>
        /// <seealso cref="FluentValidation.Validators.PropertyValidator" />
        public class IntegerValidator : PropertyValidator
        {
            /// <summary>
            /// 是否允許字串參數為空白.
            /// </summary>
            /// <value><c>true</c> if [allow empty]; otherwise, <c>false</c>.</value>
            private bool AllowEmpty { get; set; }
    
            /// <summary>
            /// Initializes a new instance of the <see cref="IntegerValidator"/> class.
            /// </summary>
            /// <param name="allowEmpty">if set to <c>true</c> [allow empty].</param>
            public IntegerValidator(bool allowEmpty = false)
                : base("型別錯誤")
            {
                this.AllowEmpty = allowEmpty;
            }
    
            /// <summary>
            /// Returns true if ... is valid.
            /// </summary>
            /// <param name="context">The context.</param>
            /// <returns>
            ///   <c>true</c> if the specified context is valid; otherwise, <c>false</c>.
            /// </returns>
            /// <exception cref="NotImplementedException"></exception>
            protected override bool IsValid(PropertyValidatorContext context)
            {
                var propertyValue = context.PropertyValue as string;
    
                if (this.AllowEmpty &&
                    string.IsNullOrWhiteSpace(propertyValue))
                {
                    return true;
                }
    
                int value;
                bool result = int.TryParse(propertyValue, out value);
                return result;
            }
        }
    

    擴充方法
            /// <summary>
            /// 是 Integer 型別.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <typeparam name="TProperty">The type of the t property.</typeparam>
            /// <param name="ruleBuilder">The rule builder.</param>
            /// <returns>IRuleBuilderOptions&lt;T, TProperty&gt;.</returns>
            public static IRuleBuilderOptions<T, TProperty> IsInteger<T, TProperty>(
                this IRuleBuilder<T, TProperty> ruleBuilder)
            {
                return ruleBuilder.SetValidator(new IntegerValidator(allowEmpty: false));
            }
    
            /// <summary>
            /// 是 Integer 型別, 但允許 String.Empty.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <typeparam name="TProperty">The type of the t property.</typeparam>
            /// <param name="ruleBuilder">The rule builder.</param>
            /// <returns>IRuleBuilderOptions&lt;T, TProperty&gt;.</returns>
            public static IRuleBuilderOptions<T, TProperty> IsIntegerAllowEmpty<T, TProperty>(
                this IRuleBuilder<T, TProperty> ruleBuilder)
            {
                return ruleBuilder.SetValidator(new IntegerValidator(allowEmpty: true));
            }
    

  • 檢查數字是否在要求範圍內

    /// <summary>
        /// 驗證數字是否在範圍內
        /// </summary>
        /// <typeparam name="TNumeric">The type of the numeric.</typeparam>
        /// <seealso cref="FluentValidation.Validators.PropertyValidator" />
        public class NumericBetweenInValidator<TNumeric> : PropertyValidator
            where TNumeric : IComparable
        {
            private TNumeric compareValueUp;
    
            private TNumeric compareValueDown;
    
            /// <summary>
            /// 是否允許字串參數為空白.
            /// </summary>
            /// <value><c>true</c> if [allow empty]; otherwise, <c>false</c>.</value>
            private bool AllowEmpty { get; set; }
    
            /// <summary>
            /// 轉型是否成功
            /// </summary>
            private bool IsConvertable;
    
            /// <summary>
            /// 是否允許等於輸入的上下閥值值
            /// </summary>
            private bool AllowEquals;
    
            public NumericBetweenInValidator(
                string valueUp,
                string valueDown,
                bool allowEquals = false,
                bool allowEmpty = false) : base("傳入參數錯誤。")
            {
                this.compareValueUp = ConvertHelper.ToT<TNumeric>(valueUp, out IsConvertable);
    
                this.compareValueDown = ConvertHelper.ToT<TNumeric>(valueDown, out IsConvertable);
    
                this.AllowEquals = allowEquals;
                this.AllowEmpty = allowEmpty;
            }
    
            protected override bool IsValid(PropertyValidatorContext context)
            {
                var propertyValue = context.PropertyValue as string;
    
                if (this.AllowEmpty &&
                    string.IsNullOrWhiteSpace(propertyValue))
                {
                    return true;
                }
    
    
                var value = ConvertHelper.ToT<TNumeric>(propertyValue, out IsConvertable);
    
                if (!IsConvertable)
                {
                    return false;
                }
    
                // -1 value < compareValue
                // 0  value = compareValue
                // 1  value > compareValue
                if (AllowEquals)
                {
                    return value.CompareTo(compareValueDown) >= 0
                           && value.CompareTo(compareValueUp) <= 0;
                }
    
                return value.CompareTo(compareValueDown) > 0
                       && value.CompareTo(compareValueUp) < 0;
            }
        }
    
    /// <summary>
        /// Class ConvertHelper
        /// </summary>
        internal static class ConvertHelper
        {
            /// <summary>
            /// 轉型成 T.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <param name="value">The value.</param>
            /// <returns></returns>
            /// <exception cref="System.ArgumentException">Convert fail.;Convert</exception>
            public static T ToT<T>(string value, out bool result) where T : IComparable
            {
                result = false;
    
                try
                {
                    switch (Type.GetTypeCode(typeof(T)))
                    {
                        case TypeCode.Double:
    
                            double doubleValue;
                            if (double.TryParse(value, out doubleValue))
                            {
                                result = true;
                            }
    
                            return (T)(object)Convert.ToDouble(value);
    
                        case TypeCode.Int16:
    
                            Int16 int16Value;
                            if (Int16.TryParse(value, out int16Value))
                            {
                                result = true;
                            }
    
                            return (T)(object)Convert.ToInt16(value);
    
                        case TypeCode.Int32:
    
                            Int32 int32Value;
                            if (Int32.TryParse(value, out int32Value))
                            {
                                result = true;
                            }
    
                            return (T)(object)Convert.ToInt32(value);
    
                        case TypeCode.Int64:
    
                            Int64 int64Value;
                            if (Int64.TryParse(value, out int64Value))
                            {
                                result = true;
                            }
    
                            return (T)(object)Convert.ToInt64(value);
    
                        case TypeCode.Decimal:
    
                            decimal decimalValue;
                            if (decimal.TryParse(value, out decimalValue))
                            {
                                result = true;
                            }
    
                            return (T)(object)Convert.ToDecimal(value);
    
                        default:
                            return default(T);
                    }
                }
                catch (Exception ex)
                {
                    return default(T);
                }
            }
        }
    
    擴充方法
    /// <summary>
            /// 符合數字區間,但允許空值.
            /// <para>EX : (1 &lt; x &lt; 3) </para>
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <typeparam name="TProperty">The type of the property.</typeparam>
            /// <typeparam name="TNumeric">The type of the numeric.</typeparam>
            /// <param name="ruleBuilder">The rule builder.</param>
            /// <param name="upThreshold">Up threshold.</param>
            /// <param name="downThreshold">Down threshold.</param>
            /// <returns></returns>
            public static IRuleBuilderOptions<T, TProperty> IsNumericAllowEmptyOrBetweenOf<T, TProperty, TNumeric>(
                this IRuleBuilder<T, TProperty> ruleBuilder,
                string upThreshold,
                string downThreshold)
                where TNumeric : IComparable
            {
                return ruleBuilder.SetValidator(
                    new NumericBetweenInValidator<TNumeric>(
                        upThreshold,
                        downThreshold,
                        allowEquals: false,
                        allowEmpty: true));
            }
    
            /// <summary>
            /// 符合數字區間且允許等於閥值,但允許空值.
            /// <para>EX : (1 &lt;= x &lt;= 3) </para>
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <typeparam name="TProperty">The type of the property.</typeparam>
            /// <typeparam name="TNumeric">The type of the numeric.</typeparam>
            /// <param name="ruleBuilder">The rule builder.</param>
            /// <param name="upThreshold">Up threshold.</param>
            /// <param name="downThreshold">Down threshold.</param>
            /// <returns></returns>
            public static IRuleBuilderOptions<T, TProperty> IsNumericAllowEmptyOrBetweenOfAllowEquals<T, TProperty, TNumeric>(
                this IRuleBuilder<T, TProperty> ruleBuilder,
                string upThreshold,
                string downThreshold)
                where TNumeric : IComparable
            {
                return ruleBuilder.SetValidator(
                    new NumericBetweenInValidator<TNumeric>(
                        upThreshold,
                        downThreshold,
                        allowEquals: true,
                        allowEmpty: true));
            }
    
            /// <summary>
            /// 符合數字區間.
            /// <para>EX : (1 &lt; x &lt; 3) </para>
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <typeparam name="TProperty">The type of the property.</typeparam>
            /// <typeparam name="TNumeric">The type of the numeric.</typeparam>
            /// <param name="ruleBuilder">The rule builder.</param>
            /// <param name="upThreshold">Up threshold.</param>
            /// <param name="downThreshold">Down threshold.</param>
            /// <returns></returns>
            public static IRuleBuilderOptions<T, TProperty> IsNumericBetweenOf<T, TProperty, TNumeric>(
                this IRuleBuilder<T, TProperty> ruleBuilder,
                string upThreshold,
                string downThreshold)
                where TNumeric : IComparable
            {
                return ruleBuilder.SetValidator(
                    new NumericBetweenInValidator<TNumeric>(
                        upThreshold,
                        downThreshold,
                        allowEquals: false,
                        allowEmpty: false));
            }
    
            /// <summary>
            /// 符合數字區間且允許等於閥值
            /// <para>EX : (1 &lt;= x &lt;= 3) </para>
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <typeparam name="TProperty">The type of the property.</typeparam>
            /// <typeparam name="TNumeric">The type of the numeric.</typeparam>
            /// <param name="ruleBuilder">The rule builder.</param>
            /// <param name="upThreshold">Up threshold.</param>
            /// <param name="downThreshold">Down threshold.</param>
            /// <returns></returns>
            public static IRuleBuilderOptions<T, TProperty> IsNumericBetweenOfAllowEquals<T, TProperty, TNumeric>(
                this IRuleBuilder<T, TProperty> ruleBuilder,
                string upThreshold,
                string downThreshold)
                where TNumeric : IComparable
            {
                return ruleBuilder.SetValidator(
                    new NumericBetweenInValidator<TNumeric>(
                        upThreshold,
                        downThreshold,
                        allowEquals: true,
                        allowEmpty: false));
            }
    
    使用方法
    //OrderBy - 允許空值或(0、1)
    this.RuleFor(x => x.OrderBy)
      .NotNumericAllowEmptyOrBetweenOfAllowEquals<GetBuildingDealCaseParameter, string, int>(
      "2",
      "1")
      .WithErrorCode("X400")
      .WithMessage("OrderBy應該在1~2之間");
    

  • 驗證是否為數字Array

    /// <summary>
        /// 驗證是否為數字 Array
        /// </summary>
        /// <seealso cref="FluentValidation.Validators.PropertyValidator" />
        public class NumericArrayValidator<TNumeric> : PropertyValidator
            where TNumeric : IComparable
        {
            /// <summary>
            /// 是否允許Array為Null或空集合.
            /// </summary>
            /// <value>
            ///   <c>true</c> if [allow empty]; otherwise, <c>false</c>.
            /// </value>
            private bool AllowEmpty { get; set; }
    
            /// <summary>
            /// Initializes a new instance of the <see cref="NumericArrayValidator{TNumeric}"/> class.
            /// </summary>
            /// <param name="allowEmpty">if set to <c>true</c> [allow empty].</param>
            public NumericArrayValidator(bool allowEmpty) : base("型別錯誤")
            {
                this.AllowEmpty = allowEmpty;
            }
    
            /// <summary>
            /// Returns true if ... is valid.
            /// </summary>
            /// <param name="context">The context.</param>
            /// <returns>
            ///   <c>true</c> if the specified context is valid; otherwise, <c>false</c>.
            /// </returns>
            protected override bool IsValid(PropertyValidatorContext context)
            {
                var propertyValue = context.PropertyValue as List<string>;
    
                if (this.AllowEmpty &&
                    (propertyValue == null || propertyValue.Count == 0))
                {
                    return true;
                }
    
                //不允許空集合或Null
                if (!this.AllowEmpty &&
                    (propertyValue == null || propertyValue.Count == 0))
                {
                    return false;
                }
    
    
                bool IsConvertable;
                foreach (var x in propertyValue)
                {
                    ConvertHelper.ToT<TNumeric>(x, out IsConvertable);
                    if (!IsConvertable)
                    {
                        return false;
                    }
                }
                return true;
            }
        }
    
    擴充方法
    /// <summary>
            /// 是數字 Array,但允許空陣列或Null.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <typeparam name="TProperty">The type of the property.</typeparam>
            /// <typeparam name="TNumeric">The type of the numeric.</typeparam>
            /// <param name="ruleBuilder">The rule builder.</param>
            /// <returns></returns>
            public static IRuleBuilderOptions<T, TProperty> IsNumericArrayAllowEmpty<T, TProperty, TNumeric>(
                this IRuleBuilder<T, TProperty> ruleBuilder)
                where TNumeric : IComparable
            {
                return ruleBuilder.SetValidator(
                    new NumericArrayValidator<TNumeric>(allowEmpty: true));
            }
    
    
            /// <summary>
            /// 是數字 Array.
            /// </summary>
            /// <typeparam name="T"></typeparam>
            /// <typeparam name="TProperty">The type of the property.</typeparam>
            /// <typeparam name="TNumeric">The type of the numeric.</typeparam>
            /// <param name="ruleBuilder">The rule builder.</param>
            /// <returns></returns>
            public static IRuleBuilderOptions<T, TProperty> IsNumericArray<T, TProperty, TNumeric>(
                this IRuleBuilder<T, TProperty> ruleBuilder)
                where TNumeric : IComparable
            {
                return ruleBuilder.SetValidator(
                    new NumericArrayValidator<TNumeric>(allowEmpty: false));
            }