Lidar com a validação de ModelState na API Web ASP.NET


106

Eu queria saber como posso obter a validação de modelo com ASP.NET Web API. Eu tenho meu modelo assim:

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

Em seguida, tenho uma ação Post em meu controlador de API:

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

Como adiciono if(ModelState.IsValid)e, em seguida, manipulo a mensagem de erro para repassar ao usuário?

Respostas:


186

Para separação de interesses, eu sugiro que você use o filtro de ação para validação de modelo, então você não precisa se preocupar muito em como fazer a validação em seu controlador de 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
Os namespaces necessários para isso são System.Net.Http, System.Net System.Web.Http.Controllerse System.Web.Http.Filters.
Christopher Stevenson

11
Há também uma implementação semelhante na página oficial do ASP.NET Web Api: asp.net/web-api/overview/formats-and-model-binding/…
Erik Schierboom

1
Mesmo se não colocar [ValidationActionFilter] acima da API da web, ele ainda chamará o código e fornecerá uma solicitação incorreta.
micronyks de

1
Vale ressaltar que a resposta de erro retornada é controlada pela IncludeErrorDetailPolicy . Por padrão, a resposta a uma solicitação remota contém apenas uma mensagem genérica "Ocorreu um erro", mas definir isso para IncludeErrorDetailPolicy.Alwaysincluirá os detalhes (correndo o risco de expor os detalhes aos usuários)
Rob

Existe um motivo específico pelo qual você não sugeriu o uso de IAsyncActionFilter?
Ravior

30

Talvez não seja o que você estava procurando, mas talvez seja bom para alguém saber:

Se você estiver usando .net Web Api 2, poderá apenas fazer o seguinte:

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

Dependendo dos erros do modelo, você obtém este resultado:

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

1
Tenha em mente quando fiz esta pergunta Web API 1 acabou de ser lançada, provavelmente mudou muito desde então :)
CallumVass

Certifique-se de marcar as propriedades como opcionais, caso contrário, você obterá um genérico não útil "Ocorreu um erro". mensagem de erro.
Bouke

1
Existe uma maneira de mudar a mensagem?
saquib adil

28

Assim, por exemplo:

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

Isso retornará uma resposta como esta (assumindo JSON, mas o mesmo princípio básico para 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."]

É claro que você pode construir seu objeto / lista de erro da maneira que desejar, por exemplo, adicionando nomes de campo, ID de campo etc.

Mesmo que seja uma chamada Ajax "unidirecional", como um POST de uma nova entidade, você ainda deve retornar algo ao chamador - algo que indica se a solicitação foi bem-sucedida ou não. Imagine um site onde seu usuário adicionará algumas informações sobre si mesmo por meio de uma solicitação AJAX POST. E se as informações que eles tentaram inserir não forem válidas - como eles saberão se a ação Salvar foi bem-sucedida ou não?

A melhor maneira de fazer isso é usando os bons e antigos códigos de status HTTP, como 200 OKe assim por diante. Dessa forma, seu JavaScript pode lidar corretamente com as falhas usando os retornos de chamada corretos (erro, sucesso etc.).

Aqui está um bom tutorial sobre uma versão mais avançada deste método, usando um ActionFilter e jQuery: http://asp.net/web-api/videos/getting-started/custom-validation


Isso apenas retorna meu enquiryobjeto, mas não diz quais propriedades são inválidas? Portanto, se eu deixar em CustomerAccountNumberbranco, deverá ser exibida a mensagem de validação padrão (o campo CusomterAccountNumber é obrigatório ..)
CallumVass

Entendo, então essa é a maneira "correta" de lidar com a validação do modelo? Parece um pouco confuso para mim ..
CallumVass

Existem outras maneiras de fazer isso, como conectar-se com a validação do jQuery. Aqui está um bom exemplo da Microsoft: asp.net/web-api/videos/getting-started/custom-validation
Anders Arpi

Esse método e o método eleito como resposta "deve ser" funcionalmente idênticos, portanto, essa resposta tem o valor agregado de mostrar como você mesmo pode fazer isso sem um filtro de ação.
Shaun Wilson

Eu tive que mudar a linha errors.Add(error.ErrorMessage);para errors.Add(error.Exception.Message);para começar este trabalho para mim.
Caltor

9

8

Ou, se você estiver procurando por uma coleção simples de erros para seus aplicativos ... aqui está minha implementação disso:

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

A resposta da mensagem de erro será semelhante a:

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

5

Adicione o código abaixo no arquivo 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

Aqui você pode verificar para mostrar o erro de estado do modelo um por um

 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

Tive um problema ao implementar o padrão de solução aceito em que meu ModelStateFiltersempre retornaria false(e subsequentemente um 400) actionContext.ModelState.IsValidpara determinados objetos de modelo:

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

Aceito apenas JSON, então implementei uma classe de fichário de modelo personalizado:

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 eu registro diretamente após meu modelo via

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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.