[NestedModelValidation]使用 ValidationAttribute 來驗證 Model 是否符合 Validation Rules

  • 9660
  • 0
  • 2013-07-03

[NestedModelValidation]使用 ValidationAttribute 來驗證 Model 是否符合 Validation Rules

前言

從 .NET Framework 3.5 推出 Dynamic Data Web 時,就留意到了 DataAnnotation 這種善用 Attribute 來提昇設計的內聚力,以及使用一致的方式來「標記」一些目的的美妙。直到 ASP.NET MVC 直接運用在針對 model 進行 binding, 自動產生 validator 等動作,這樣的方式就更趨近於完成體了。(在 ASP.NET 4.5 Webform 、Web API 也都納入此機制)

不過,實務上的需求以及現存的系統,有蠻多不允許使用 Dynamice Data Web 或翻寫成 ASP.NET MVC 來設計。更甚至說,我們往往有許多 model validation 的動作,是與 UI 完全無關的(這一點跟 Web API 比較接近),例如資料已經被序列化存在於 DB 中,或是 web service 類型(如 .ashx, .asmx, WCF 等等…)當 request message 進來後,會被反序列化成對應的 model ,這時反序列化基本上只能協助驗證 model property 的 type 是否可以相容,但無法達到像其他 ValidationAttribute 的限制:如 Range, Required, 字數長度, RegularExpression 等等。

因此,這個 project 的目的相當簡單,因應我們環境條件,希望可以做到幾點:

  1. Creating a model-validation total solution
  2. Adopting this solution immediately in our production environment (.NET framework 4.0, ASP.NET Webform, .ashx, .asmx, WCF, console project, library)
  3. Easily design, easily use

而從需求端出發,我們希望這個 library 可以提供一些 features :

  1. Support Nested/Composite Model
  2. Support Collection Enumerate for Validating
  3. Support Custom Validation Rules
  4. Just .NET Framework 4.0
  5. Just invoke DataAnnotationValidator.TryValidate(model): bool isValid

 

怎麼開始

首先,絕大部分的事情,System.Component.DataAnnotation 這顆 DLL 都幫我們做掉了,因此我們要做的,只是把 DataAnnotation 做不到的部份補上去。

先來看一下個最簡單的例子 (TDD 設計第一步,可以從最常見、簡單的需求來進行設計),請看下面的 model: Person ,這邊我們希望限制 Name 這個 property 應該是必要輸入的,且不得為空字串,因此使用原生的 ValidationAttribute: Required。

    {
        public int Id { get; set; }

        [Required(AllowEmptyStrings = false)]
        public string Name { get; set; }

        public DateTime Birthday { get; set; }       
    }

接著來設計針對這個需求的測試案例,有一個 Person Name 為空值時,呼叫驗證方法,應得到 Name 不得為空。

這邊我使用 BDD 的方式來設計測試案例,工具是 SpecFlow ,不熟悉 TDD, BDD 的朋友,可以參考一下前面的系列文: [30天快速上手TDD]目錄與附錄

image

這邊簡單貼一下 Scenario 所對應的 Step 資訊:

        [Scope(Feature = "PropertyValidation")]        
        [BeforeScenario]
        public static void BeforeFeature()
        {
            target = new DataAnnotationValidator();
        }

        [Scope(Feature = "PropertyValidation", Scenario = "驗證Person的Name必須有值,若null或空字串,則驗證結果失敗,取得對應的error message")]
        [Given(@"針對Person")]
        public void Given針對Person(Table table)
        {
            var person = table.CreateInstance<Person>();            

            ScenarioContext.Current.Set<Person>(person);
        }

        [Scope(Feature = "PropertyValidation", Scenario = "驗證Person的Name必須有值,若null或空字串,則驗證結果失敗,取得對應的error message")]
        [When(@"呼叫DataAnnotationValidator的TryValidate方法")]
        public void When呼叫DataAnnotationValidator的TryValidate方法()
        {
            var person = ScenarioContext.Current.Get<Person>();

            bool result = target.TryValidate(person);

            ScenarioContext.Current.Set<ICollection<ValidationResult>>(target.ValidationResults);
            ScenarioContext.Current.Set<bool>(result, "result");
        }

        [Scope(Feature = "PropertyValidation")]
        [Then(@"應回傳 (.*)")]
        public void Then應回傳False(bool expectResult)
        {
            var result = ScenarioContext.Current.Get<bool>("result");
            Assert.AreEqual(expectResult, result);
        }

        [Scope(Feature = "PropertyValidation")]
        [Then(@"ValidationResult應為 (.*) 筆,其PropertyName為 (.*) ,其ErrorMessage應為 (.*)")]
        public void ThenValidationResult應為筆其PropertyName為Name其ErrorMessage應為Xxx(int errorCount, string propertyName, string errorMessage)
        {
            var validationResults = ScenarioContext.Current.Get<ICollection<ValidationResult>>().ToList();

            Assert.AreEqual(errorCount, validationResults.Count);

            if (string.IsNullOrEmpty(propertyName) || errorCount == 0)
            {
                return;
            }

            var properties = propertyName.Split(',');
            var errorMessages = errorMessage.Split(',');

            for (int i = 0; i < properties.Length; i++)
            {
                Assert.AreEqual(properties[i], validationResults[i].MemberNames.FirstOrDefault());
                Assert.AreEqual(errorMessages[i], validationResults[i].ErrorMessage);
            }
        }

