Quantcast
Channel: ProgrammerXDB Blog - C#
Viewing all articles
Browse latest Browse all 73

Entity Framework Validation API - 2

$
0
0

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N170518302
出刊日期: 2017/5/17

本文延續《Entity Framework Validation API - 1》一文的說明,介紹Entity Framework驗證應用程式介面(Validation API)的基本應用。本文將探討類別階層驗證、驗證多個物件、攔截DbEntityValidationException例外錯誤與關閉驗證功能等議題。

 

類別階層驗證 – IValidatableObject介面

.NET Framework 4版新增一個IValidatableObject介面,提供類別階層的驗證能力。類別屬性資料若有相依關係,可以實作此介面來處理驗證邏輯。舉例來說,若撰寫一個訂返鄉火車票的功能,則去程日期必需小於回程日期,且額外又要要求回程日期必需要在去程日期的十天內。類似這種牽涉到多個屬性的資料檢查動作,就可以透過IValidatableObject介面來完成。

IValidatableObject介面包含一個Validate()方法,你可以在此方法加入自訂驗證邏輯。為了簡單起見,我們把前文提及的使用CustomValidationAttribute自訂驗證stor_name屬性的驗證程式碼,搬到類別階層,檢查指定的屬性值是否包含不合法的字串「admin」與「test」。參考以下程式碼範例,加入一個部分store類別,實作IValidatableObject介面的Validate()方法:

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class store : IValidatableObject
    {
        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            string errMsg = "";
            if (stor_name != null)
            {
                if (stor_name.Contains("admin") || stor_name.Contains("test"))
                {
                    errMsg = "名稱不可包含admin或test字串";
                    yield return new ValidationResult(errMsg, new[] { "stor_name" });
                }
            }
        }
    }
    public partial class store
    {
        public store()
        {
        }

        [Key]
        [StringLength(4, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_id { get; set; }

        [StringLength(40, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_name { get; set; }

        [StringLength(40)]
        public virtual string stor_address { get; set; }

        [StringLength(20)]
        public virtual string city { get; set; }

        [StringLength(2, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string state { get; set; }

        [StringLength(5, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string zip { get; set; }

        public virtual ICollection<sale> sales { get; set; }

        public virtual ICollection<discount> discounts { get; set; }
    }
}

 

使用以下程式碼進行測試,然後利用一個foreach迴圈印出驗證不成功的屬性名稱與錯誤訊息:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = new store();
                aStore.stor_id = "99999";
                aStore.stor_name = "9999 admin store 9999  ";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";

                DbEntityValidationResult result = context.Entry(aStore).GetValidationResult();
                if (!result.IsValid)
                {
                    foreach (DbValidationError item in result.ValidationErrors)
                    {
                        Console.WriteLine($" {item.PropertyName} - {item.ErrorMessage}");
                    }
                }
            }
        }
    }
}

 

此範例執行結果如下所示,只有顯示出執行Attribute Validation驗證的錯誤訊息,並沒有觸發IValidatableObject的Validate()方法之驗證邏輯,:

stor_id - stor_id 長度不可超過 4

這是因為IValidatableObject只有在Attribute驗證通過之後,才會觸發驗證邏輯。讓我們修改測試程式碼如下,讓stor_id的長度不超過「4」,先通過資料驗證:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = new store();
                aStore.stor_id = "9999";
                aStore.stor_name = "9999 admin store 9999  ";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";

                DbEntityValidationResult result = context.Entry(aStore).GetValidationResult();
                if (!result.IsValid)
                {
                    foreach (DbValidationError item in result.ValidationErrors)
                    {
                        Console.WriteLine($" {item.PropertyName} - {item.ErrorMessage}");
                    }
                }
            }
        }
    }
}

 

這次執行範例程式碼,範例執行結果如下所示:

stor_name - 名稱不可包含admin或test字串

 

類別階層驗證 - CustomValidationAttribute

