資料驗證

備註

僅限 EF4.1 及更新 版本 - 在 Entity Framework 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 程式集的批註作為設定 Code First 類別的一種方法。 在這些批註中,有些提供了規則,例如 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 嘗試儲存含有超過 10 字元的 BloggerName 的部落格時擷取該驗證錯誤。

[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 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 如果在驗證期間發生錯誤,則會擲回
  • Entity Framework 包含在模型中的面向(長度上限、必要等)都會導致驗證,即便您的類別和/或您使用 EF 設計工具建立模型時沒有數據批注,也一樣。
  • 優先順序規則:
    • Fluent API 呼叫會覆寫對應的數據註解
  • 執行順序:
    • 在類型驗證之前發生屬性驗證
    • 只有在屬性驗證成功時,才會發生類型驗證
  • 如果屬性很複雜,其驗證也會包含:
    • 複雜類型屬性的屬性層級驗證
    • 複雜類型的型別層級驗證,包括對複雜類型的IValidatableObject驗證

總結

Entity Framework 中的驗證 API 與 MVC 中的用戶端驗證搭配運作非常好,但您不需要依賴客戶端驗證。 Entity Framework 會負責處理伺服器端的驗證,無論您使用的是 DataAnnotations 還是程式碼優先的 Fluent API 所應用的配置。

您也看到一些用於自定義行為的擴充點,無論您是使用 IValidatableObject 介面還是使用 DbContext.ValidateEntity 方法。 而最後兩種驗證方法可透過 DbContext取得,不論您使用 Code First、Model First 或 Database First 工作流程來描述概念模型。