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 系列文章导航
  1. EF如何操作内存中的数据和加载外键数据:延迟加载、贪婪加载、显示加载  本章源码
  2. EF里单个实体的增查改删以及主从表关联数据的各种增删改查  本章源码
  3. 使用EF自带的EntityState枚举和自定义枚举实现单个和多个实体的增删改查  本章源码
  4. EF里查看/修改实体的当前值、原始值和数据库值以及重写SaveChanges方法记录实体状态  本章源码
  5. EF里如何定制实体的验证规则和实现IObjectWithState接口进行验证以及多个实体的同时验证  本章源码
  6. 重写ValidateEntity虚方法实现可控的上下文验证和自定义验证  本章源码
posted @ 2013-09-14 15:41  oppoic  阅读(2969)  评论(1编辑  收藏  举报