Validação de dados

Observação

Somente EF4.1 – os recursos, as APIs etc. discutidos nesta página foram introduzidos no Entity Framework 4.1. Se você estiver usando uma versão anterior, algumas ou todas as informações não se aplicarão

O conteúdo desta página é adaptado de um artigo originalmente escrito por Julie Lerman (https://thedatafarm.com).

O Entity Framework fornece uma grande variedade de recursos de validação que podem ser alimentados por meio de uma interface do usuário para validação do lado do cliente ou ser usados para validação do lado do servidor. Ao usar o código primeiro, você pode especificar validações usando anotações ou configurações de API fluente. Validações adicionais, que são mais complexas, podem ser especificadas no código e funcionarão independentemente de se o modelo tem origem no código, no modelo ou no banco de dados.

O modelo

Demonstrarei as validações com um par simples de classes: Blog e 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; }
}

Anotações de dados

O Code First usa anotações do System.ComponentModel.DataAnnotations assembly como um meio de configurar as primeiras classes de código. Entre essas anotações estão aquelas que fornecem regras como , RequiredMaxLength e MinLength. Vários aplicativos cliente .NET também reconhecem essas anotações, por exemplo, ASP.NET MVC. Você pode obter a validação do lado do cliente e do servidor com essas anotações. Por exemplo, você pode forçar a propriedade Título do Blog a ser uma propriedade necessária.

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

Sem alterações adicionais de código ou marcação no aplicativo, um aplicativo MVC existente executará a validação do lado do cliente, mesmo criando dinamicamente uma mensagem usando os nomes de propriedade e anotação.

figura 1

No método post back dessa exibição Create, o Entity Framework é usado para salvar o novo blog no banco de dados, mas a validação do lado do cliente do MVC é disparada antes que o aplicativo atinja esse código.

No entanto, a validação do lado do cliente não é à prova de falhas. Os usuários podem afetar os recursos de seu navegador ou pior ainda, um hacker pode usar algum truque para evitar as validações da interface do usuário. Mas o Entity Framework também reconhecerá a Required anotação e a validará.

Uma maneira simples de testar isso é desabilitar o recurso de validação do lado do cliente do MVC. Você pode fazer isso no arquivo de web.config do aplicativo MVC. A seção appSettings tem uma chave para ClientValidationEnabled. Definir essa chave como false impedirá que a interface do usuário execute validações.

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

Mesmo com a validação do lado do cliente desabilitada, você obterá a mesma resposta em seu aplicativo. A mensagem de erro "O campo Título é necessário" será exibida como antes. Exceto que agora será resultado da validação do lado do servidor. O Entity Framework executará a validação na Required anotação (antes mesmo de se preocupar em criar um INSERT comando para enviar ao banco de dados) e retornará o erro ao MVC, que exibirá a mensagem.

API fluente

Você pode usar a API fluente do code first em vez de anotações para obter a mesma validação do lado do cliente e do lado do servidor. Em vez de usar Required, mostrarei isso usando uma validação MaxLength.

As configurações do Fluent API são aplicadas enquanto o Code First está construindo o modelo a partir das classes. Você pode injetar as configurações substituindo o método OnModelCreating da classe DbContext. Aqui está uma configuração que especifica que a propriedade BloggerName não pode ter mais de 10 caracteres.

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);
    }
}

Os erros de validação gerados com base nas configurações da API fluente não alcançarão automaticamente a interface do usuário, mas você pode capturá-la no código e, em seguida, responder a ela de acordo.

Aqui está um código de erro de tratamento de exceção na classe BlogController do aplicativo que captura esse erro de validação quando o Entity Framework tenta salvar um blog com um BloggerName que excede o máximo de 10 caracteres.

[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();
    }
}

A validação não é automaticamente passada de volta para a exibição e é por isso que o código adicional que usa ModelState.AddModelError está sendo usado. Isso garante que os detalhes do erro cheguem à exibição, que então usará o ValidationMessageFor Htmlhelper para mostrar o erro.

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

IValidatableObject

IValidatableObject é uma interface que reside em System.ComponentModel.DataAnnotations. Embora não faça parte da API do Entity Framework, você ainda pode aproveitá-la para validação do lado do servidor em suas classes do Entity Framework. IValidatableObject fornece um Validate método que o Entity Framework chamará durante SaveChanges ou você pode se chamar sempre que quiser validar as classes.

Configurações como Required e MaxLength executar a validação em um único campo. Validate No método, você pode ter uma lógica ainda mais complexa, por exemplo, comparando dois campos.