除了IValidatableObject介面之外,類別階層驗證也可以透過CustomValidationAttribute來完成,讓我們修改store類別程式碼,讓它可以達到和上例IValidatableObject介面範例一樣的驗證功能。在store部分類別之中加入一個static方法 – TextValidationRule(),加入驗證邏輯,檢查指定的屬性值是否包含不合法的字串「admin」與「test」。因為一個ValidationAttribute只能回傳一個ValidationResult,若類別階層有多個驗證的條件,你可以撰寫多個方法針對不同規則來進行驗證。

最後只要在store類別上方套用CustomValidationAttribute傳入兩個參數,第一個參數指定驗證程式碼所在的類別之型別,本例為「typeof(store)」;第二個參數則是要叫用的方法名稱,本例為「TextValidationRule」方法,參考以下範例程式碼:

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;


    [CustomValidation(typeof(store), "TextValidationRule")]
    public partial class store {
        public static ValidationResult TextValidationRule(store store, ValidationContext validationContext)
        {
            string errMsg = "";
            if (store.stor_name != null)
            {
                if (store.stor_name.Contains("admin") || store.stor_name.Contains("test"))
                {
                    errMsg = "名稱不可包含admin或test字串";
                    return new ValidationResult(errMsg, new[] { "stor_name" });
                }
            }
            return ValidationResult.Success;
        }
    }
    public partial class store
    {
        public store()
        {
        }

        [Key]
        [StringLength(4, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_id { get; set; }

        [StringLength(40, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_name { get; set; }

        [StringLength(40)]
        public virtual string stor_address { get; set; }

        [StringLength(20)]
        public virtual string city { get; set; }

        [StringLength(2, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string state { get; set; }

        [StringLength(5, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string zip { get; set; }

        public virtual ICollection<sale> sales { get; set; }

        public virtual ICollection<discount> discounts { get; set; }
    }
}

 

使用以下程式碼進行測試,建立一個store物件,並且故意填入無效的「admin」字串到stor_name屬性中,然後使用GetValidationErrors()方法驗證stor_name屬性,然後利用一個foreach迴圈印出驗證不成功的屬性名稱與錯誤訊息:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var aStore = new store();
                aStore.stor_id = "9999";
                aStore.stor_name = "9999 admin store 9999  ";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";

                DbEntityValidationResult result = context.Entry(aStore).GetValidationResult();
                if (!result.IsValid)
                {
                    foreach (DbValidationError item in result.ValidationErrors)
                    {
                        Console.WriteLine($" {item.PropertyName} - {item.ErrorMessage}");
                    }
                }
            }
        }
    }
}

 

執行測試程式碼,此範例執行結果如下所示:

stor_name - 名稱不可包含admin或test字串

 

驗證多個物件

有時新增或修改資料會牽涉到多個物件,在將資料寫到資料庫之前,我們可以使用DbContext類別的GetValidationErrors()方法一次檢查多個物件的資料是否有效,預設DbContext類別的GetValidationErrors()方法會驗證狀態為Added與Modified的物件。

以目前模型為例,模型包含store和discount,其關係如下圖所示:

clip_image002

圖 3:store和discount的關係。

參考以下範例程式碼,目前store類別的程式碼定義如下:

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class store
    {
        public store()
        {
        }

        [Key]
        [StringLength(4, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_id { get; set; }

        [StringLength(40, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string stor_name { get; set; }

        [StringLength(40)]
        public virtual string stor_address { get; set; }

        [StringLength(20)]
        public virtual string city { get; set; }

        [StringLength(2, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string state { get; set; }

        [StringLength(5, ErrorMessage = "{0} 長度不可超過 {1}")]
        public virtual string zip { get; set; }

        public virtual ICollection<sale> sales { get; set; }

        public virtual ICollection<discount> discounts { get; set; }
    }
}

 

參考以下範例程式碼,目前Discount類別的程式碼定義如下:

namespace PubsDemo
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Spatial;

    public partial class discount
    {
        [Key]
        [Column(Order = 0)]
        [StringLength(40, ErrorMessage = "{0} 長度不可超過 {1}")]
        public  string discounttype { get; set; }

        [StringLength(4, ErrorMessage = "{0} 長度不可超過 {1}")]
        public  string stor_id { get; set; }

        public  short? lowqty { get; set; }

        public  short? highqty { get; set; }

        [Key]
        [Column("discount", Order = 1)]
        public virtual decimal discount1 { get; set; }

        public virtual store store { get; set; }
    }
}

 

參考以下程式碼範例加入測試程式,建立一個discount物件,將discount加入context.discounts屬性,故意填入長度超過「40」的字串到discounttype屬性;建立一個store物件,,將newDiscount加入store物件discounts屬性,故意讓stor_id屬性值的長度超過「4」個字。然後將aStore加入context.stores屬性,然後利用一個foreach迴圈印出驗證不成功的屬性名稱與錯誤訊息:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                discount newDiscount = new discount()
                {
                    discounttype = "Special Discount Special Discount Special Discount Special Discount",
                    discount1 = 0.3m
                };
                context.discounts.Add(newDiscount);
                var aStore = new store();
                aStore.stor_id = "99999";
                aStore.stor_name = "9999 store";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";
                aStore.discounts = new List<discount>() { newDiscount };

                context.stores.Add(aStore);

                foreach (var result in context.GetValidationErrors())
                {
                    Console.WriteLine(result.Entry.Entity.ToString());
                    foreach (DbValidationError error in result.ValidationErrors)
                    {
                        Console.WriteLine($" \t{error.PropertyName} - {error.ErrorMessage}");
                    }
                }

            }
        }
    }
}

 

此範例執行結果如下所示:

PubsDemo.store

stor_id - stor_id 長度不可超過 4

PubsDemo.discount

discounttype - discounttype 長度不可超過 40

 

攔截DbEntityValidationException例外錯誤

當你叫用DbContext類別的SaveChanges()方法試圖將新增或修改的資料寫回資料庫,Entity Framework會自動叫用GetValidationErrors()方法進行資料驗證。Entity Framework會驗證所有狀態為Added與Modified的實體。預設Entity Framework會驗證所有你套用在屬性上方的ValidationAttributes,以及叫用IValidatableObject的Validate()方法進行驗證,若發生驗證錯誤,將觸發DbEntityValidationException例外錯誤,並將錯誤放在EntityValidationErrors屬性中,其型別為IEnumerable<DbEntityValidationResult>。

參考以下程式碼範例,展示如何攔截DbEntityValidationException例外錯誤,範例先建立discount物件,故意讓newDiscount物件discounttype屬性值的長度超過40個字;接著建立store物件,故意讓stor_id屬性值的長度超過「4」個字:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                discount newDiscount = new discount()
                {
                    discounttype = "Special Discount Special Discount Special Discount Special Discount",
                    discount1 = 0.3m
                };
                context.discounts.Add(newDiscount);
                var aStore = new store();
                aStore.stor_id = "99999";
                aStore.stor_name = "9999 store";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";
                aStore.discounts = new List<discount>() { newDiscount };

                context.stores.Add(aStore);
               
                try
                {
                    context.SaveChanges();
                }
                catch (DbEntityValidationException ex)
                {
                    foreach (var result in ex.EntityValidationErrors)
                    {
                        Console.WriteLine(result.Entry.Entity.ToString());
                        foreach (DbValidationError error in result.ValidationErrors)
                        {
                            Console.WriteLine($" \t{error.PropertyName} - {error.ErrorMessage}");
                        }
                    }
                   
                }
            }
        }
    }
}

因為資料驗證不通過,因此context.SaveChanges()這行程式碼一執行,就會產生例外誤,這兩筆資料將不會寫到資料庫之中。我們利用try..catch語法攔截DbEntityValidationException,從EntityValidationErrors屬性取得IEnumerable<DbEntityValidationResult>,從DbEntityValidationResult的ValidationErrors屬性取得DbValidationError,然後透過foreach將所有驗證錯誤的屬性名稱與錯誤訊息一一印出。

此範例執行結果如下所示,

PubsDemo.discount

discounttype - discounttype 長度不可超過 40

PubsDemo.store

stor_id - stor_id 長度不可超過 4

 

關閉驗證功能

預設Entity Framework會在你叫用DbContext物件的SaveChanges()方法時,自動叫用GetValidationErrors()方法,進行資料驗證的動作。Entity Framework會驗證所有利用ValidationAttribute與IValidatableObject介面定義的規則。若驗證不通過,將觸發DbEntityValidationException例外錯誤,你可以從例外錯誤物件的EntityValidationErrors屬性取得驗證結果。

有時在進行大量資料轉換的動作時,若已經能夠確保資料都是有效的,那麼在叫用SaveChanges()方法之前,關閉驗證的動作可以加快程式的執行效能。我們可以在DbContext類別的建構函式關閉驗證功能,參考以下範例程式碼,將Configuration.ValidateOnSaveEnabled設定為「false」:

namespace PubsDemo
{
    using System;
    using System.Data.Entity;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Linq;
    using System.Data.Entity.Infrastructure;

    public partial class PubsContext : DbContext
    {
        public PubsContext()
            : base("name=PubsContext")
        {
            Configuration.ValidateOnSaveEnabled = false;
        }

        public virtual DbSet<author> authors { get; set; }
        public virtual DbSet<employee> employees { get; set; }
        public virtual DbSet<job> jobs { get; set; }
        public virtual DbSet<pub_info> pub_info { get; set; }
        public virtual DbSet<publisher> publishers { get; set; }
        public virtual DbSet<sale> sales { get; set; }
        public virtual DbSet<store> stores { get; set; }
        public virtual DbSet<titleauthor> titleauthors { get; set; }
        public virtual DbSet<title> titles { get; set; }
        public virtual DbSet<discount> discounts { get; set; }
        public virtual DbSet<roysched> royscheds { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {

           //以下略


           
        }
    }
}

 

修改上個範例進行測試,於try..catch中增加一個catch區塊,攔截通用的Exception錯誤:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                discount newDiscount = new discount()
                {
                    discounttype = "Special Discount Special Discount Special Discount Special Discount",
                    discount1 = 0.3m
                };
                context.discounts.Add(newDiscount);
                var aStore = new store();
                aStore.stor_id = "99999";
                aStore.stor_name = "9999 store";
                aStore.stor_address = "679 Carson St.";
                aStore.city = "Portland";
                aStore.state = "OR";
                aStore.zip = "89076";
                aStore.discounts = new List<discount>() { newDiscount };

                context.stores.Add(aStore);

                try
                {
                    context.SaveChanges();
                }
                catch (DbEntityValidationException ex)
                {
                    foreach (var result in ex.EntityValidationErrors)
                    {
                        Console.WriteLine(result.Entry.Entity.ToString());
                        foreach (DbValidationError error in result.ValidationErrors)
                        {
                            Console.WriteLine($" \t{error.PropertyName} - {error.ErrorMessage}");
                        }
                    }

                }
                catch (Exception ex)
                {

                    Console.WriteLine(ex.Message);
                }
            }
        }
    }
}

 

關閉驗證功能後,將不會觸發DbEntityValidationException例外錯誤,但因資料本身有問題,所以資料也無法寫到資料庫,此範例執行結果如下所示,將印出錯誤訊息:

An error occurred while updating the entries. See the inner exception for details.

另一種關閉驗證的方式是透過DbContext實體,參考以下範例程式碼,假設將上例建構函式Configuration.ValidateOnSaveEnabled這行程式碼註解:

   public partial class PubsContext : DbContext
    {
        public PubsContext()
            : base("name=PubsContext")
        {
            //Configuration.ValidateOnSaveEnabled = false;
        }
    //以下略
}

 

修改測試程式,設定ValidateOnSaveEnabled為「false」:

using (var context = new PubsContext())

{

context.Configuration.ValidateOnSaveEnabled = false;

//以下略

}

 

 

此範例執行結果同上例。


Viewing all articles
Browse latest Browse all 73

Latest Images

Trending Articles