要注意的地方,只有:

bool result = target.TryValidate(person);
var validationResults = target.ValidationResults;

不管 model 多麼複雜,就是使用 DataAnnotationValidator 的 TryValidate 方法,傳入要驗證的 model ,回傳值即為 isValid 代表是否驗證通過。當驗證為非法時,要取得相關驗證錯誤資訊,只要取得 DataAnnotationValidator 的 ValidationResults 即可取得驗證錯誤的 property name ,以及對應的錯誤訊息。

以這個 case 來說,取得的 ErrorMessage 即為: The Name field is required.

image

單純的 model 如上述所提,只要使用 DataAnnotation 所提供的 class 即可達成,接著讓我們看一下 DataAnnotationValidator 是怎麼進行驗證的。基本上只要下列三行,即可驗證該 Entity 是否合法,以及相關錯誤訊息為何。

        {
            var context = new ValidationContext(model, serviceProvider: null, items: null);
            var validationResults = new List<ValidationResult>();

            var result = Validator.TryValidateObject(model, context, validationResults, validateAllProperties: true);

            //  ....

         }

Validator 是 DataAnnotation 的靜態類別,其中有不少用來 Validate 的方法,有興趣的朋友可以自行了解一下。

 

Model 如果是組合型或集合型態的類別呢?

接著,我們想了解的是:若 model 是組合型的類別呢?或是是集合型態的 property 呢?

為了使得原生的 Validator 靜態類別可以直接使用,這邊我們自訂了一個 NestedValidationAttribute ,專門用來 drill down 該 property ,不論是展開集合,或是把標記的 property 當獨立的 model 繼續 validate ,都是透過 NestedValidationAttribute 來標記。

一樣先來看一下,需求的測試案例應該長怎樣。請見下面的 model ,這邊 Order 除了自身帶著一個 int Id 的 property ,還帶著一個 MyCustomer 的 property ,其 type 為 Customer。

    {
        [Range(0, 2)]
        public int Id { get; set; }

        [Range(18, 80)]
        public int Age { get; set; }
    }

    public class Order
    {
        [Range(0, 100)]
        public int Id { get; set; }

        [NestedValidation]
        public Customer MyCustomer { get; set; }
    }

接著看一下這個 scenario 如何描述:

image

這邊我方便起見,把 errorMessage 用逗號串起來,以便在同一個 example 中呈現。

可以看到,當 MyCustomer 中的值有錯誤時,最後的 ValidationResults 中會有對應的錯誤訊息,並且其 MemberNames 為 「MyCustomer.Id」、「MyCustomer.Age」,這樣讀取訊息的人就不會不了解是哪一個 Id 或 Age 。依此類推,深度越深,前面就會串越多 property name。錯誤訊息也是一樣,可以看到是哪一個 property 裡面的 property 驗證錯誤。

在揭開 NestedValidationAttribute 之前,我想再把集合型態的測試案例也放進來說明,雖然一般我在用 TDD 開發時,比較少會把多個 scenario 寫完在做一次開發,因為那違背了 TDD 的開發習慣,小步前進,小步重構,可以讓 engineer 持續 focus, 用最小的力氣,一步一步達成實際的目標。

