Gérer la validation ModelState dans l'API Web ASP.NET


106

Je me demandais comment réaliser la validation de modèle avec l'API Web ASP.NET. J'ai mon modèle comme ça:

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

J'ai alors une action Post dans mon contrôleur API:

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

Comment ajouter if(ModelState.IsValid)puis gérer le message d'erreur à transmettre à l'utilisateur?

Réponses:


186

Pour séparer les préoccupations, je vous suggère d'utiliser un filtre d'action pour la validation du modèle, vous n'avez donc pas besoin de vous soucier de la façon de faire la validation dans votre contrôleur 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
Les espaces de noms nécessaires à cette fin sont System.Net.Http, System.Net System.Web.Http.Controllerset System.Web.Http.Filters.
Christopher Stevenson

11
Il existe également une implémentation similaire sur la page officielle de l'API Web ASP.NET: asp.net/web-api/overview/formats-and-model-binding
Erik Schierboom

1
Même si ne mettez pas [ValidationActionFilter] au-dessus de l'API Web, il appelle toujours le code et me donne une mauvaise demande.
micronyks

1
Il convient de souligner que la réponse d'erreur renvoyée est contrôlée par IncludeErrorDetailPolicy . Par défaut, la réponse à une demande distante ne contient qu'un message générique "Une erreur s'est produite", mais le paramétrer sur IncludeErrorDetailPolicy.Alwaysinclura le détail (au risque d'exposer des détails à vos utilisateurs)
Rob

Y a-t-il une raison spécifique pour laquelle vous n'avez pas suggéré d'utiliser IAsyncActionFilter à la place?
Ravior

30

Peut-être pas ce que vous cherchiez, mais peut-être agréable à savoir:

Si vous utilisez .net Web Api 2, procédez comme suit:

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

En fonction des erreurs du modèle, vous obtenez ce résultat:

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

1
Gardez à l'esprit quand j'ai posé cette question Web API 1 vient de sortir, elle a probablement beaucoup évolué depuis :)
CallumVass

Assurez-vous de marquer les propriétés comme facultatives, sinon vous obtiendrez un message générique non utile "Une erreur s'est produite". Message d'erreur.
Bouke

1
Existe-t-il un moyen de changer le message?
saquib adil

29

Comme ceci, par exemple:

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

Cela renverra une réponse comme celle-ci (en supposant JSON, mais même principe de base pour 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."]

Vous pouvez bien sûr construire votre objet / liste d'erreur comme vous le souhaitez, par exemple en ajoutant des noms de champ, des identifiants de champ, etc.

Même s'il s'agit d'un appel Ajax "à sens unique" comme un POST d'une nouvelle entité, vous devez quand même renvoyer quelque chose à l'appelant - quelque chose qui indique si la demande a réussi ou non. Imaginez un site où votre utilisateur ajoutera des informations sur lui-même via une requête AJAX POST. Que faire si les informations qu'ils ont essayé de saisir ne sont pas valides - comment sauront-ils si leur action Enregistrer a réussi ou non?

La meilleure façon de le faire est d'utiliser de bons vieux codes d'état HTTP comme 200 OKet ainsi de suite. De cette façon, votre JavaScript peut gérer correctement les échecs en utilisant les bons rappels (erreur, succès, etc.).

Voici un joli tutoriel sur une version plus avancée de cette méthode, utilisant un ActionFilter et jQuery: http://asp.net/web-api/videos/getting-started/custom-validation


Cela renvoie simplement mon enquiryobjet, cela ne dit pas quelles propriétés sont invalides? Donc, si je laisse CustomerAccountNumbervide, il devrait indiquer le message de validation par défaut (le champ CusomterAccountNumber est requis ..)
CallumVass

Je vois, alors est-ce la manière «correcte» de gérer la validation de modèle?
Cela

Il existe également d'autres moyens de le faire, comme se connecter à la validation jQuery. Voici un bel exemple Microsoft: asp.net/web-api/videos/getting-started/custom-validation
Anders Arpi

Cette méthode et la méthode choisie comme réponse "devraient" être "fonctionnellement identiques, donc cette réponse a la valeur ajoutée de vous montrer comment vous pourriez le faire vous-même sans filtre d'action.
Shaun Wilson

Je devais changer la ligne errors.Add(error.ErrorMessage);pour errors.Add(error.Exception.Message);obtenir ce travail pour moi.
Caltor

9

8

Ou, si vous recherchez une simple collection d'erreurs pour vos applications ... voici ma mise en œuvre de ceci:

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 réponse au message d'erreur ressemblera à ceci:

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

5

Ajouter le code ci-dessous dans le fichier 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

Ici, vous pouvez vérifier pour afficher l'erreur d'état du modèle une par une

 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

J'ai eu un problème d'implémentation du modèle de solution accepté où mon ModelStateFilterretournait toujours false(et par la suite un 400) actionContext.ModelState.IsValidpour certains objets de modèle:

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

J'accepte uniquement JSON, j'ai donc implémenté une classe de classeur de modèle personnalisée:

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

Que j'inscris directement après mon modèle via

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

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.