.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,其關係如下圖所示:

圖 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;
//以下略
}
此範例執行結果同上例。