這邊,為了方便顯示 model 的階層起見,我就使用 Father 來當例子:

    {
        [Range(1, 130)]
        public int Age { get; set; }

        [Required]
        public string Name { get; set; }

        [NestedValidation]
        public IEnumerable<Son> Sons { get; set; }
    }

    public class Son : IValidatableObject
    {
        [Range(1, 130)]
        public int Age { get; set; }

        [Required(AllowEmptyStrings = false)]
        public string Name { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            var result = new List<ValidationResult>();

            if (this.Age % 2 == 0)
            {
                var memberNames = new List<string> { "Age" };
                result.Add(new ValidationResult("Age不得為偶數", memberNames));
            }

            return result;
        }
    }

IValidatableObject 介面後面會說明

測試案例如下:

        public void 驗證List集合是否能正常驗證Validation()
        {
            var father = new Father
            {
                Name = "Joey",
                Age = 999,
                Sons = new List<Son>                
                {
                    new Son{Age=0},
                    new Son{Name=string.Empty, Age=200},
                    new Son{Name="91", Age=300},
                }
            };

            var validator = new DataAnnotationValidator();

            var isValid = validator.TryValidate(father);

            Assert.AreEqual(false, isValid);

            Assert.AreEqual(6, validator.ValidationResults.Count);

            Assert.AreEqual("The field Age must be between 1 and 130.", validator.ValidationResults[0].ErrorMessage);
            Assert.AreEqual("The field Sons[0].Age must be between 1 and 130.", validator.ValidationResults[1].ErrorMessage);
            Assert.AreEqual("The Sons[0].Name field is required.", validator.ValidationResults[2].ErrorMessage);
            Assert.AreEqual("The field Sons[1].Age must be between 1 and 130.", validator.ValidationResults[3].ErrorMessage);
            Assert.AreEqual("The Sons[1].Name field is required.", validator.ValidationResults[4].ErrorMessage);
            Assert.AreEqual("The field Sons[2].Age must be between 1 and 130.", validator.ValidationResults[5].ErrorMessage);
        }

這邊要留意一下,因為集合中不容易辨別是哪一個 element 導致的問題,因此我們希望得到的 ErrorMessage 是可以看出集合的 index ,讓呼叫端可以很明確地了解是哪個部分導致驗證失敗。

 

NestedValidationAttribute 的設計

需求很明確,接著是程式要怎麼寫。讓我們來看 NestedValidationAttribute 寫了哪些東西。先直接來看完整的程式碼:

        /// Validates the specified entity with respect to the current validation attribute.
        /// </summary>
        /// <param name="entity">The entity to validate.</param>
        /// <param name="validationContext">The context information about the validation operation.</param>
        /// <returns>
        /// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult" /> class.
        /// </returns>
        protected override ValidationResult IsValid(object entity, ValidationContext validationContext)
        {
            if (validationContext == null)
            {
                throw new ArgumentNullException("validationContext");
            }

            if (entity == null)
            {
                return ValidationResult.Success;
            }

            var displayName = validationContext.DisplayName;
            var compositeResults = new CompositeValidationResult(string.Format("Validation for {0} failed!", displayName));

            IEnumerable items = entity as IEnumerable;

            if (items != null)
            {
                var index = 0;
                foreach (var item in items)
                {
                    var validator = new DataAnnotationValidator();

                    validator.TryValidate(item);
                    var results = validator.ValidationResults;

                    if (results.Count != 0)
                    {
                        results.ForEach(x => compositeResults.AddResult(x, displayName, index));
                    }

                    index++;
                }
                
                var isAnythingInvalid = compositeResults.Results.Any();

                return isAnythingInvalid ? compositeResults : ValidationResult.Success;
            }
            else
            {
                var validator = new DataAnnotationValidator();

                validator.TryValidate(entity);
                var results = validator.ValidationResults;

                if (results.Count != 0)
                {
                    results.ForEach(x => compositeResults.AddResult(x, displayName));
                    return compositeResults;
                }
            }

            return ValidationResult.Success;
        }

 

可以看到 NestedValidationAttribute 是繼承自 ValidationAttribute ,因此,Validator 的 TryValidateObject 對 NestedValidationAttribute 也會有效。接著,只要覆寫 IsValid 方法即可。

注意,這邊會碰到一個很大的問題,一個 ValidationAttribute 的 IsValid 方法,只會回傳一個 ValidationResult 。但在我們的需求中, NestedValidationAttribute 是給 組合型/巢狀/集合型態 的 model 來使用的,因此標記為 NestedValidationAttribute 的 property ,通常應該要得到不只一個 ValidationResult 才是我們要的。

 