No exemplo a seguir, a classe Blog foi estendida para implementar IValidatableObject e, em seguida, fornecer uma regra que Title e BloggerName não podem corresponder.

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) });
        }
    }
}

O ValidationResult construtor usa um string que representa a mensagem de erro e uma matriz de strings que representam os nomes dos membros associados à validação. Como essa validação verifica tanto o Title quanto o BloggerName, ambos os nomes de propriedade são retornados.

Ao contrário da validação fornecida pela API fluente, esse resultado de validação será reconhecido pelo View e o manipulador de exceção que eu usei anteriormente para adicionar o erro em ModelState é desnecessário. Porque defini os dois nomes de propriedade no ValidationResult, os HtmlHelpers do MVC exibem a mensagem de erro para ambas as propriedades.

figura 2

DbContext.ValidateEntity

DbContext tem um método substituível chamado ValidateEntity. Quando você chamar SaveChanges, o Entity Framework chamará este método para cada entidade em seu cache cujo estado não é Unchanged. Você pode colocar a lógica de validação diretamente aqui ou até mesmo usar esse método para chamar, por exemplo, o Blog.Validate método adicionado na seção anterior.

Aqui está um exemplo de uma ValidateEntity substituição que valida os novos Posts para garantir que o título da postagem não tenha sido usado anteriormente. Ele primeiro verifica se a entidade é uma postagem e se seu estado é Adicionado. Se esse for o caso, ele procurará no banco de dados para ver se já há uma postagem com o mesmo título. Se já houver uma postagem existente, uma nova DbEntityValidationResult será criada.

DbEntityValidationResult abriga um DbEntityEntry e um ICollection<DbValidationErrors> para uma única entidade. No início desse método, um DbEntityValidationResult é instanciado e, em seguida, todos os erros descobertos são adicionados à sua coleção 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);
    }
}

Acionando a validação de forma explícita

Uma chamada para SaveChanges aciona todas as validações abordadas neste artigo. Mas você não precisa confiar em SaveChanges. Talvez você prefira validar em outro lugar em seu aplicativo.

DbContext.GetValidationErrors disparará todas as validações, aquelas definidas por anotações ou pela API fluente, a validação criada em IValidatableObject (por exemplo), Blog.Validatee as validações executadas no DbContext.ValidateEntity método.

O código a seguir chamará GetValidationErrors na instância atual de um DbContext. ValidationErrors são agrupados por tipo de entidade em DbEntityValidationResult. O código itera primeiro por meio dos DbEntityValidationResults retornados pelo método e, em seguida, por cada DbValidationError dentro.

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

Outras considerações ao usar a validação

Aqui estão alguns outros pontos a serem considerados ao usar a validação do Entity Framework:

  • O carregamento lento é desabilitado durante a validação
  • O EF validará anotações de dados em propriedades não mapeadas (propriedades que não são mapeadas para uma coluna no banco de dados)
  • A validação é executada depois que as alterações são detectadas durante SaveChanges. Se você fizer alterações durante a validação, será sua responsabilidade notificar o rastreador de alterações
  • DbUnexpectedValidationException será lançado se ocorrerem erros durante a validação
  • As facetas que o Entity Framework inclui no modelo (comprimento máximo, obrigatório etc.) causarão validação, mesmo que não haja anotações de dados em suas classes e/ou você tenha usado o Designer EF para criar seu modelo
  • Regras de precedência:
    • As chamadas à API fluente substituem as anotações de dados correspondentes
  • Ordem de execução:
    • A validação de propriedade ocorre antes da validação de tipo
    • A validação de tipo só ocorrerá se a validação de propriedade for bem-sucedida
  • Se uma propriedade for complexa, sua validação também incluirá:
    • Validação em nível de propriedade nas propriedades de tipo complexo
    • Validação em nível de tipo no tipo complexo, incluindo validação IValidatableObject no tipo complexo.

Resumo

A API de validação no Entity Framework é muito boa com a validação do lado do cliente no MVC, mas você não precisa depender da validação do lado do cliente. O Entity Framework cuidará da validação no lado do servidor para DataAnnotations ou configurações que você aplicou com o Fluent API em código primeiro.

Quer você use a IValidatableObject interface ou utilize o método DbContext.ValidateEntity, você também viu vários pontos de extensibilidade para personalizar o comportamento. E esses dois últimos meios de validação estão disponíveis por meio do DbContextfluxo de trabalho Code First, Model First ou Database First para descrever seu modelo conceitual.