EF里如何定制实体的验证规则和实现IObjectWithState接口进行验证以及多个实体的同时验证
之前的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 系列文章导航: