Convalida dei dati

Annotazioni

EF4.1 Solo versioni successive : le funzionalità, le API e così via descritte in questa pagina sono state introdotte in Entity Framework 4.1. Se si usa una versione precedente, alcune o tutte le informazioni non si applicano

Il contenuto di questa pagina è adattato da un articolo originariamente scritto da Julie Lerman (https://thedatafarm.com).

Entity Framework offre un'ampia gamma di funzionalità di convalida che possono essere fornite tramite un'interfaccia utente per la convalida lato client o per la convalida lato server. Quando si usa il codice per primo, è possibile specificare le convalide usando le configurazioni di annotazione o API Fluent. Le convalide aggiuntive e più complesse possono essere specificate nel codice e funzioneranno se il modello proviene prima dal codice, dal modello o dal database.

Il modello

Dimostrare le convalide con una semplice coppia di classi: 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; }
}

Annotazioni dei dati

Code First usa annotazioni dall'assembly System.ComponentModel.DataAnnotations come mezzo per configurare le classi code first. Tra queste annotazioni sono quelle che forniscono regole come Required, MaxLength e MinLength. Alcune applicazioni client .NET riconoscono anche queste annotazioni, ad esempio ASP.NET MVC. È possibile ottenere la convalida lato client e lato server con queste annotazioni. Ad esempio, è possibile forzare la proprietà Titolo blog per essere una proprietà obbligatoria.

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

Senza modifiche di codice o markup aggiuntive nell'applicazione, un'applicazione MVC esistente eseguirà la convalida lato client, anche creando dinamicamente un messaggio usando i nomi di proprietà e annotazione.

Figura 1

Nel metodo di postback di questa visualizzazione Crea, Entity Framework viene usato per salvare il nuovo blog nel database, ma la convalida lato client di MVC viene attivata prima che l'applicazione raggiunga tale codice.

La convalida lato client non è tuttavia completamente sicura. Gli utenti possono influire sulle funzionalità del browser o, peggio ancora, un hacker potrebbe usare qualche inganno per evitare le convalide dell'interfaccia utente. Ma Entity Framework riconoscerà anche l'annotazione Required e la convalida.

Un modo semplice per testare questo è disabilitare la funzionalità di convalida lato client di MVC. È possibile eseguire questa operazione nel file di web.config dell'applicazione MVC. La sezione appSettings ha una chiave per ClientValidationEnabled. L'impostazione di questa chiave su false impedirà all'interfaccia utente di eseguire convalide.

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

Anche se la convalida lato client è disabilitata, si otterrà la stessa risposta nell'applicazione. Il messaggio di errore "Il campo Titolo è obbligatorio" verrà visualizzato come in precedenza. Ad eccezione di questo momento, sarà il risultato della convalida lato server. Entity Framework eseguirà la convalida per l'annotazione Required (prima di creare un INSERT comando da inviare al database) e restituirà l'errore a MVC che visualizzerà il messaggio.

Fluent API

È possibile usare l'API Fluent di Code First anziché le annotazioni per ottenere la stessa convalida lato client e lato server. Invece di usare Required, ti mostrerò questo utilizzando una validazione MaxLength.

Le configurazioni dell'API Fluent vengono applicate mentre Code First costruisce il modello dalle classi. È possibile inserire le configurazioni eseguendo l'override del metodo OnModelCreating della classe DbContext. Ecco una configurazione che specifica che la proprietà BloggerName non può contenere più di 10 caratteri.

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

Gli errori di convalida generati in base alle configurazioni dell'API Fluent non raggiungono automaticamente l'interfaccia utente, ma è possibile acquisirlo nel codice e quindi rispondere di conseguenza.

Ecco un codice di errore per la gestione delle eccezioni nella classe BlogController dell'applicazione che acquisisce l'errore di convalida quando Entity Framework tenta di salvare un blog con un BloggerName che supera il massimo di 10 caratteri.

[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 convalida non viene automaticamente riportata nella visualizzazione, motivo per cui viene utilizzato del codice aggiuntivo con ModelState.AddModelError. In questo modo si garantisce che i dettagli dell'errore vengano visualizzati nella visualizzazione, che successivamente utilizzerà ValidationMessageFor Htmlhelper per mostrare l'errore.

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

IValidatableObject

IValidatableObject è un'interfaccia che risiede in System.ComponentModel.DataAnnotations. Anche se non fa parte dell'API Entity Framework, è comunque possibile sfruttarla per la convalida lato server nelle classi di Entity Framework. IValidatableObject fornisce un Validate metodo che Entity Framework chiamerà durante SaveChanges oppure è possibile chiamare se stessi ogni volta che si desidera convalidare le classi.

Configurazioni come Required e MaxLength eseguono la convalida su un singolo campo. Validate Nel metodo è possibile avere una logica ancora più complessa, ad esempio confrontando due campi.

Nell'esempio seguente la Blog classe è stata estesa per implementare IValidatableObject e quindi fornire una regola che non può corrispondere a Title e 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) });
        }
    }
}

Il ValidationResult costruttore accetta un string oggetto che rappresenta il messaggio di errore e una matrice di strings che rappresentano i nomi dei membri associati alla convalida. Poiché questa convalida controlla sia e TitleBloggerName, vengono restituiti entrambi i nomi di proprietà.

A differenza della convalida fornita dall'API Fluent, questo risultato di convalida verrà riconosciuto dalla visualizzazione e dal gestore eccezioni usato in precedenza per aggiungere l'errore in ModelState non è necessario. Poiché sono stati impostati entrambi i nomi di proprietà in ValidationResult, in MVC HtmlHelpers viene visualizzato il messaggio di errore per entrambe le proprietà.

figura 2

DbContext.ValidateEntity

DbContext dispone di un metodo sottoposto a override denominato ValidateEntity. Quando si chiama SaveChanges, Entity Framework chiamerà questo metodo per ogni entità nella cache il cui stato non è Unchanged. È possibile inserire la logica di convalida direttamente qui o anche usare questo metodo per chiamare, ad esempio, il Blog.Validate metodo aggiunto nella sezione precedente.

Di seguito è riportato un esempio di ValidateEntity override che convalida i nuovi Postelementi per assicurarsi che il titolo del post non sia già stato usato. Prima di tutto verifica se l'entità è un post ed il suo stato è Aggiunto. In questo caso, il database cerca se è già presente un post con lo stesso titolo. Se è già presente un post esistente, viene creato un nuovo DbEntityValidationResult post.

DbEntityValidationResult ospita un DbEntityEntry e un ICollection<DbValidationErrors> per una singola entità. All'inizio di questo metodo viene creata un'istanza di DbEntityValidationResult e successivamente vengono aggiunti eventuali errori individuati alla sua raccolta 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);
    }
}

Attivazione esplicita della convalida

Una chiamata a SaveChanges attiva tutte le convalide descritte in questo articolo. Ma non è necessario basarsi su SaveChanges. È possibile che si preferisca convalidare altrove nell'applicazione.

DbContext.GetValidationErrors attiverà tutte le convalide, quelle definite dalle annotazioni o dall'API Fluent, la convalida creata in IValidatableObject (ad esempio, Blog.Validate) e le convalide eseguite nel DbContext.ValidateEntity metodo .

Il codice seguente chiamerà GetValidationErrors sull'istanza corrente di un oggetto DbContext. ValidationErrors sono raggruppati per tipo di entità in DbEntityValidationResult. Il codice scorre prima l'oggetto DbEntityValidationResultrestituito dal metodo e quindi ognuno DbValidationError all'interno.

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

Altre considerazioni sull'uso della convalida

Ecco alcuni altri punti da considerare quando si usa la convalida di Entity Framework:

  • Il caricamento differito è disabilitato durante convalida
  • Entity Framework convaliderà le annotazioni dei dati in proprietà non mappate (proprietà non mappate a una colonna nel database)
  • La convalida viene eseguita dopo il rilevamento delle modifiche durante SaveChanges. Se si apportano modifiche durante la convalida, è responsabilità dell'utente notificare il responsabile delle modifiche.
  • DbUnexpectedValidationException viene generata se si verificano errori durante la convalida
  • I facet che Entity Framework include nel modello (lunghezza massima, necessaria e così via) avvieranno la convalida, anche se non sono presenti annotazioni dei dati nelle classi e/o è stato usato l'EF Designer per creare il modello.
  • Regole di precedenza:
    • Le chiamate API Fluent sostituiscono le annotazioni dei dati corrispondenti
  • Ordine di esecuzione:
    • La convalida delle proprietà viene eseguita prima della convalida del tipo
    • La convalida dei tipi si verifica solo se la convalida delle proprietà ha esito positivo
  • Se una proprietà è complessa, la convalida includerà anche:
    • Convalida a livello di proprietà nelle proprietà di tipo complesso
    • Convalida del tipo a livello di tipo per il tipo complesso, inclusa la convalida IValidatableObject sul tipo complesso

Riassunto

L'API di convalida in Entity Framework funziona molto bene con la convalida lato client in MVC, ma non è necessario basarsi sulla convalida lato client. Entity Framework si occuperà della convalida sul lato server per le DataAnnotations o le configurazioni che hai applicato con il codice first Fluent API.

Hai anche visto diversi punti di estendibilità per personalizzare il comportamento, sia che usi l'interfaccia IValidatableObject o il metodo DbContext.ValidateEntity. E questi ultimi due mezzi di convalida sono disponibili tramite DbContext, indipendentemente dal fatto che si usi il flusso di lavoro Code First, Model First o Database First per descrivere il modello concettuale.