Validation des données

Remarque

À partir d’EF4.1 uniquement : les fonctionnalités, les API et autres points abordés dans cette page ont été introduits dans Entity Framework 4.1. Si vous utilisez une version antérieure, certaines informations, voire toutes, ne s’appliquent pas

Le contenu de cette page est adapté d’un article écrit à l’origine par Julie Lerman (https://thedatafarm.com).

Entity Framework fournit une grande variété de fonctionnalités de validation, qui peuvent se répercuter sur une interface utilisateur pour la validation côté client ou être utilisées pour la validation côté serveur. Lorsque vous utilisez code-first, vous pouvez spécifier des validations à l’aide d’annotations ou de configurations d’API Fluent. Des validations supplémentaires, et plus complexes, peuvent être spécifiées dans le code et elles fonctionneront si votre modèle a une origine code-first, model-first ou database-first.

Modèle

Je vais illustrer les validations avec une paire simple de classes : Blog et 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; }
}

Annotations de données

Code First utilise des annotations de l’assembly System.ComponentModel.DataAnnotations comme un moyen de configurer les classes code-first. Parmi ces annotations figurent celles qui fournissent des règles comme Required, MaxLength et MinLength. Un certain nombre d’applications clientes .NET reconnaissent également ces annotations, par exemple ASP.NET MVC. Vous pouvez obtenir la validation côté client et côté serveur à la fois avec ces annotations. Par exemple, vous pouvez forcer la propriété Blog Title à être une propriété requise.

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

Sans modification supplémentaire du code ou du balisage dans l’application, une application MVC existante effectuera une validation côté client, et génèrera même dynamiquement un message à l’aide des noms de propriété et d’annotation.

figure 1

Dans la méthode post-back de cette vue Create, Entity Framework est utilisé pour enregistrer le nouveau blog dans la base de données, mais la validation côté client de MVC est déclenchée avant que l’application atteigne ce code.

Toutefois, la validation côté client n’est pas infaillible. Les utilisateurs peuvent impacter les fonctionnalités de leur navigateur ou, pire encore, un pirate peut utiliser des astuces pour éviter les validations de l’interface utilisateur. Mais Entity Framework reconnaîtra également l’annotation Required et la validera.

Un moyen simple de tester ce point consiste à désactiver la fonctionnalité de validation côté client de MVC. Vous pouvez le faire dans le fichier web.config de l’application MVC. La section appSettings a une clé pour ClientValidationEnabled. Définir cette clé sur false empêchera l’interface utilisateur d’effectuer des validations.

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

Même si la validation côté client est désactivée, vous obtiendrez la même réponse dans votre application. Le message d’erreur « Le champ Titre est requis » s’affiche comme précédemment. À la différence que, maintenant, il sera le résultat de la validation côté serveur. Entity Framework effectuera la validation sur l’annotation Required (avant même qu’elle ne commence à générer une commande INSERT à envoyer à la base de données) et retournera l’erreur à MVC, ce qui affichera le message.

API Fluent

Vous pouvez utiliser l’API Fluent de code-first au lieu d’annotations pour obtenir la même validation côté serveur et côté client. Au lieu d’utiliser Required, je vais vous montrer cela à l’aide d’une validation MaxLength.

Les configurations d’API Fluent sont appliquées lorsque que code-first génère le modèle à partir des classes. Vous pouvez injecter les configurations en remplaçant la méthode OnModelCreating de la classe DbContext. Voici une configuration spécifiant que la propriété BloggerName ne peut pas dépasser 10 caractères.

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

Les erreurs de validation levées en fonction des configurations de l’API Fluent n’atteignent pas automatiquement l’interface utilisateur, mais vous pouvez la capturer dans le code, puis y répondre en conséquence.

Voici un code d’erreur de gestion des exceptions dans la classe BlogController de l’application, qui capture cette erreur de validation lorsque Entity Framework tente d’enregistrer un blog avec un BloggerName qui dépasse le maximum de 10 caractères.

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

La validation n’est pas renvoyée automatiquement dans la vue, c’est pourquoi le code supplémentaire qui utilise ModelState.AddModelError est utilisé. Cela garantit que les détails de l’erreur le arrivent dans la vue, qui utilisera ensuite le Htmlhelper ValidationMessageFor pour afficher l’erreur.

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

IValidatableObject

IValidatableObject est une interface qui réside dans System.ComponentModel.DataAnnotations. Bien qu’elle ne fasse pas partie de l’API Entity Framework, vous pouvez toujours l’exploiter pour la validation côté serveur dans vos classes Entity Framework. IValidatableObject fournit une méthode Validate qu’Entity Framework appelle pendant SaveChanges, ou que vous pouvez appeler vous-même au moment où vous souhaitez valider les classes.

