重写ValidateEntity虚方法实现可控的上下文验证和自定义验证
上篇文章介绍了ValidationAttribute和IValidatableObject.Validate验证,但是这种验证还是稍微简单了,对于复杂的实体,例如:继承过来的实体、实现某接口的实体等等,简单的验证就无能为力了。这里重写ValidateEntity方法可以实现更为复杂的验证。
ValidateEntity本身是虚方法(virtual),故可以重写此方法加上自己的验证逻辑。在引入:System.Data.Entity.Infrastructure、System.Data.Entity.Validation、System.Collections.Generic三个命名空间的前提下,直接在BreakAwayContext类里输入protected override,vs就会智能提示出所有可以重写的需方法,ValidateEntity就是其中:
/// <summary> /// 重写ValidateEntity方法实现验证 /// </summary> protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items) { return base.ValidateEntity(entityEntry, items); }
方法分析:简单说就是传递了几个参数,然后调用基类(base)的验证方法。这里未对参数进行任何操作直接返回,实际情况中是先操作传递过来的参数,然后返回,这样就达到了自定义验证的效果。
另外一个把验证逻辑写在上下文BreakAwayContext类里的好处就是可以根据上下文追踪到的其他实体来验证需要验证的实体,甚至可以验证数据库里的数据。这些都是ValidationAttribute和IValidatableObject.Validate方法做不到的。除非传递一个上下文对象的实例,但是这个明显不符合分层的思想。
ok,看下接下来要操作的两个实体:
/// <summary> /// 结账类 /// </summary> public class Payment { public Payment() { PaymentDate = DateTime.Now; } public int PaymentId { get; set; } //主键 public int ReservationId { get; set; } //预约id public DateTime PaymentDate { get; set; } //结账日期 public decimal Amount { get; set; } //金额 }
/// <summary> /// 预约类 /// </summary> public class Reservation { public Reservation() { Payments = new List<Payment>(); } public int ReservationId { get; set; } public DateTime DateTimeMade { get; set; } //预约时间 public Person Traveler { get; set; } //预约人 public Trip Trip { get; set; } //属于哪个旅行 public Nullable<DateTime> PaidInFull { get; set; } //已付全款 public List<Payment> Payments { get; set; } //一对多 }
很明显是一个一对多的关系,预约类对应多个结账类。从表结账类和主表预约类是通过ReservationId连接的。上验证方法:
/// <summary> /// 重写ValidateEntity方法实现验证(ValidateEntity验证会在定制的验证规则通过后执行) /// </summary> protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items) { var result = new DbEntityValidationResult(entityEntry, new List<DbValidationError>()); var reservation = entityEntry.Entity as DbContexts.Model.Reservation; if (reservation != null) { if (entityEntry.State == EntityState.Added && reservation.Payments.Count == 0) { result.ValidationErrors.Add(new DbValidationError("Reservation", "New reservation must have a payment.")); } } if (!result.IsValid) { return result; } return base.ValidateEntity(entityEntry, items); }
此方法的验证规则是:新添加的Reservation预约类实体必须得有从表Payment数据,否则添加验证错误到DbEntityValidationResult里,这样验证就不通过了。
当然,为了整洁和方便整理,也可以把Reservation的验证单独提取出来:
/// <summary> /// 重写ValidateEntity方法实现验证(ValidateEntity验证会在定制的验证规则通过后执行) /// </summary> protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items) { var result = new DbEntityValidationResult(entityEntry, new List<DbValidationError>()); ValidateReservation(result); if (!result.IsValid) { return result; } return base.ValidateEntity(entityEntry, items); //调用基类的验证方法 } /// <summary> /// 多个验证规则 /// </summary> private void ValidateReservation(DbEntityValidationResult result) { var reservation = result.Entry.Entity as DbContexts.Model.Reservation; if (reservation != null) { if (result.Entry.State == EntityState.Added && reservation.Payments.Count == 0) { result.ValidationErrors.Add(new DbValidationError("Reservation", "New reservation must have a payment.")); } } }
注:ValidateEntity临时的关闭了延迟加载,故上面的方法不会去数据库里查从表数据。
上面的验证是先进行自定义的规则验证,都通过了再进行上下文的验证。现在颠倒下顺序,先走上下文的验证,通过了再进行个性化的验证。这里的上下文验证是Data Annotation定义的属性最大长度、不为空等;自定义验证是每一个目的地类Destination下不能有同名的住宿类Lodging,这个也是符合逻辑的,如果连基本的Data Annotation验证都不通过也没必须再去数据库查询唯一不唯一了。上方法:
/// <summary> /// 先走上下文验证,再进行自定义验证 /// </summary> protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items) { var result = base.ValidateEntity(entityEntry, items); //先走上下文验证 if (result.IsValid) { ValidateLodging(result); //上下文验证通过再验证自定义验证 } return result; } /// <summary> /// 自定义验证:每一个目的地类Destination下不能有同名的住宿类Lodging /// </summary> /// <param name="result"></param> private void ValidateLodging(DbEntityValidationResult result) { var lodging = result.Entry.Entity as DbContexts.Model.Lodging; if (lodging != null && lodging.DestinationId != 0) { if (Lodgings.Any(l => l.Name == lodging.Name && l.DestinationId == lodging.DestinationId)) { result.ValidationErrors.Add(new DbValidationError("Lodging", "There is already a lodging named " + lodging.Name + " at this destination.")); } } }
来写个方法测试下这个验证:
/// <summary> /// 先验证上下文,再验证自定义的规则 /// </summary> private static void CreateDuplicateLodging() { using (var context = new DbContexts.DataAccess.BreakAwayContext()) { var destination = context.Destinations.FirstOrDefault(d => d.Name == "Grand Canyon"); try { context.Lodgings.Add(new DbContexts.Model.Lodging { Destination = destination, Name = "Grand Hotel" }); context.SaveChanges(); Console.WriteLine("Save Successful"); } catch (DbEntityValidationException ex) { Console.WriteLine("Save Failed: "); foreach (var error in ex.EntityValidationErrors) { Console.WriteLine(string.Join(Environment.NewLine, error.ValidationErrors.Select(v => v.ErrorMessage))); } return; } } }
跑下程序显然这条数据添加不进去,上下文的验证的确能通过,但是自定义的验证不能通过。因为Name为Grand Hotel的Lodging已经存在了。看看输出:
Save Failed:
There is already a lodging named Grand Hotel at this destination.
当然也可以让它连第一层验证都通过不了,在Lodging的MilesFromNearestAirport属性上加上区间验证:
[Range(.5, 150)] public decimal MilesFromNearestAirport { get; set; }
由于实体已经发生变化,必须先重新生成下数据库再跑上面的方法会输出:
Save Failed:
字段 MilesFromNearestAirport 必须在 0.5 和 150 之间。
第一层验证没通过也就不会去数据库里查是否有重复的记录了,这个效率比较高,不会发送不必要的sql到数据库。当然也可以两者同时验证:
/// <summary> ///同时验证 /// </summary> protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items) { var result = base.ValidateEntity(entityEntry, items); ValidateLodging(result); return result; }
输出:
Save Failed:
字段 MilesFromNearestAirport 必须在 0.5 和 150 之间。
There is already a lodging named Grand Hotel at this destination.
简单的示例结束,其实利用传递进来的参数可以做很多自定义和个性化的验证。明白其原理后项目中自己就可以随意发挥了。本章源码点这里。
DbContext系列文章到这就结束了,奉上所有文章的导航:
EF DbContext 系列文章导航: