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虛方法實現可控的上下文驗證和自定義驗證  本章源碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM