Gestire la convalida ModelState nell'API Web ASP.NET


106

Mi chiedevo come posso ottenere la convalida del modello con l'API Web ASP.NET. Ho il mio modello così:

public class Enquiry
{
    [Key]
    public int EnquiryId { get; set; }
    [Required]
    public DateTime EnquiryDate { get; set; }
    [Required]
    public string CustomerAccountNumber { get; set; }
    [Required]
    public string ContactName { get; set; }
}

Ho quindi un'azione Post nel mio controller API:

public void Post(Enquiry enquiry)
{
    enquiry.EnquiryDate = DateTime.Now;
    context.DaybookEnquiries.Add(enquiry);
    context.SaveChanges();
}

Come aggiungo if(ModelState.IsValid)e poi gestisco il messaggio di errore da trasmettere all'utente?

Risposte:


186

Per separare le preoccupazioni, ti suggerirei di utilizzare il filtro di azione per la convalida del modello, quindi non devi preoccuparti molto di come eseguire la convalida nel tuo controller API:

using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace System.Web.Http.Filters
{
    public class ValidationActionFilter : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            var modelState = actionContext.ModelState;

            if (!modelState.IsValid)
                actionContext.Response = actionContext.Request
                     .CreateErrorResponse(HttpStatusCode.BadRequest, modelState);
        }
    }
}

27
Gli spazi dei nomi necessari per questo sono System.Net.Http, System.Net System.Web.Http.Controllerse System.Web.Http.Filters.
Christopher Stevenson

11
Esiste anche un'implementazione simile nella pagina ufficiale ASP.NET Web Api: asp.net/web-api/overview/formats-and-model-binding/…
Erik Schierboom

1
Anche se non metti [ValidationActionFilter] sopra l'API web, chiama comunque il codice e mi da una cattiva richiesta.
micronyks

1
Vale la pena sottolineare che la risposta di errore restituita è controllata da IncludeErrorDetailPolicy . Per impostazione predefinita, la risposta a una richiesta remota contiene solo un messaggio generico "Si è verificato un errore", ma l'impostazione di questo IncludeErrorDetailPolicy.Alwaysincluderà i dettagli (a rischio di esporre i dettagli ai tuoi utenti)
Rob

C'è un motivo specifico per cui non hai suggerito di utilizzare IAsyncActionFilter invece?
Ravior

30

Forse non quello che stavi cercando, ma forse è bello che qualcuno lo sappia:

Se stai usando .net Web Api 2 puoi semplicemente fare quanto segue:

if (!ModelState.IsValid)
     return BadRequest(ModelState);

A seconda degli errori del modello, ottieni questo risultato:

{
   Message: "The request is invalid."
   ModelState: {
       model.PropertyA: [
            "The PropertyA field is required."
       ],
       model.PropertyB: [
             "The PropertyB field is required."
       ]
   }
}

1
Tieni a mente quando ho posto questa domanda Web API 1 è stata appena rilasciata, probabilmente è
cambiata

Assicurati di contrassegnare le proprietà come facoltative, altrimenti riceverai un generico non utile "Si è verificato un errore". messaggio di errore.
Bouke

1
C'è un modo per cambiare il messaggio?
saquib adil

29

In questo modo, ad esempio:

public HttpResponseMessage Post(Person person)
{
    if (ModelState.IsValid)
    {
        PersonDB.Add(person);
        return Request.CreateResponse(HttpStatusCode.Created, person);
    }
    else
    {
        // the code below should probably be refactored into a GetModelErrors
        // method on your BaseApiController or something like that

        var errors = new List<string>();
        foreach (var state in ModelState)
        {
            foreach (var error in state.Value.Errors)
            {
                errors.Add(error.ErrorMessage);
            }
        }
        return Request.CreateResponse(HttpStatusCode.Forbidden, errors);
    }
}

Questo restituirà una risposta come questa (assumendo JSON, ma lo stesso principio di base per XML):

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
(some headers removed here)

["A value is required.","The field First is required.","Some custom errorm essage."]

Ovviamente puoi costruire il tuo oggetto / elenco di errore nel modo che preferisci, ad esempio aggiungendo nomi di campo, ID di campo ecc.

Anche se è una chiamata Ajax "unidirezionale" come un POST di una nuova entità, dovresti comunque restituire qualcosa al chiamante, qualcosa che indichi se la richiesta ha avuto successo o meno. Immagina un sito in cui il tuo utente aggiungerà alcune informazioni su se stesso tramite una richiesta AJAX POST. Cosa succede se le informazioni che hanno cercato di inserire non sono valide, come fanno a sapere se la loro azione di salvataggio ha avuto successo o meno?

Il modo migliore per farlo è utilizzare Good Old HTTP Status Code come 200 OKe così via. In questo modo il tuo JavaScript può gestire correttamente gli errori utilizzando i callback corretti (errore, successo, ecc.).

Ecco un bel tutorial su una versione più avanzata di questo metodo, utilizzando un ActionFilter e jQuery: http://asp.net/web-api/videos/getting-started/custom-validation


