注释
仅 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 注释作为配置代码第一类的一种方法。 在这些批注中,有提供规则的批注,如Required和MaxLengthMinLength。 许多 .NET 客户端应用程序还识别这些注释,例如 ASP.NET MVC。 可以使用这些注释实现客户端和服务器端验证。 例如,可以强制 Blog Title 属性成为必需属性。
[Required]
public string Title { get; set; }
由于应用程序中没有其他代码或标记更改,现有的 MVC 应用程序将执行客户端验证,甚至使用属性和批注名称动态生成消息。
在此“创建”视图的回发方法中,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 时调用该方法,或者您可以在任何需要验证类的时候自行调用。
配置,如 Required 和 MaxLength,执行对单个字段的验证。 在 Validate 方法中,你可以实现更复杂的逻辑,例如比较两个字段。
在以下示例中,该 Blog 类已扩展为实现 IValidatableObject ,然后提供一个规则,即 Title 和 BloggerName 不匹配。
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。 由于此验证同时检查 Title 和 BloggerName,因此将返回这两个属性名称。
与 Fluent API 提供的验证不同,当前验证结果将被视图识别,且我之前用于在 ModelState 中添加错误的异常处理器是不必要的。 由于我在其中 ValidationResult设置了这两个属性名称,因此 MVC HtmlHelpers 将显示这两个属性的错误消息。
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 的当前实例上调用 DbContext。
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 - 实体框架在模型中包括的特性(最大长度、必需等)将触发验证,即使在您的类中没有数据注释和/或您使用 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 工作流来描述您的概念模型。