如何在一個 ValidationAttribute 中回傳多個 ValidationResult

這裡,我們再自訂一個繼承自 ValidationResult 的 CompositeValidationResult 類別。

剛剛的 NestedValidationAttribute 繼承自 ValidationAttribute 目的是為了覆寫 IsValid 行為。這裡的 CompositeValidationResult 則是透過繼承來擴充原本的型別。將只能回傳一個 ValidationResult ,改為回傳一個 CompositeValidationResult ,接著在 CompositeValidationResult 中,便可以取得多個 ValidationResult ,接下來來看一下 CompositeValidationResult 做了什麼。

雖然這兩個作法只是物件導向中,最基本的繼承應用,一個是覆寫父類行為,一個是擴充父類。但因為繼承為 is-A 的關係,使得舊有的驗證類別都不需要修改,也能達到我們期望的結果。

    /// 一個ValidationAttribute只能回傳一個ValidationResult, 透過CompositeValidationResult的擴充,可從Results獲得多個ValidationResult的集合。
    /// 透過AddResult()也可以加入ValidationResult的element。
    /// </summary>
    public class CompositeValidationResult : ValidationResult
    {
        /// <summary>
        /// The _results
        /// </summary>
        private readonly List<ValidationResult> _results = new List<ValidationResult>();

        /// <summary>
        /// Initializes a new instance of the <see cref="CompositeValidationResult"/> class.
        /// </summary>
        /// <param name="errorMessage">The error message.</param>
        public CompositeValidationResult(string errorMessage)
            : base(errorMessage)
        {
        }

        /// <summary>
        /// Gets the results.
        /// </summary>
        /// <value>
        /// The results.
        /// </value>
        public IEnumerable<ValidationResult> Results
        {
            get
            {
                return this._results;
            }
        }

        /// <summary>
        /// Adds the result.
        /// </summary>
        /// <param name="validationResult">The validation result.</param>
        /// <param name="displayName">The display name.</param>
        public void AddResult(ValidationResult validationResult, string displayName)
        {
            if (validationResult == null)
            {
                throw new ArgumentNullException("validationResult");
            }

            var fieldName = validationResult.MemberNames.FirstOrDefault();
            if (fieldName != null)
            {
                var propertyName = string.Format("{0}.{1}", displayName, fieldName);
                var errorMessage = validationResult.ErrorMessage.Replace(fieldName, propertyName);

                var memberNames = validationResult.MemberNames.Select(x => propertyName).ToList();
                var result = new ValidationResult(errorMessage, memberNames);

                this._results.Add(result);
            }
        }

        /// <summary>
        /// Adds the result.
        /// </summary>
        /// <param name="validationResult">The validation result.</param>
        /// <param name="displayName">The display name.</param>
        /// <param name="index">The index.</param>
        public void AddResult(ValidationResult validationResult, string displayName, int index)
        {
            if (validationResult == null)
            {
                throw new ArgumentNullException("validationResult");
            }

            var fieldName = validationResult.MemberNames.FirstOrDefault();
            if (fieldName != null)
            {
                var propertyName = string.Format("{0}[{2}].{1}", displayName, fieldName, index.ToString());
                var errorMessage = validationResult.ErrorMessage.Replace(fieldName, propertyName);

                var memberNames = validationResult.MemberNames.Select(x => propertyName).ToList();
                var result = new ValidationResult(errorMessage, memberNames);

                this._results.Add(result);
            }
        }
    }

CompositeValidationResult 相當單純,透過兩個多載方法,供外部可以 Add 多個 ValidationResult 到 Results property中。這樣,最後呼叫端就可以將 CompositeValidationResult.Results 攤平,取得多個 ValidationResult 。

 

在 NestedValidationAttribute 中使用遞迴

回到 NestedValidationAttribute ,其實用的概念也相當簡單: 遞迴+巡覽集合

先解釋 NestedValidationAttribute 標記的 property 若非集合,代表被標記的 property instance ,還需要再一次被 Validator 驗證,然後將每次的驗證結果暫存起來。如下面這段程式碼:

                validator.TryValidate(entity);
                var results = validator.ValidationResults;

                if (results.Count != 0)
                {
                    results.ForEach(x => compositeResults.AddResult(x, displayName));
                    return compositeResults;
                }

我們在 NestedValidationAttribute 中,遞迴呼叫 DataAnnotationValidator 的 TryValidate 方法。這時當然也會取得相關的驗證結果。因為 IsValid 方法,僅能回傳一個 ValidationResult ,因此我們使用 CompositeValidationResult 來取代。