Questo restituisce solo il mio enquiryoggetto, ma non dice quali proprietà non sono valide? Quindi, se ho lasciato CustomerAccountNumbervuoto, dovrebbe indicare il messaggio di convalida predefinito (il campo
CusomterAccountNumber

Capisco, quindi questo è il modo "corretto" di gestire la convalida del modello? Mi sembra un po 'disordinato ..
CallumVass

Ci sono anche altri modi per farlo, come collegarsi con jQuery validation. Ecco un bell'esempio di Microsoft: asp.net/web-api/videos/getting-started/custom-validation
Anders Arpi

Questo metodo e il metodo scelto come risposta "dovrebbero essere" funzionalmente identici, quindi questa risposta ha il valore aggiunto di mostrarti come potresti farlo da solo senza un filtro di azione.
Shaun Wilson

Ho dovuto cambiare la linea errors.Add(error.ErrorMessage);per errors.Add(error.Exception.Message);per ottenere questo lavoro per me.
Caltor

9

8

Oppure, se stai cercando una semplice raccolta di errori per le tue app ... ecco la mia implementazione di questo:

public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var modelState = actionContext.ModelState;

        if (!modelState.IsValid) 
        {

            var errors = new List<string>();
            foreach (var state in modelState)
            {
                foreach (var error in state.Value.Errors)
                {
                    errors.Add(error.ErrorMessage);
                }
            }

            var response = new { errors = errors };

            actionContext.Response = actionContext.Request
                .CreateResponse(HttpStatusCode.BadRequest, response, JsonMediaTypeFormatter.DefaultMediaType);
        }
    }

La risposta al messaggio di errore sarà simile a:

{
  "errors": [
    "Please enter a valid phone number (7+ more digits)",
    "Please enter a valid e-mail address"
  ]
}

5

Aggiungi sotto il codice nel file startup.cs

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2).ConfigureApiBehaviorOptions(options =>
            {
                options.InvalidModelStateResponseFactory = (context) =>
                {
                    var errors = context.ModelState.Values.SelectMany(x => x.Errors.Select(p => new ErrorModel()
                   {
                       ErrorCode = ((int)HttpStatusCode.BadRequest).ToString(CultureInfo.CurrentCulture),
                        ErrorMessage = p.ErrorMessage,
                        ServerErrorMessage = string.Empty
                    })).ToList();
                    var result = new BaseResponse
                    {
                        Error = errors,
                        ResponseCode = (int)HttpStatusCode.BadRequest,
                        ResponseMessage = ResponseMessageConstants.VALIDATIONFAIL,

                    };
                    return new BadRequestObjectResult(result);
                };
           });

3

Qui puoi controllare per mostrare l'errore di stato del modello uno per uno

 public HttpResponseMessage CertificateUpload(employeeModel emp)
    {
        if (!ModelState.IsValid)
        {
            string errordetails = "";
            var errors = new List<string>();
            foreach (var state in ModelState)
            {
                foreach (var error in state.Value.Errors)
                {
                    string p = error.ErrorMessage;
                    errordetails = errordetails + error.ErrorMessage;

                }
            }
            Dictionary<string, object> dict = new Dictionary<string, object>();



            dict.Add("error", errordetails);
            return Request.CreateResponse(HttpStatusCode.BadRequest, dict);


        }
        else
        {
      //do something
        }
        }

}


3

C #

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

...

    [ValidateModel]
    public HttpResponseMessage Post([FromBody]AnyModel model)
    {

Javascript

$.ajax({
        type: "POST",
        url: "/api/xxxxx",
        async: 'false',
        contentType: "application/json; charset=utf-8",
        data: JSON.stringify(data),
        error: function (xhr, status, err) {
            if (xhr.status == 400) {
                DisplayModelStateErrors(xhr.responseJSON.ModelState);
            }
        },
....


function DisplayModelStateErrors(modelState) {
    var message = "";
    var propStrings = Object.keys(modelState);

    $.each(propStrings, function (i, propString) {
        var propErrors = modelState[propString];
        $.each(propErrors, function (j, propError) {
            message += propError;
        });
        message += "\n";
    });

    alert(message);
};

2

Ho avuto un problema nell'implementazione del modello di soluzione accettato in cui il mio ModelStateFiltersarebbe sempre tornato false(e successivamente un 400) actionContext.ModelState.IsValidper determinati oggetti del modello:

public class ModelStateFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response = new HttpResponseMessage { StatusCode = HttpStatusCode.BadRequest};
        }
    }
}

Accetto solo JSON, quindi ho implementato una classe di raccoglitore di modelli personalizzata:

public class AddressModelBinder : System.Web.Http.ModelBinding.IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, System.Web.Http.ModelBinding.ModelBindingContext bindingContext)
    {
        var posted = actionContext.Request.Content.ReadAsStringAsync().Result;
        AddressDTO address = JsonConvert.DeserializeObject<AddressDTO>(posted);
        if (address != null)
        {
            // moar val here
            bindingContext.Model = address;
            return true;
        }
        return false;
    }
}

Quale registro direttamente dopo il mio modello tramite

config.BindParameter(typeof(AddressDTO), new AddressModelBinder());

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.