Les configurations telles que Required et MaxLength effectuent la validation sur un seul champ. Dans la méthode Validate, vous pouvez avoir une logique encore plus complexe, comme par exemple comparer deux champs.

Dans l’exemple suivant, la classe Blog a été étendue pour implémenter IValidatableObject, puis pour fournir une règle que ni Title ni BloggerName ne peuvent respecter.

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

Le constructeur ValidationResult accepte un string qui représente le message d’erreur et un tableau de string qui représentent les noms de membres associés à la validation. Cette validation vérifiant à la fois Title et BloggerName, les deux noms de propriété sont retournés.

Contrairement à la validation fournie par l’API Fluent, ce résultat de validation est reconnu par la vue et le gestionnaire d’exceptions que j’ai utilisé précédemment pour ajouter l’erreur dans ModelState est inutile. Étant donné que j’ai défini les deux noms de propriétés dans ValidationResult, le MVC HtmlHelpers affiche le message d’erreur pour ces deux propriétés.

figure 2

DbContext.ValidateEntity

DbContext a une méthode substituable appelée ValidateEntity. Lorsque vous appelez SaveChanges, Entity Framework appellera cette méthode pour chaque entité dans son cache dont l’état n’est pas Unchanged. Vous pouvez placer la logique de validation directement ici ou encore utiliser cette méthode pour appeler, par exemple, la méthode Blog.Validate ajoutée dans la section précédente.

Voici un exemple de remplacement de ValidateEntity qui valide les nouveaux Post pour garantir que le titre du billet n’a pas déjà été utilisé. Il vérifie d’abord si l’entité est un billet et que son état est Ajouté. Si c’est le cas, il cherche dans la base de données pour voir s’il existe déjà un billet portant le même titre. Si un tel billet existe déjà, un nouveau DbEntityValidationResult est créé.

DbEntityValidationResult héberge DbEntityEntry et ICollection<DbValidationErrors> pour une entité unique. Au début de cette méthode, un DbEntityValidationResult est instancié, puis toutes les erreurs découvertes sont ajoutées à sa collection 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);
    }
}

Validation à déclenchement explicite

Un appel de SaveChanges déclenche toutes les validations abordées dans cet article. Mais vous n’avez pas besoin de ne compter que sur SaveChanges. Vous préférez peut-être valider ailleurs dans votre application.

DbContext.GetValidationErrors déclenche toutes les validations, celles définies par les annotations ou l’API Fluent, la validation créée dans IValidatableObject (par exemple, Blog.Validate) et les validations effectuées dans la méthode DbContext.ValidateEntity.

Le code suivant appellera GetValidationErrors sur l’instance actuelle d’un DbContext. Les ValidationErrors sont regroupées par type d’entité dans DbEntityValidationResult. Le code itère d’abord sur les DbEntityValidationResult retournés par la méthode, puis sur chaque DbValidationError à l’intérieur.

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

Autres considérations lors de l’utilisation de la validation

Voici quelques autres points à prendre en compte lors de l’utilisation de la validation Entity Framework :

  • Le chargement différé est désactivé pendant la validation
  • EF validera les annotations de données sur les propriétés non mappées (propriétés qui ne sont pas mappées à une colonne dans la base de données)
  • La validation est effectuée une fois les modifications détectées pendant SaveChanges. Si vous apportez des modifications pendant la validation, il vous incombe de notifier le suivi des modifications
  • Une DbUnexpectedValidationException est levée si des erreurs se produisent pendant la validation
  • Les facettes qu’Entity Framework inclut dans le modèle (longueur maximale, obligatoire, etc.) entraîneront la validation, même s’il n’existe aucune annotation de données dans vos classes et/ou si vous avez utilisé EF Designer pour créer votre modèle
  • Règles de précédence :
    • Les appels d’API Fluent remplacent les annotations de données correspondantes
  • Ordre d’exécution :
    • La validation de propriété se produit avant la validation de type
    • La validation de type se produit uniquement si la validation de propriété réussit
  • Si une propriété est complexe, sa validation inclut également :
    • Validation au niveau de la propriété sur les propriétés de type complexes
    • Validation au niveau du type complexe, y compris la validation IValidatableObject sur le type complexe

Résumé

L’API de validation dans Entity Framework fonctionne parfaitement avec la validation côté client dans MVC, mais vous n’avez pas à dépendre de la validation côté client. Entity Framework s’occupe de la validation côté serveur pour les DataAnnotations ou les configurations que vous avez appliquées avec l’API Fluent code-first.

Vous avez également vu un certain nombre de points d’extensibilité pour personnaliser le comportement, que vous utilisiez l’interface IValidatableObject ou exploitiez la méthode DbContext.ValidateEntity. Ces deux derniers moyens de validation sont disponibles via le DbContext, que vous utilisiez un flux de travail Code First, Model First ou Database First pour décrire votre modèle conceptuel.