EF6数据验证

[删除(380066935@qq.com或微信通知)]

更好的阅读体验请查看原文:https://docs.microsoft.com/zh-cn/ef/ef6/saving/validation

注意

仅限 EF4.1 及更高版本 - 此页面中讨论的功能、API 等已引入实体框架 4.1。 如果使用的是早期版本,则部分或全部信息不适用

此页面上的内容改编自最初由 Julie Lerman (https://thedatafarm.com) 撰写的一篇文章。

Entity Framework 提供了各种各样的验证功能,这些功能可以馈送到用户界面进行客户端验证或用于服务器端验证。 首先使用代码时,可使用注释或 Fluent API 配置指定验证。 可在代码中指定其他更复杂的验证,无论模型是来自 Code First、Model First 还是 Database First,这些验证都将起作用。

模型

我将使用一对简单的类来演示验证:Blog 和 Post。

public class Blog
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string BloggerName { get; set; }
    public DateTime DateCreated { get; set; }
    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public DateTime DateCreated { get; set; }
    public string Content { get; set; }
    public int BlogId { get; set; }
    public ICollection<Comment> Comments { get; set; }
}

数据注释

Code First 使用来自 System.ComponentModel.DataAnnotations 程序集的注释作为配置 Code First 类的一种方法。 这些注释包括那些提供规则的注释,例如 RequiredMaxLengthMinLength。 许多 .NET 客户端应用程序也可识别这些注释,例如 ASP.NET MVC。 可使用这些注释实现客户端和服务器端验证。 例如,可强制将“博客标题”属性作为必需属性。

[Required]
public string Title { get; set; }

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


在这个 Create 视图的回发方法中,Entity Framework 用于将新博客保存到数据库,但在应用程序到达该代码之前会触发 MVC 的客户端验证。

但是,客户端验证并非无懈可击。 用户可影响其浏览器的功能,或者更糟糕的是,黑客可能会使用一些技巧来避免 UI 验证。 但 Entity Framework 还会识别 Required 注释并对其进行验证。

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

<appSettings>
    <add key="ClientValidationEnabled"value="false"/>
    ...
</appSettings>

即使禁用了客户端验证,应用程序中也会出现相同的响应。 错误消息“标题字段是必需的”将像以前一样显示。 除了现在,它将是服务器端验证的结果。 Entity Framework 将对 Required 注释执行验证(甚至在它费心构建要发送到数据库的 INSERT 命令之前),并会将错误返回给 MVC 以显示此消息。

Fluent API

可使用 Code First 的 Fluent API 而不是注释来获得相同的客户端和服务器端验证。 我将使用 MaxLength 验证而不是使用 Required 来说明这一点。

Code First 从类构建模型时,Fluent API 配置将作为代码优先进行应用。 可通过替代 DbContext 类的 OnModelCreating 方法来注入配置。 下面是一个配置,指定 BloggerName 属性不能超过 10 个字符。

public class BlogContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
    public DbSet<Comment> Comments { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>().Property(p => p.BloggerName).HasMaxLength(10);
    }
}

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

下面是应用程序的 BlogController 类中的一些异常处理错误代码,当 Entity Framework 尝试使用超过 10 个字符的最大字符数保存博客时,该代码会捕获该验证错误。

[HttpPost]
public ActionResult Edit(int id, Blog blog)
{
    try
    {
        db.Entry(blog).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    catch (DbEntityValidationException ex)
    {
        var error = ex.EntityValidationErrors.First().ValidationErrors.First();
        this.ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
        return View();
    }
}

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

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

IValidatableObject

IValidatableObject 是位于 System.ComponentModel.DataAnnotations 中的接口。 虽然它不是 Entity Framework API 的一部分,但你仍可利用它在 Entity Framework 类中进行服务器端验证。 IValidatableObject 提供了 Entity Framework 将在 SaveChanges 期间调用的 Validate 方法,或者你可以随时自行调用来验证类。

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

在下面的示例中,Blog 类已扩展为实现 IValidatableObject,然后提供 TitleBloggerName 无法匹配的规则。

public class Blog : IValidatableObject
{
    public int Id { get; set; }

    [Required]
    public string Title { get; set; }

    public string BloggerName { get; set; }
    public DateTime DateCreated { get; set; }
    public virtual ICollection<Post> Posts { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Title == BloggerName)
        {
            yield return new ValidationResult(
                "Blog Title cannot match Blogger Name",
                new[] { nameof(Title), nameof(BloggerName) });
        }
    }
}

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

