Поделиться через


Проверка данных

Замечание

EF4.1 и далее — функции, API и т. д., обсуждаемые на этой странице, были введены в Entity Framework 4.1. Если вы используете более раннюю версию, некоторые или все сведения не применяются.

Содержимое на этой странице адаптировано из статьи, первоначально написанной Джули Лерман (https://thedatafarm.com).

Entity Framework предоставляет множество функций проверки, которые могут передаваться в пользовательский интерфейс для проверки на стороне клиента или использоваться для проверки на стороне сервера. При использовании подхода 'code first' вы можете указать проверки с помощью аннотаций или конфигураций Fluent API. Дополнительные и более сложные проверки могут быть указаны в коде и будут работать независимо от того, создаете ли вы модель с использованием подхода "code first", "model first" или "database first".

Модель

Я продемонстрирую валидации с простой парой классов: Блог и Пост.

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". Среди этих заметок есть те, которые предоставляют такие правила, как Requiredи MaxLengthMinLength. Ряд клиентских приложений .NET также распознает эти заметки, например, ASP.NET MVC. С помощью этих заметок можно выполнить проверку на стороне клиента и сервера. Например, можно сделать название блога обязательным свойством.

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

При отсутствии дополнительных изменений кода или разметки в приложении существующее приложение MVC будет выполнять проверку на стороне клиента, даже динамически создавая сообщение с помощью имен свойств и заметок.

рисунок 1

В методе обратной передачи этого представления создания, Entity Framework используется для сохранения нового блога в базе данных, но проверка на стороне клиента MVC запускается до того, как приложение достигнет этого кода.

Проверка на стороне клиента не является безупречной. Пользователи могут повлиять на функции браузера или еще хуже, хакер может использовать некоторые трюки, чтобы избежать проверок пользовательского интерфейса. Но Entity Framework также распознает заметку Required и проверит ее.

Простой способ проверить это — отключить функцию проверки на стороне клиента MVC. Это можно сделать в web.config файле приложения MVC. В разделе appSettings есть ключ для ClientValidationEnabled. Если этот ключ задан как false, пользовательский интерфейс не будет выполнять проверки.

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

Даже если проверка на стороне клиента отключена, вы получите тот же ответ в приложении. Сообщение об ошибке "Поле заголовка является обязательным" будет отображаться как раньше. Кроме того, теперь это будет результатом проверки на стороне сервера. Entity Framework выполнит проверку аннотации Required (прежде чем создавать команду INSERT, чтобы отправить ее в базу данных) и возвратит ошибку в MVC, которая отобразит сообщение.

Fluent API

Вы можете использовать простой API кода вместо заметок, чтобы получить ту же проверку на стороне клиента и сервера. Вместо использования Required, я покажу вам это с помощью проверки MaxLength.

Конфигурации Fluent API применяются во время построения модели из классов с помощью подхода Code First. Конфигурации можно внедрить, переопределив метод OnModelCreating класса DbContext. Ниже приведена конфигурация, указывающая, что свойство BlogName не может превышать 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);
    }
}

Ошибки проверки, возникающие на основе конфигураций API Fluent, не будут автоматически достигать пользовательского интерфейса, но вы можете записать его в коде, а затем ответить на него соответствующим образом.

Ниже приведен код ошибки обработки исключений в классе 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. Хотя он не является частью API Entity Framework, вы по-прежнему можете использовать его для проверки на стороне сервера в классах 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 отображают сообщение об ошибке для каждого из этих свойств.

рис. 2

DbContext.ValidateEntity (DbContext.ValidateEntity)

DbContext имеет переопределимый метод ValidateEntity. При вызове SaveChanges платформа Entity Framework вызовет этот метод для каждой сущности в своем кэше, состояние которой не равно Unchanged. Логику проверки можно поместить непосредственно здесь или даже использовать этот метод для вызова, например метода, добавленного Blog.Validate в предыдущем разделе.

Ниже приведен пример ValidateEntity override, который проверяет новые 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 активирует все проверки, определенные заметками или API Fluent, проверку, созданную 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 возникает при возникновении ошибок во время проверки
  • Аспекты, которые Entity Framework включает в модель (максимальная длина, обязательность и т. д.), вызовут проверку, даже если в ваших классах нет аннотаций данных и (или) использовался дизайнер EF для создания модели.
  • Правила приоритета:
    • Вызовы "Fluent API" переопределяют соответствующие аннотации данных
  • Порядок выполнения:
    • Проверка свойств происходит перед проверкой типа
    • Проверка типа возникает только в том случае, если проверка свойств завершается успешно
  • Если свойство является сложным, его проверка также будет включать:
    • Проверка уровня свойств для свойств сложного типа
    • Проверка уровня типа для сложного типа, в том числе проверка сложного типа с IValidatableObject.

Сводка

API проверки в Entity Framework играет очень хорошо с проверкой на стороне клиента в MVC, но вам не нужно полагаться на проверку на стороне клиента. Entity Framework будет обрабатывать проверку на стороне сервера для DataAnnotations или конфигураций, которые вы применили с помощью Fluent API типа Code First.

Вы также видели ряд точек расширяемости для настройки поведения, независимо от того, используете ли вы интерфейс IValidatableObject или обращаетесь к методу DbContext.ValidateEntity. И эти два последних средства проверки доступны через DbContext, независимо от того, используете ли вы рабочий процесс Code First, Model First или Database First для описания вашей концептуальной модели.