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?

CallumVass
fuente

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);
        }
    }
}
cuongle
fuente
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."
       ]
   }
}
Son Almaas
fuente
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

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

Puede utilizar atributos del System.ComponentModel.DataAnnotationsespacio de nombres para establecer reglas de validación. Consulte Validación del modelo: por Mike Wasson para obtener más detalles.

Consulte también el video ASP.NET Web API, Parte 5: Validación personalizada - Jon Galloway

Otras referencias

  1. Dé un paseo por el lado del cliente con WebAPI y WebForms
  2. Cómo ASP.NET Web API enlaza mensajes HTTP a modelos de dominio y cómo trabajar con formatos multimedia en Web API.
  3. Dominick Baier: protección de las API web de ASP.NET
  4. Enganchando la validación de AngularJS a la validación de la API web de ASP.NET
  5. Visualización de errores ModelState con AngularJS en ASP.NET MVC
  6. ¿Cómo presentar errores al cliente? AngularJS / WebApi ModelState
  7. Validación inyectada por dependencias en API web
LCJ
fuente
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"
  ]
}
sandeep talabathula
fuente
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);
                };
           });
MayankGaur
fuente
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
        }
        }

}

Debendra Dash
fuente
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
fuente
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());
user326608
fuente