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?

CallumVass
fonte

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);
        }
    }
}
cuongle
fonte
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."
       ]
   }
}
São Almaas
fonte
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

Anders Arpi
fonte
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

Você pode usar atributos do System.ComponentModel.DataAnnotationsnamespace para definir regras de validação. Consulte Validação do modelo - por Mike Wasson para obter detalhes.

Consulte também o vídeo ASP.NET Web API, Parte 5: Validação Personalizada - Jon Galloway

Outras referências

  1. Faça uma caminhada pelo cliente com WebAPI e WebForms
  2. Como a API da Web ASP.NET vincula mensagens HTTP a modelos de domínio e como trabalhar com formatos de mídia na API da Web.
  3. Dominick Baier - Protegendo APIs Web ASP.NET
  4. Vinculando validação de AngularJS à validação de API da Web ASP.NET
  5. Exibindo Erros ModelState com AngularJS na ASP.NET MVC
  6. Como renderizar erros ao cliente? AngularJS / WebApi ModelState
  7. Validação injetada por dependência na API Web
LCJ
fonte
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"
  ]
}
Sandeep Talabathula
fonte
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);
                };
           });
MayankGaur
fonte
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
        }
        }

}

Debendra Dash
fonte
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);
};
Nick Hermans
fonte
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());
user326608
fonte