与 Fluent API 提供的验证不同,此验证结果将由视图识别,并且不需要之前用于将错误添加到 ModelState 的异常处理程序。 由于在 ValidationResult 中设置了这两个属性名称,因此 MVC HtmlHelpers 将显示这两个属性的错误消息。


DbContext.ValidateEntity

DbContext 具有名为 ValidateEntity 的可替代方法。 调用 SaveChanges 时,Entity Framework 将为其缓存中状态为 Unchanged 的每个实体调用此方法。 可直接在此处运用验证逻辑,甚至可使用此方法进行调用(例如,在上一部分中添加的 Blog.Validate 方法)。

下面是一个 ValidateEntity 替代的示例,它验证新 Post,以确保帖子标题尚未被使用。 它首先检查实体是否为帖子,以及其状态是否为“已添加”。 如果是这种情况,那么它会在数据库中查看是否已经存在具有相同标题的帖子。 如果已有帖子,则会创建一个新的 DbEntityValidationResult

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

protected override DbEntityValidationResult ValidateEntity (
    System.Data.Entity.Infrastructure.DbEntityEntry entityEntry,
    IDictionary<object, object> items)
{
    var result = new DbEntityValidationResult(entityEntry, new List<DbValidationError>());

    if (entityEntry.Entity is Post post && entityEntry.State == EntityState.Added)
    {
        // Check for uniqueness of post title
        if (Posts.Where(p => p.Title == post.Title).Any())
        {
            result.ValidationErrors.Add(
                    new System.Data.Entity.Validation.DbValidationError(
                        nameof(Title),
                        "Post title must be unique."));
        }
    }

    if (result.ValidationErrors.Count > 0)
    {
        return result;
    }
    else
    {
        return base.ValidateEntity(entityEntry, items);
    }
}

显式触发验证

SaveChanges 的调用将触发本文中介绍的所有验证。 但无需依赖 SaveChanges。 你可能更喜欢在应用程序中的其他位置进行验证。

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

以下代码将调用 DbContext 当前实例上的 GetValidationErrors。 按实体类型将 ValidationErrors 分组到 DbEntityValidationResult。 代码首先循环访问方法返回的 DbEntityValidationResult,然后循环访问内部的每个 DbValidationError

foreach (var validationResult in db.GetValidationErrors())
{
    foreach (var error in validationResult.ValidationErrors)
    {
        Debug.WriteLine(
            "Entity Property: {0}, Error {1}",
            error.PropertyName,
            error.ErrorMessage);
    }
}

使用验证时的其他注意事项

以下是使用 Entity Framework 验证时要考虑的其他几点:

  • 验证期间禁用延迟加载
  • EF 将验证非映射属性(未映射到数据库中列的属性)上的数据注释
  • 验证是在 SaveChanges 期间检测到更改后执行的。 如果在验证期间进行了更改,则你有责任通知更改跟踪器
  • 如果在验证期间发生错误,则会引发 DbUnexpectedValidationException
  • Entity Framework 包含在模型中的 facet(最大长度、必需等) 将导致验证,即使类中没有数据注释和/或使用 EF Designer 创建模型
  • 优先规则:
    • Fluent API 调用替代相应的数据注释
  • 执行顺序:
    • 属性验证在类型验证之前进行
    • 仅当属性验证成功时,才会进行类型验证
  • 如果属性很复杂,则其验证还包括:
    • 复杂类型属性的属性级验证
    • 复杂类型的类型级验证,包括对复杂类型的 IValidatableObject 验证

摘要

Entity Framework 中的验证 API 与 MVC 中的客户端验证配合得非常好,但不必依赖客户端验证。 Entity Framework 将在服务器端处理使用 Code First Fluent API 应用的 DataAnnotations 或配置的验证。

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