在回傳之前,將遞迴呼叫所取得的 DataAnnotationValidator.ValidationResults ,都塞到 CompositeValidationResult 中,再回傳這個 CompositeValidationResult。(雖然只回傳一個 CompositeValidationResult ,但它身上的 Results 卻是遞迴驗證後取得的多個 ValidationResult)。

看完單一 instance 的狀況,接著來看如果 NestedValidationAttribute 標記的是集合時,該怎麼處理。

其實也相當簡單,判斷是否為集合,foreach 展開集合中每一個 element ,這些 element 就跟剛剛單一的 instance 一樣,遞迴呼叫 DataAnnotationValidator.TryValidate() 即可。如下所示:

            if (items != null)
            {
                var index = 0;
                foreach (var item in items)
                {
                    var validator = new DataAnnotationValidator();

                    validator.TryValidate(item);
                    var results = validator.ValidationResults;

                    if (results.Count != 0)
                    {
                        results.ForEach(x => compositeResults.AddResult(x, displayName, index));
                    }

                    index++;
                }

                return compositeResults;
            }

判斷是否為 IEnumerable ,若是,則代表可以使用 foreach 展開。其他的,就跟單一 instance 處理方式一模一樣,只是在遞迴外多加了一層迴圈罷了。

 

DataAnnotationValidator 展開 CompositeValidationResult

接著,回過頭來看 DataAnnotationValidator 拿到了這些 ValidatorResults 時,該怎麼處理。程式碼如下:

事實上,DataAnnotationValidator 所取得的 ValidationResults ,可能包含了 ValidationResult 與 CompositeValidationResult 兩種型別,而我們要針對 CompositeValidationResult 的部分攤平展開。

        /// try to validate model
        /// </summary>
        /// <param name="model">The model.</param>
        /// <returns>isValid</returns>
        public bool TryValidate(object model)
        {
            var context = new ValidationContext(model, serviceProvider: null, items: null);
            var validationResults = new List<ValidationResult>();

            var result = Validator.TryValidateObject(model, context, validationResults, validateAllProperties: true);

            this.ValidationResults = new List<ValidationResult>();

            this.ValidationResults.AddRange(validationResults.OfType<ValidationResult>().Where(x => !(x is CompositeValidationResult)));

            var customValidationResults = validationResults.OfType<CompositeValidationResult>();

            foreach (var customValidationResult in customValidationResults)
            {
                this.ValidationResults.AddRange(customValidationResult.Results);
            }

            return result;
        }

如上面所提,validationResults 其實可能包含了一般 ValidationAttribute 所回傳的 ValidationResult, 也包含了 NestedValidationAttribute 所回傳的 ComposisteValidationResult。

而我們最後想取得的結果,是全部攤開的 ValidationResult ,因此只要針對 ComposisteValidationResult 展開,加回最後要回傳的 ValidationResults 即可。

this.ValidationResults.AddRange(validationResults.OfType<ValidationResult>().Where(x => !(x is CompositeValidationResult)));

先將正常的 ValidationResult 加入要回傳的 ValidationResults 中。

var customValidationResults = validationResults.OfType<CompositeValidationResult>();

接著取出 validationResults 中 type 為 CompositeValidationResult 的 elements。

foreach (var customValidationResult in customValidationResults) { this.ValidationResults.AddRange(customValidationResult.Results); }

展開 customValidationResults ,把裡面的 Results 都倒給 this.ValidationResults 中。

如此一來, context 端呼叫 DataAnnotationValidator 的 TryValidate 方法後,就可以透過 DataAnnotationValidator 的 ValidationResults 取得該 model 所有有被標示 ValidationAttribute 的驗證結果了。

 

自訂商業邏輯驗證:實作 IValidatableObject

還有一個東西忘了提,也就是 IValidatableObject 介面。

當 model 需要自訂的商業邏輯做驗證時,只需要實作 IValidatableObject 的

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)

就可以自行定義該 model 除了 property 要滿足驗證條件以外,還需要滿足 Validate 方法的限制。如下所示:

    {
        [Range(100, int.MaxValue)]
        public int Cost { get; set; }

        [Range(100, int.MaxValue)]
        public int SellPrice { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            var results = new List<ValidationResult>();

            var benefitRate = this.SellPrice / Convert.ToDouble(this.Cost);

            var minimumBenefitRate = 0.06;
            if (benefitRate - 1 < minimumBenefitRate)
            {
                var memberNames = new List<string> { "毛利率" };
                results.Add(new ValidationResult(string.Format("毛利率需大於{0}%", minimumBenefitRate * 100), memberNames));
            }

            return results;
        }
    }

