之前的Code First系列文章已經演示了如何使用Fluent API和Data Annotation的方式配置實體的屬性,比如配置Destination類的Name屬性長度不大於50等。本文介紹EF里更強大的Validation API達到實體屬性驗證的效果。主要是通過ValidationAttributes屬性和IValidatebleObject接口來進行的驗證。
一、實體屬性的簡單驗證(GetValidationResult方法)
修改person類LastName屬性不超過10個字符:
[MaxLength(10)] public string LastName { get; set; }
看看程序中如何使用:
/// <summary> /// 驗證單個實體的屬性:GetValidationResult().IsValid方法 /// </summary> private static void ValidateNewPerson() { var person = new DbContexts.Model.Person { FirstName = "Julie", LastName = "Lerman", Photo = new DbContexts.Model.PersonPhoto { Photo = new Byte[] { 0 } } }; using (var context = new DbContexts.DataAccess.BreakAwayContext()) { if (context.Entry(person).GetValidationResult().IsValid) Console.WriteLine("Person is Valid"); else Console.WriteLine("Person is Invalid"); } }
顯然,控制台打印出來的是:Person is Valid,因為插入的LastName才6個字符,不到標注的最大10個長度。
注:因為修改了實體,故必須重新生成下數據庫,否則報錯。
上面的方法通過GetValidationResult驗證的實體。當然,GetValidationResult不僅驗證實體屬性的最大長度,同時驗證任何標注ValidationAttribute的實體屬性:
- DataTypeAttribute [DataType(DataType enum)] -> 實體類型驗證
- RangeAttribute [Range (low value, high value, error message string)] -> 范圍驗證
- RegularExpressionAttribute [RegularExpression(@”expression”)] -> 正則表達式驗證
- RequiredAttribute [Required] -> 非空驗證
- StringLengthAttribute [StringLength(max length value,MinimumLength=min length value)] -> 最大程度驗證
- CustomValidationAttribute -> 自定義驗證
GetValidationResult方法返回的是一個ValidationResult類型,ValidationResult類型不僅包括IsValid屬性,還包括其他一個很重要的屬性:ValidationErrors。修改下LastName上的Data Annotation標注:
[MaxLength(10, ErrorMessage= "Dude! Last name is too long! 10 is max.")] public string LastName { get; set; }
再修改下方法:
var result = context.Entry(person).GetValidationResult(); if (!result.IsValid) { Console.WriteLine(result.ValidationErrors.First().ErrorMessage); }
方法分析:如果驗證不通過,就打印出錯誤信息。這個錯誤信息是上面自定義錯誤信息:Dude! Last name is too long! 10 is max.
方法里的ValidationErrors方法后點了個First方法,意為獲取第一個錯誤,因為ValidationErrors是一個集合類型,記錄實體的所有驗證錯誤。看圖:
更簡單的方法就是可以直接遍歷:
/// <summary> /// 通用的打印錯誤方法 /// </summary> private static void ConsoleValidationResults(object entity) { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { var result = context.Entry(entity).GetValidationResult(); foreach (DbValidationError error in result.ValidationErrors) { Console.WriteLine(error.ErrorMessage); } } }
注:需要應用命名空間:System.Data.Entity.Validation
二、定制驗證規則(CustomValidationAttributes)
上面的方法只是簡單的實體屬性驗證,真實項目中的實體驗證肯定是多種多樣復雜多變的,來看看如何定制實體的驗證規則達到更強大的驗證功能。
/// <summary> /// 自定義驗證類BusinessValidations /// </summary> public static class BusinessValidations { /// <summary> /// 驗證description不包括!:) :( 等符號 /// </summary> public static ValidationResult DescriptionRules(string value) { var errors = new System.Text.StringBuilder(); if (value != null) { var description = value as string; if (description.Contains("!")) { errors.AppendLine("Description should not contain '!'."); } if (description.Contains(":)") || description.Contains(":(")) { errors.AppendLine("Description should not contain emoticons."); } } if (errors.Length > 0) return new ValidationResult(errors.ToString()); else return ValidationResult.Success; } }
在Destination類的Description屬性上應用這個驗證:
[MaxLength(500)] [CustomValidation(typeof(BusinessValidations), "DescriptionRules")] public string Description { get; set; }
上測試方法:
/// <summary> /// 定制驗證規則測試方法 /// </summary> public static void ValidateDestination() { ConsoleValidationResults(new DbContexts.Model.Destination { Name = "New York City", Country = "U.S.A", Description = "Big city! :) " }); }
打印結果:
Description should not contain '!'.
Description should not contain emoticons.
單獨驗證實體的屬性:
上一篇文章演示了如何使用DbEntityEntry操作實體的單個屬性,例:context.Entry(trip).Property(t => t.Description); 返回的就是Trip類的Description屬性,繼續看方法:
/// <summary> /// 單獨驗證實體的屬性:GetValidationErrors方法 /// </summary> private static void ValidatePropertyOnDemand() { var trip = new DbContexts.Model.Trip { EndDate = DateTime.Now, StartDate = DateTime.Now, CostUSD = 500.00M, Description = "Hope you won't be freezing :)" }; using (var context = new DbContexts.DataAccess.BreakAwayContext()) { var errors = context.Entry(trip).Property(t => t.Description).GetValidationErrors(); Console.WriteLine("# Errors from Description validation: {0}", errors.Count()); } }
打印結果:
# Errors from Description validation: 1
注:Trip類的Description也需要標注定制的驗證規則:
[CustomValidation(typeof(BusinessValidations), "DescriptionRules")] public string Description { get; set; }
三、使用IValidatebleObject接口驗證
除了定制驗證規則,還可以利用IValidatebleObject接口進行實體的驗證。實戰:添加Trip類的StartDate必須在EndDate之前,先看代碼:
/// <summary> /// 旅行類 /// </summary> public class Trip : IValidatableObject { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public Guid Identifier { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } [CustomValidation(typeof(BusinessValidations), "DescriptionRules")] public string Description { get; set; } public decimal CostUSD { get; set; } [Timestamp] public byte[] RowVersion { get; set; } public int DestinationId { get; set; } [Required] public Destination Destination { get; set; } public List<Activity> Activities { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { if (StartDate.Date >= EndDate.Date) yield return new ValidationResult("Start Date must be earlier than End Date", new[] { "StartDate", "EndDate" }); } }
方法分析:讓Trip類實現IValidatableObject接口並重寫接口里的驗證方法Validate。方法里對比了兩個時間,返回對比結果ValidationResult。當然Validate方法里可以添加更多驗證。
測試方法:
/// <summary> /// 驗證實體單個屬性:IValidatableObject接口的Validate方法 /// </summary> private static void ValidateTrip() { ConsoleValidationResults(new DbContexts.Model.Trip { EndDate = DateTime.Now, StartDate = DateTime.Now.AddDays(2), //開始時間比結束時間晚2天 CostUSD = 500.00M, Destination = new DbContexts.Model.Destination { Name = "Somewhere Fun" } }); }
開始時間比結束時間晚2天,很明顯不符合驗證規則。跑下程序會輸出兩次:Start Datemust be earlier than End Date. 因為同時驗證了StartDate和EndDate。
注:使用IValidatableObject接口所有Mode、DataAccess和BreakAwayConsole都需要添加引用:System.ComponentModel.DataAnnotations
試着向Validate方法里添加過濾關鍵字的驗證,現在Validate方法里已經有兩個驗證了:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { //驗證結束時間必須大於開始時間 if (StartDate.Date >= EndDate.Date) yield return new ValidationResult("Start Date must be earlier than End Date", new[] { "StartDate", "EndDate" }); //過濾關鍵字驗證 var unwantedWords = new List<string> { "sad", "worry", "freezing", "cold" }; var badwords = unwantedWords.Where(word => Description.Contains(word)); if (badwords.Any()) yield return new ValidationResult("Description has bad words: " + string.Join(";", badwords), new[] { "Description" }); }
注:需要引用system.Linq
修改測試方法:
/// <summary> /// 驗證實體單個屬性:IValidatableObject接口的Validate方法 /// </summary> private static void ValidateTrip() { ConsoleValidationResults(new DbContexts.Model.Trip { EndDate = DateTime.Now, StartDate = DateTime.Now.AddDays(2), //開始時間比結束時間晚2天 CostUSD = 500.00M, Description = "Don't worry about freezing on this trip", //待過濾的關鍵字 Destination = new DbContexts.Model.Destination { Name = "Somewhere Fun" } }); }
加了一個Description屬性,看看輸出:
Start Date must be earlier than End Date
Start Date must be earlier than End Date
Description has bad words: worry;freezing
CustomValidationAttributes不僅可以驗證實體的單個屬性,同樣可以驗證整個類,到Trip類里添加:
/// <summary> /// IValidatableObject接口驗證整個實體 /// </summary> public static ValidationResult TripDateValidator(Trip trip, ValidationContext validationContext) { if (trip.StartDate.Date >= trip.EndDate.Date) { return new ValidationResult("Start Date must be earlier than End Date", new[] { "StartDate", "EndDate" }); } return ValidationResult.Success; } /// <summary> /// IValidatableObject接口驗證整個實體 /// </summary> public static ValidationResult TripCostInDescriptionValidator(Trip trip, ValidationContext validationContext) { if (trip.CostUSD > 0) { if (trip.Description.Contains(Convert.ToInt32(trip.CostUSD).ToString())) { return new ValidationResult("Description cannot contain trip cost", new[] { "Description" }); } } return ValidationResult.Success; }
方法必須是pubic、static。分別驗證了開始日期必須小於結束日期、旅行的簡介不能包含旅行的花費。將兩個驗證方法應用到Trip類上:
[CustomValidation(typeof(Trip), "TripDateValidator")] [CustomValidation(typeof(Trip), "TripCostInDescriptionValidator")] public class Trip : IValidatableObject
再跑下程序就可以看到驗證效果了。
疑問:什么時候定制驗證規則,什么時候使用IValidatableObject接口驗證呢?
定制驗證規則一般是單獨開一個類寫驗證規則,然后以標注的形式標注到實體類的屬性上達到驗證效果。如果你的代碼是用Data Annotation的方式配置的,那么這個較好;
IValidatableObject接口驗證的驗證規則是寫在類里面的,不需要單獨寫新類比較方便也比較好管理,但是這些驗證規則只針對本類,無法實現重用。
四、驗證多個實體(GetValidationErrors)
前面已經演示了使用GetValidationResult驗證單個實體,同樣可以使用DbContext.GetValidationErrors強制上下文驗證那些被標記為添加(Added)和修改(Modified)的實體。整個驗證過程是這樣的:當程序運行的時候,上下文會循環所有被標記為添加和刪除的實體並調用DbContext.ValidateEntity方法,ValidateEntity會在目標實體上依次調用GetValidationResult方法,所有實體驗證后,GetValidationErrors會返回一個IEnumerable<DbEntityValidationResult>集合,這個集合里的的實體都是DbEntityValidationResult類型的,就是每個不通過驗證的實體。ok,來看個方法:
/// <summary> /// 驗證多個實體 /// </summary> private static void ValidateEverything() { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { var station = new DbContexts.Model.Destination { Name = "Antartica Research Station", Country = "Antartica", Description = "You will be freezing!" //這個不通過驗證:Description不能包括“!” }; context.Destinations.Add(station); //添加實體 context.Trip.Add(new DbContexts.Model.Trip //添加實體 { EndDate = new DateTime(2012, 4, 7), StartDate = new DateTime(2012, 4, 1), CostUSD = 500.00M, Description = "A valid trip.", Destination = station }); context.Trip.Add(new DbContexts.Model.Trip //添加實體 { EndDate = new DateTime(2012, 4, 7), StartDate = new DateTime(2012, 4, 15), //這個不通過驗證:開始日期大於結束日期 CostUSD = 500.00M, Description = "There were sad deaths last time.", Destination = station }); var dbTrip = context.Trip.First(); dbTrip.Destination = station; dbTrip.Description = "don't worry, this one's from the database"; //修改實體(這個不通過驗證:worry) DisplayErrors(context.GetValidationErrors()); } } private static void DisplayErrors(IEnumerable<DbEntityValidationResult> results) { int counter = 0; foreach (DbEntityValidationResult result in results) { counter++; Console.WriteLine("Failed Object #{0}: Type is {1}", counter, result.Entry.Entity.GetType().Name); Console.WriteLine(" Number of Problems: {0}", result.ValidationErrors.Count); foreach (DbValidationError error in result.ValidationErrors) { Console.WriteLine(" - {0}", error.ErrorMessage); } } }
方法分析:上面的方法有兩個新添加的Trip、一個新添加的Destination、一個修改的Trip。這些被上下文標記添加(Added)、修改(Modified)的實體都會被驗證。方法的思路是:通過調用上下文的GetValidationErrors方法獲取所有驗證不通過的實體,GetValidationErrors方法返回一個IEnumerable<DbEntityValidationResult>的集合類型,這個集合就是所有不通過驗證實體的集合,驗證錯誤的實體是DbEntityValidationResult類型,遍歷就可以輸出之前定義的驗證規則里的錯誤信息了。輸出結果跟預期的是一致的:
Failed Object #1: Type is Destination
Number of Problems: 1
- Description should not contain '!'.
Failed Object #2: Type is Trip
Number of Problems: 5
- Start Date must be earlier than End Date
- Start Date must be earlier than End Date
- Start Date must be earlier than End Date
- Start Date must be earlier than End Date
- Description has bad words: sad
Failed Object #3: Type is Trip
Number of Problems: 1
- Description has bad words: worry
注:上面演示的是通過調用GetValidationResults方法然后遍歷輸出才知道哪些實體不通過驗證的,且並沒有調用上下文的SaveChanges方法。其實不調用GetValidationResults方法驗證實體,直接調用SaveChanges方法也會調用GetValidationResults方法,這是EF的內部實現,由興趣的同學可以看看EF的源碼。試着用SaveChanges方法修改下:
//DisplayErrors(context.GetValidationErrors()); //使用SaveChanges代替GetValidationErrors方法驗證實體 try { context.SaveChanges(); Console.WriteLine("Save Succeeded."); } catch (DbEntityValidationException ex) { Console.WriteLine("Validation failed for {0} objects", ex.EntityValidationErrors.Count()); }
再運行下程序會捕捉到一個DbEntityValidationException的異常:
Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.
同樣也捕獲了幾個實體的驗證錯誤信息,可見SaveChanges方法執行保存之前是調用了GetValidationResults驗證實體的方法的。當然,同樣可以禁用驗證實體的方法,只需要在上下文的構造函數里加上一句:
Configuration.ValidateOnSaveEnabled = false; //調用SaveChanges方法的時候不驗證實體
再運行上面的方法會打印出:Save Succeeded. 禁止驗證也超級有用,后續章節會陸續講解。
五、本文源碼和系列文章導航
ok,本文結束,感謝閱讀。如果覺得本文還可以,希望不嗇點下【推薦】,謝謝!本文源碼
EF DbContext 系列文章導航: