Manejar la validación ModelState en ASP.NET Web API


106

Me preguntaba cómo puedo lograr la validación del modelo con ASP.NET Web API. Tengo mi modelo así:

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

Luego tengo una acción Publicar en mi controlador API:

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

¿Cómo agrego if(ModelState.IsValid)y luego manejo el mensaje de error para transmitirlo al usuario?

Respuestas:


186

Para la separación de preocupaciones, le sugiero que use el filtro de acción para la validación del modelo, por lo que no necesita preocuparse mucho de cómo hacer la validación en su 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
Los espacios de nombres necesarios para ello son System.Net.Http, System.Net System.Web.Http.Controllersy System.Web.Http.Filters.
Christopher Stevenson

11
También hay una implementación similar en la página oficial de ASP.NET Web Api: asp.net/web-api/overview/formats-and-model-binding/…
Erik Schierboom

1
Incluso si no pone [ValidationActionFilter] arriba de la API web, todavía llama al código y me da una solicitud incorrecta.
micronyks

1
Vale la pena señalar que la respuesta de error devuelta está controlada por IncludeErrorDetailPolicy . De forma predeterminada, la respuesta a una solicitud remota contiene solo un mensaje genérico de "Se ha producido un error", pero si se configura en, IncludeErrorDetailPolicy.Alwaysse incluirán los detalles (a riesgo de exponer los detalles a los usuarios)
Rob

¿Existe una razón específica por la que no sugirió usar IAsyncActionFilter en su lugar?
Ravior

30

Quizás no sea lo que estabas buscando, pero quizás sea bueno que alguien lo sepa:

Si está utilizando .net Web Api 2, puede hacer lo siguiente:

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

Dependiendo de los errores del modelo, obtendrá este resultado:

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

1
Ten en cuenta que cuando hice esta pregunta, la API web 1 acaba de ser lanzada, probablemente se haya movido mucho desde entonces :)
CallumVass

Asegúrese de marcar las propiedades como opcionales; de lo contrario, obtendrá un mensaje genérico no útil "Se ha producido un error". mensaje de error.
Bouke

1
¿Hay alguna forma de cambiar el mensaje?
saquib adil

28

Así, por ejemplo:

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

Esto devolverá una respuesta como esta (asumiendo JSON, pero el mismo principio 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."]

Por supuesto, puede construir su objeto / lista de error de la forma que desee, por ejemplo, agregando nombres de campo, ID de campo, etc.

Incluso si es una llamada Ajax "unidireccional" como un POST de una nueva entidad, aún debe devolver algo a la persona que llama, algo que indique si la solicitud fue exitosa o no. Imagine un sitio donde su usuario agregará información sobre sí mismo a través de una solicitud POST AJAX. ¿Qué pasa si la información que han intentado ingresar no es válida? ¿Cómo sabrán si su acción Guardar fue exitosa o no?

La mejor manera de hacerlo es usando códigos de estado HTTP buen viejo como 200 OKy así sucesivamente. De esa manera, su JavaScript puede manejar correctamente las fallas utilizando las devoluciones de llamada correctas (error, éxito, etc.).

Aquí hay un buen tutorial sobre una versión más avanzada de este método, usando ActionFilter y jQuery: http://asp.net/web-api/videos/getting-started/custom-validation


Eso solo devuelve mi enquiryobjeto, pero no dice qué propiedades no son válidas. Entonces, si lo dejé CustomerAccountNumbervacío, debería decir el mensaje de validación predeterminado (el campo CusomterAccountNumber es obligatorio ..)
CallumVass

Ya veo, entonces, ¿es esta la forma "correcta" de manejar la validación del modelo? Me parece un poco complicado ...
CallumVass

También hay otras formas de hacerlo, como conectarse con la validación de jQuery. Aquí hay un buen ejemplo de Microsoft: asp.net/web-api/videos/getting-started/custom-validation
Anders Arpi

Este método y el método elegido como respuesta "deberían ser" funcionalmente idénticos, por lo que esta respuesta tiene el valor agregado de mostrarle cómo podría hacerlo usted mismo sin un filtro de acción.
Shaun Wilson

Tuve que cambiar la línea errors.Add(error.ErrorMessage);para errors.Add(error.Exception.Message);que esto funcionara para mí.
Caltor

9

8

O, si está buscando una colección simple de errores para sus aplicaciones ... aquí está mi implementación de esto:

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 respuesta del mensaje de error se verá así:

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

5

Agregue el siguiente código en el archivo 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

Aquí puede verificar para mostrar el error de estado del modelo uno por 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

Tuve un problema al implementar el patrón de solución aceptado donde mi ModelStateFiltersiempre regresaría false(y posteriormente un 400) actionContext.ModelState.IsValidpara ciertos objetos del modelo:

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

Solo acepto JSON, así que implementé una clase de carpeta de modelos personalizada:

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 registro directamente después de mi modelo a través de

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

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.