EF里如何定制实体的验证规则和实现IObjectWithState接口进行验证以及多个实体的同时验证

原文: 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虚方法实现可控的上下文验证和自定义验证  本章源码

你可能感兴趣的:(object)