通过


数据验证

注释

仅 EF4.1 及更高版本 - 实体框架 4.1 中介绍了本页中讨论的功能、API 等。 如果使用早期版本,某些或全部信息不适用

此页上的内容改编自朱莉·勒曼(https://thedatafarm.com)撰写的文章。

Entity Framework 提供了各种各样的验证功能,这些功能可以馈送至用户界面进行客户端验证,或用于服务器端验证。 首次使用代码时,可以使用批注或 Fluent API 配置指定验证。 可以在代码中指定额外验证以及更复杂的验证,无论您的模型是源于代码优先、模型优先还是数据库优先的方法,这些验证都可以正常工作。

模型

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

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 注释作为配置代码第一类的一种方法。 在这些批注中,有提供规则的批注,如RequiredMaxLengthMinLength。 许多 .NET 客户端应用程序还识别这些注释,例如 ASP.NET MVC。 可以使用这些注释实现客户端和服务器端验证。 例如,可以强制 Blog Title 属性成为必需属性。

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

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

图 1

在此“创建”视图的回发方法中,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 而不是注解来实现相同的客户端和服务器端验证。 我不会使用 Required,而是使用 MaxLength 验证来向你展示这一点。

在从类生成模型时,应用 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 尝试保存 BloggerName 超过 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 的接口。 虽然它不是实体框架 API 的一部分,但仍可以在 Entity Framework 类中利用它进行服务器端验证。 IValidatableObject 提供了一个Validate方法,Entity Framework 会在 SaveChanges 时调用该方法,或者您可以在任何需要验证类的时候自行调用。

配置,如 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 将显示这两个属性的错误消息。

图 2

DbContext.ValidateEntity

DbContext 具有一个可重写的方法,名为ValidateEntity。 调用 SaveChanges时,Entity Framework 将为其缓存中的每个实体调用此方法,其状态不是 Unchanged。 您可以直接在此处放置验证逻辑,或使用此方法来调用在上一部分中添加的 Blog.Validate 方法,例如。

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

DbEntityValidationResult 包含一 DbEntityEntry 和一 ICollection<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 方法中执行的验证。

以下代码将在 GetValidationErrors 的当前实例上调用 DbContextValidationErrors 按实体类型分组为 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
  • 实体框架在模型中包括的特性(最大长度、必需等)将触发验证,即使在您的类中没有数据注释和/或您使用 EF 设计器创建模型。
  • 优先规则:
    • Fluent API 调用替代相应的数据注释
  • 执行顺序:
    • 在类型验证之前发生属性验证
    • 仅当属性验证成功时,才会发生类型验证
  • 如果属性很复杂,则其验证也将包括:
    • 复杂类型属性的属性级别验证
    • 复杂类型上的类型级别验证,包括对复杂类型的IValidatableObject验证

概要

Entity Framework中的验证API与MVC中的客户端验证配合得很好,但不必仅依赖客户端验证。 Entity Framework 将负责在服务器端验证数据注解 (DataAnnotations) 或者您通过代码优先 (Code First) 的 Fluent API 应用的配置。

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