或是

    {
        [Range(1, 130)]
        public int Age { get; set; }

        [Required(AllowEmptyStrings = false)]
        public string Name { get; set; }

        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            var result = new List<ValidationResult>();

            if (this.Age % 2 == 0)
            {
                var memberNames = new List<string> { "Age" };
                result.Add(new ValidationResult("Age不得為偶數", memberNames));
            }

            return result;
        }
    }

要特別注意一點的是,IValidatableObject 介面的 Validate 方法,需要該 model 所有的 property 都通過 validation 後,才會觸發。也就是 Validate() 不會與 ValidatonAttribute 同時作用,只有 ValidationAttribute 是驗證成功時,才會發動。

測試案例如下圖所示:

image

  1. 當 Cost 跟 SellPrice 都滿足 Range 條件時,才會觸發毛利率的檢查。
  2. 當 Cost 與 SellPrice 不滿足 Range 條件時,只會驗證 property 的部份,不會觸發毛利率的檢查。

其他的測試案例,歡迎各位朋友到 GitHub 上了解: GitHub 位置

 

結論

DataAnnotation 的方式,真的可以讓 model 的 validation 方式,變得更單純、簡單、一致、好維護。

期望達到的效益如下列所示:

  1. 增加大家的生產力
    以往各專案反序列化完,都需要再透過很多方式進行驗證,如我先前這篇文章所示:[ASP.NET]重構之路系列v9 –使用介面+迴圈取代不穩定的判斷式 。現在,至少可以讓 ValidationAttribute 解決掉一半以上的需求。
  2. 改善程式碼可維護性與可讀性
    在團隊中,絕大部分的人程式碼設計的方式都不太一致,也沒什麼絕對的好壞。但光一堆需要 validation 的邏輯,可能就會導致程式碼複雜度暴增,或是 validator class 暴增。透過這樣的方式,可以對該 model 該符合哪些驗證邏輯一目了然,不管資深資淺,能力深淺, ValidationAttribute 可以幫忙做掉的,大家肯定都會用一致的方式寫,自然也就好維護、好看多了。
  3. 降低重複的程式碼,提昇重用性
    Model 的驗證其實是內聚力相當高的行為,往往在不同地方使用的 model 相同時,它們要驗證的邏輯也會一致,這時候我們一份 model 設計好,其他地方就不需要在自行設計驗證邏輯,而可以重複使用(就跟 contract 一樣)。未來要修改,也才會一致。另外,也不需要因此 public 不必要的方法給物件外知道,外面知道的越少,我們就越符合最小知識原則,跟外部的耦合性就越小。
  4. 不只是型態、格式,還可以支援自訂商業邏輯
    實務上的需求,很多是需要自訂商業邏輯驗證,來決定是否存在著髒資料。甚至可以很簡單的避免,「request 給的資料長度大於 DB 允許的長度」的錯誤發生。越早拒絕髒資料,流程就會越乾淨,效能自然也會越好,因為沒跑到多餘的程式碼。
  5. 對升級到新的 .NET framework 或平台更加 friendly
    如前言所提,Web API, ASP.NET MVC 以及 ASP.NET Webform 4.5 都已經支援 model-binding 了,也有內建的 model validation 機制,使用的也是同一套 DataAnnotation 機制。如果現在我們的驗證就透過這樣的方式,那現在設計的 model ,未來在其他更新的平台上,自然就可以重複使用,而不必打掉重做了。
  6. 推廣強型別
    這雖是微不足道的效益,卻是相當重要的趨勢。在實務上因為弱型別導致的線上問題實在太多太多了,一般的弱型別也不容易享受到物件導向的好處,開發的速度與正確性也不如強型別來的好。希望透過這樣好用的機制,可以讓大家更往強型別之路邁進。

接下來,我會朝向的目標應該是把之前透過 T4 template 長出 mapping table 的 model ,自動加上一些 ValidationAttribute ,至少字串類型的可以加上 StringLength 先卡第一關。

希望我這小小的 project, 可以給大家帶來一些方便。


blog 與課程更新內容,請前往新站位置:http://tdd.best/