Convalida dei modelli in un'API Web ASP.NET

Questo articolo illustra come annotare i modelli, usare le annotazioni per la convalida dei dati e gestire gli errori di convalida nell'API Web. Quando un client invia dati all'API Web, spesso si vuole convalidare i dati prima di eseguire qualsiasi elaborazione.

Annotazioni dei dati

In API Web ASP.NET è possibile usare gli attributi dello spazio dei nomi System.ComponentModel.DataAnnotations per impostare le regole di convalida per le proprietà nel modello. Si consideri il modello seguente:

using System.ComponentModel.DataAnnotations;

namespace MyApi.Models
{
    public class Product
    {
        public int Id { get; set; }
        [Required]
        public string Name { get; set; }
        public decimal Price { get; set; }
        [Range(0, 999)]
        public double Weight { get; set; }
    }
}

Se è stata usata la convalida del modello in ASP.NET MVC, l'aspetto dovrebbe essere familiare. L'attributo Required indica che la Name proprietà non deve essere Null. L'attributo Range indica che Weight deve essere compreso tra zero e 999.

Si supponga che un client invii una richiesta POST con la rappresentazione JSON seguente:

{ "Id":4, "Price":2.99, "Weight":5 }

È possibile notare che il client non include la Name proprietà , contrassegnata come richiesta. Quando l'API Web converte il codice JSON in un'istanza Product , convalida l'oggetto in base agli Product attributi di convalida. Nell'azione del controller è possibile verificare se il modello è valido:

using MyApi.Models;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace MyApi.Controllers
{
    public class ProductsController : ApiController
    {
        public HttpResponseMessage Post(Product product)
        {
            if (ModelState.IsValid)
            {
                // Do something with the product (not shown).

                return new HttpResponseMessage(HttpStatusCode.OK);
            }
            else
            {
                return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
            }
        }
    }
}

La convalida del modello non garantisce che i dati client siano sicuri. Potrebbe essere necessaria una convalida aggiuntiva in altri livelli dell'applicazione. Ad esempio, il livello dati potrebbe applicare vincoli di chiave esterna. L'esercitazione Uso dell'API Web con Entity Framework illustra alcuni di questi problemi.

"Under-Posting": l'inserimento avviene quando il cliente esce da alcune proprietà. Si supponga, ad esempio, che il client invii quanto segue:

{"Id":4, "Name":"Gizmo"}

In questo caso, il client non ha specificato i valori per Price o Weight. Il formattatore JSON assegna un valore predefinito pari a zero alle proprietà mancanti.

Screenshot del frammento di codice con le opzioni del menu a discesa del punto Product punto product.

Lo stato del modello è valido, perché zero è un valore valido per queste proprietà. Se si tratta di un problema dipende dallo scenario in uso. In un'operazione di aggiornamento, ad esempio, è possibile distinguere tra "zero" e "non impostato". Per forzare i client a impostare un valore, rendere la proprietà nullable e impostare l'attributo Required :

[Required]
public decimal? Price { get; set; }

"Over-Posting": un client può anche inviare più dati del previsto. Ad esempio:

{"Id":4, "Name":"Gizmo", "Color":"Blue"}

In questo caso, il codice JSON include una proprietà ("Color") che non esiste nel Product modello. In questo caso, il formattatore JSON ignora semplicemente questo valore. Il formattatore XML esegue la stessa operazione. L'over-posting causa problemi se il modello ha proprietà che si intende essere di sola lettura. Ad esempio:

public class UserProfile
{
    public string Name { get; set; }
    public Uri Blog { get; set; }
    public bool IsAdmin { get; set; }  // uh-oh!
}

Non si vuole che gli utenti aggiornino la IsAdmin proprietà e si elevano a amministratori. La strategia più sicura consiste nell'usare una classe modello che corrisponde esattamente a ciò che il client può inviare:

public class UserProfileDTO
{
    public string Name { get; set; }
    public Uri Blog { get; set; }
    // Leave out "IsAdmin"
}

Nota

Il post di blog di Brad Wilson "Input Validation vs. Model Validation in ASP.NET MVC" (Convalida dell'input e convalida del modello in ASP.NET MVC) ha una buona discussione sulla sottosezione e l'over-post. Anche se il post riguarda ASP.NET MVC 2, i problemi sono ancora rilevanti per l'API Web.

Gestione degli errori di convalida

L'API Web non restituisce automaticamente un errore al client quando la convalida non riesce. Spetta all'azione del controller controllare lo stato del modello e rispondere in modo appropriato.

È anche possibile creare un filtro azioni per controllare lo stato del modello prima che venga richiamata l'azione del controller. Il codice seguente visualizza un esempio:

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Web.Http.ModelBinding;

namespace MyApi.Filters
{
    public class ValidateModelAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            if (actionContext.ModelState.IsValid == false)
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, actionContext.ModelState);
            }
        }
    }
}

Se la convalida del modello non riesce, questo filtro restituisce una risposta HTTP che contiene gli errori di convalida. In tal caso, l'azione del controller non viene richiamata.

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Date: Tue, 16 Jul 2013 21:02:29 GMT
Content-Length: 331

{
  "Message": "The request is invalid.",
  "ModelState": {
    "product": [
      "Required property 'Name' not found in JSON. Path '', line 1, position 17."
    ],
    "product.Name": [
      "The Name field is required."
    ],
    "product.Weight": [
      "The field Weight must be between 0 and 999."
    ]
  }
}

Per applicare questo filtro a tutti i controller API Web, aggiungere un'istanza del filtro alla raccolta HttpConfiguration.Filters durante la configurazione:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ValidateModelAttribute());

        // ...
    }
}

Un'altra opzione consiste nell'impostare il filtro come attributo su singoli controller o azioni del controller:

public class ProductsController : ApiController
{
    [ValidateModel]
    public HttpResponseMessage Post(Product product)
    {
        // ...
    }
}