数据验证Data Validation

备注

仅限 ef 4.1 -在实体框架4.1 中引入了本页中所述的功能、api 等。 如果使用的是早期版本,则不会应用部分或全部信息

此页面上的内容适用于最初由 Julie Lerman (https://thedatafarm.com)编写的文章。

实体框架提供了一种非常丰富的验证功能,这些功能可用于向用户界面提供客户端验证或用于服务器端验证。 使用 code first 时,可以使用批注或 Fluent API 配置来指定验证。 可以在代码中指定其他验证和更复杂的验证,无论模型是从代码 hails、模型优先还是数据库优先进行操作,都可以使用。

模型The model

我将使用一对简单的类来演示验证:博客和文章。

  1. public class Blog
  2. {
  3. public int Id { get; set; }
  4. public string Title { get; set; }
  5. public string BloggerName { get; set; }
  6. public DateTime DateCreated { get; set; }
  7. public virtual ICollection<Post> Posts { get; set; }
  8. }
  9. public class Post
  10. {
  11. public int Id { get; set; }
  12. public string Title { get; set; }
  13. public DateTime DateCreated { get; set; }
  14. public string Content { get; set; }
  15. public int BlogId { get; set; }
  16. public ICollection<Comment> Comments { get; set; }
  17. }

数据注释Data Annotations

Code First 使用 System.ComponentModel.DataAnnotations 程序集中的批注作为配置代码优先类的一种方法。 这些批注中包括 RequiredMaxLengthMinLength等规则。 许多 .NET 客户端应用程序还识别这些批注,例如,ASP.NET MVC。 您可以通过这些批注实现客户端和服务器端验证。 例如,你可以强制 “博客 Title” 属性为必需属性。

  1. [Required]
  2. public string Title { get; set; }

如果应用程序中没有额外的代码或标记更改,则现有 MVC 应用程序会执行客户端验证,甚至使用属性和批注名称动态生成消息。

图 1

在此 “创建” 视图的 “回发” 方法中,实体框架用于将新的博客保存到数据库中,但在应用程序到达该代码之前触发了 MVC 的客户端验证。

不过,客户端验证不是项目符号。 用户可能会影响浏览器的功能或更糟的功能,黑客可能会使用某些 trickery 来避免 UI 验证。 但实体框架还将识别 Required 注释并对其进行验证。

对此进行测试的一种简单方法是禁用 MVC 的客户端验证功能。 可以在 MVC 应用程序的 web.config 文件中执行此操作。 AppSettings 节具有 ClientValidationEnabled 的键。 将此项设置为 false 将阻止 UI 执行验证。

  1. <appSettings>
  2. <add key="ClientValidationEnabled"value="false"/>
  3. ...
  4. </appSettings>

即使已禁用客户端验证,也会在应用程序中获得相同的响应。 错误消息 “需要标题字段” 将像以前一样显示。 除非现在是服务器端验证的结果。 实体框架将在 Required 注释上执行验证(甚至在麻烦之前生成用于发送到数据库的 INSERT 命令),并将错误返回到将显示消息的 MVC。

Fluent APIFluent API

您可以使用 code first 的 Fluent API 而不是使用批注来获取相同的客户端 & 服务器端验证。 我将使用 MaxLength 验证来说明这一点,而不是使用 Required

当代码优先从类生成模型时,将应用 “熟知 API 配置”。 可以通过重写 DbContext 类的 OnModelCreating 方法注入配置。 以下配置指定 BloggerName 属性的长度不能超过10个字符。

  1. public class BlogContext : DbContext
  2. {
  3. public DbSet<Blog> Blogs { get; set; }
  4. public DbSet<Post> Posts { get; set; }
  5. public DbSet<Comment> Comments { get; set; }
  6. protected override void OnModelCreating(DbModelBuilder modelBuilder)
  7. {
  8. modelBuilder.Entity<Blog>().Property(p => p.BloggerName).HasMaxLength(10);
  9. }
  10. }

基于流畅 API 配置引发的验证错误不会自动到达 UI,但你可以在代码中捕获该错误,然后相应地对其进行响应。

下面是应用程序的 BlogController 类中的一些异常处理错误代码,该代码在实体框架尝试使用超过10个字符的 BloggerName 保存博客时捕获验证错误。

  1. [HttpPost]
  2. public ActionResult Edit(int id, Blog blog)
  3. {
  4. try
  5. {
  6. db.Entry(blog).State = EntityState.Modified;
  7. db.SaveChanges();
  8. return RedirectToAction("Index");
  9. }
  10. catch (DbEntityValidationException ex)
  11. {
  12. var error = ex.EntityValidationErrors.First().ValidationErrors.First();
  13. this.ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
  14. return View();
  15. }
  16. }

验证不会自动传递回视图,这就是使用 ModelState.AddModelError 的其他代码的原因。 这可以确保错误详细信息使其成为视图,然后将使用 ValidationMessageFor Htmlhelper 显示错误。

  1. @Html.ValidationMessageFor(model => model.BloggerName)

IValidatableObjectIValidatableObject

IValidatableObjectSystem.ComponentModel.DataAnnotations的接口。 尽管它不是实体框架 API 的一部分,但仍可在实体框架类中利用它进行服务器端验证。 IValidatableObject 提供实体框架将在 SaveChanges 期间调用的 Validate 方法,你可以随时调用以验证类。

RequiredMaxLength 等配置对单个字段执行验证。 在 Validate 方法中,可以使用更复杂的逻辑,例如,比较两个字段。

在下面的示例中,扩展了 Blog 类以实现 IValidatableObject,然后提供 TitleBloggerName 不能匹配的规则。

  1. public class Blog : IValidatableObject
  2. {
  3. public int Id { get; set; }
  4. [Required]
  5. public string Title { get; set; }
  6. public string BloggerName { get; set; }
  7. public DateTime DateCreated { get; set; }
  8. public virtual ICollection<Post> Posts { get; set; }
  9. public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
  10. {
  11. if (Title == BloggerName)
  12. {
  13. yield return new ValidationResult(
  14. "Blog Title cannot match Blogger Name",
  15. new[] { nameof(Title), nameof(BloggerName) });
  16. }
  17. }
  18. }

ValidationResult 构造函数采用一个表示错误消息的 string,以及一个 string的数组,该数组表示与验证关联的成员名称。 由于此验证同时检查 TitleBloggerName,因此将返回这两个属性名。

与 “流畅 API” 提供的验证不同,此验证结果将被视图识别,并且之前用于将错误添加到 ModelState 中的异常处理程序不是必需的。 因为我在 ValidationResult中设置了两个属性名,所以 MVC HtmlHelpers 将显示这两个属性的错误消息。

图2

DbContext.ValidateEntityDbContext.ValidateEntity

DbContext 具有称为 ValidateEntity的可重写方法。 调用 SaveChanges时,实体框架将为其缓存中其状态为 “不 Unchanged“ 的每个实体调用此方法。 您可以在此处直接放置验证逻辑,甚至可以使用此方法来调用,例如,在上一节中添加的 Blog.Validate 方法。

下面是一个用于验证新的 PostValidateEntity 重写示例,以确保尚未使用帖子标题。 它首先检查实体是否为 post 并添加其状态。 如果是这种情况,则会在数据库中查找具有相同标题的帖子。 如果已存在现有的公告,则会创建一个新的 DbEntityValidationResult

DbEntityValidationResult 承载单个实体的 DbEntityEntryICollection<DbValidationErrors>。 此方法开始时,实例化 DbEntityValidationResult,然后将发现的任何错误添加到其 ValidationErrors 集合。

  1. protected override DbEntityValidationResult ValidateEntity (
  2. System.Data.Entity.Infrastructure.DbEntityEntry entityEntry,
  3. IDictionary<object, object> items)
  4. {
  5. var result = new DbEntityValidationResult(entityEntry, new List<DbValidationError>());
  6. if (entityEntry.Entity is Post post && entityEntry.State == EntityState.Added)
  7. {
  8. // Check for uniqueness of post title
  9. if (Posts.Where(p => p.Title == post.Title).Any())
  10. {
  11. result.ValidationErrors.Add(
  12. new System.Data.Entity.Validation.DbValidationError(
  13. nameof(Title),
  14. "Post title must be unique."));
  15. }
  16. }
  17. if (result.ValidationErrors.Count > 0)
  18. {
  19. return result;
  20. }
  21. else
  22. {
  23. return base.ValidateEntity(entityEntry, items);
  24. }
  25. }

显式触发验证Explicitly triggering validation

SaveChanges 的调用会触发本文中所述的所有验证。 但不需要依赖 SaveChanges。 你可能希望在应用程序中的其他地方进行验证。

DbContext.GetValidationErrors 将触发所有验证、批注定义的验证或流畅的 API、在 IValidatableObject 中创建的验证(例如 Blog.Validate)以及在 DbContext.ValidateEntity 方法中执行的验证。

下面的代码将对 DbContext的当前实例调用 GetValidationErrorsValidationErrors 按实体类型分组到 DbEntityValidationResult中。 此代码首先遍历方法返回的 DbEntityValidationResult,然后遍历其中的每个 DbValidationError

  1. foreach (var validationResult in db.GetValidationErrors())
  2. {
  3. foreach (var error in validationResult.ValidationErrors)
  4. {
  5. Debug.WriteLine(
  6. "Entity Property: {0}, Error {1}",
  7. error.PropertyName,
  8. error.ErrorMessage);
  9. }
  10. }

使用验证时的其他注意事项Other considerations when using validation

下面是在使用实体框架验证时要考虑的一些其他事项:

  • 在验证过程中禁用延迟加载
  • EF 将验证非映射属性(未映射到数据库中的列的属性)中的数据批注
  • SaveChanges期间检测到更改后,将执行验证。 如果在验证期间进行更改,则需要负责通知更改跟踪器
  • 如果在验证期间发生错误,则会引发 DbUnexpectedValidationException
  • 实体框架包括在模型中的方面(最大长度、必需等)将导致验证,即使类和/或使用 EF 设计器创建模型
  • 优先规则:
    • 流畅的 API 调用替代相应的数据批注
  • 执行顺序:
    • 属性验证发生在类型验证之前
    • 仅当属性验证成功时才发生类型验证
  • 如果某个属性是复杂的,则其验证也包括:
    • 针对复杂类型属性的属性级验证
    • 对复杂类型的类型级别验证,包括对复杂类型 IValidatableObject 验证

摘要Summary

实体框架中的验证 API 与 MVC 中的客户端验证非常完美,但你不必依赖于客户端验证。 实体框架将在服务器端对 DataAnnotations 或使用 code first 流畅 API 应用的配置进行验证。

你还看到了许多扩展点,用于自定义该行为,无论你使用 IValidatableObject 接口还是点击 DbContext.ValidateEntity 方法。 无论是使用 Code First、Model First 还是 Database First 工作流来描述概念模型,最后两种验证方法都可通过 DbContext来使用。