113

I was wondering how I can achieve model validation with ASP.NET Web API. I have my model like so:

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

I then have a Post action in my API Controller:

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

How do I add if(ModelState.IsValid) and then handle the error message to pass down to the user?

sashoalm
  • 75,001
  • 122
  • 434
  • 781
CallumVass
  • 11,288
  • 26
  • 84
  • 154

11 Answers11

193

For separation of concern, I would suggest you use action filter for model validation, so you don't need to care much how to do validation in your api controller:

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);
        }
    }
}
Shaun Wilson
  • 8,727
  • 3
  • 50
  • 48
cuongle
  • 74,024
  • 28
  • 151
  • 206
  • 27
    The namespaces needed for this are `System.Net.Http`, `System.Net` `System.Web.Http.Controllers`, and `System.Web.Http.Filters`. – Christopher Stevenson Jan 15 '13 at 22:38
  • 12
    There is also a similar implementation at the official ASP.NET Web Api page: http://www.asp.net/web-api/overview/formats-and-model-binding/model-validation-in-aspnet-web-api – Erik Schierboom Jul 04 '13 at 14:08
  • 2
    Even if don't put [ValidationActionFilter] above web api, it still calls the code and give me bad request. – micronyks May 26 '15 at 14:48
  • 3
    It's worth pointing out that the error response returned is controlled by the [IncludeErrorDetailPolicy](https://msdn.microsoft.com/en-us/library/system.web.http.httpconfiguration.includeerrordetailpolicy(v=vs.118).aspx). By default the response to a remote request contains only a generic "An error has occurred" message, but setting this to `IncludeErrorDetailPolicy.Always` will include the detail (at the risk of exposing detail to your users) – Rob Nov 15 '16 at 11:15
  • Is there a specific reason why you did not suggest using IAsyncActionFilter instead? – Ravior Aug 11 '20 at 07:37
  • The fact that this requires error details to be turned on for the whole site makes it useless, IMO. – JohnOpincar Sep 13 '21 at 15:35
35

Maybe not what you were looking for, but perhaps nice for someone to know:

If you are using .net Web Api 2 you could just do the following:

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

Depending on the model errors, you get this result:

{
   Message: "The request is invalid."
   ModelState: {
       model.PropertyA: [
            "The PropertyA field is required."
       ],
       model.PropertyB: [
             "The PropertyB field is required."
       ]
   }
}
carlin.scott
  • 6,214
  • 3
  • 30
  • 35
Are Almaas
  • 1,063
  • 12
  • 26
29

Like this, for example:

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

This will return a response like this (assuming JSON, but same basic principle for 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."]

You can of course construct your error object/list any way you like, for example adding field names, field id's etc.

Even if it's a "one way" Ajax call like a POST of a new entity, you should still return something to the caller - something that indicates whether or not the request was successful. Imagine a site where your user will add some info about themselves via an AJAX POST request. What if the information they have tried to entered isn't valid - how will they know if their Save action was successful or not?

The best way to do this is using Good Old HTTP Status Codes like 200 OK and so on. That way your JavaScript can properly handle failures using the correct callbacks (error, success etc).

Here's a nice tutorial on a more advanced version of this method, using an ActionFilter and jQuery: http://asp.net/web-api/videos/getting-started/custom-validation

Shaun Wilson
  • 8,727
  • 3
  • 50
  • 48
Anders Arpi
  • 8,277
  • 3
  • 33
  • 49
  • That just returns my `enquiry` object, it doesn't say which properties are invalid though? So If I left `CustomerAccountNumber` empty, it should say the default validation message (CusomterAccountNumber field is required..) – CallumVass Jul 27 '12 at 11:26
  • I see, so is this the "correct" way of handling Model Validation then? Seems a bit messy to me.. – CallumVass Jul 27 '12 at 11:34
  • There are others ways to do it as well, like hooking up with jQuery validation. Here's a nice Microsoft example: http://www.asp.net/web-api/videos/getting-started/custom-validation – Anders Arpi Jul 27 '12 at 12:27
  • This method and the method elected as the answer "should be" functionally identical, so this answer has the added value of showing you how you might do it yourself without an action filter. – Shaun Wilson Jan 16 '13 at 10:01
  • I had to change the line `errors.Add(error.ErrorMessage);` to `errors.Add(error.Exception.Message);` to get this working for me. – Caltor Apr 26 '17 at 15:04
9

Or, if you are looking for simple collection of errors for your apps.. here is my implementation of this:

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

Error Message Response will look like:

{
  "errors": [
    "Please enter a valid phone number (7+ more digits)",
    "Please enter a valid e-mail address"
  ]
}
sandeep talabathula
  • 3,238
  • 3
  • 29
  • 38
6

Add below code in startup.cs file

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
  • 957
  • 11
  • 22
  • make sure the controller is marked with the attribute [ApiController] for handling as above – Raghav Jun 22 '21 at 13:53
4

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
  • 349
  • 2
  • 3
3

Here you can check to show the model state error one by one

 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
  • 5,334
  • 46
  • 38
2

I had an issue implementing the accepted solution pattern where my ModelStateFilter would always return false (and subsequently a 400) for actionContext.ModelState.IsValid for certain model objects:

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

I only accept JSON, so I implemented a custom model binder class:

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

Which I register directly after my model via

config.BindParameter(typeof(AddressDTO), new AddressModelBinder());
Community
  • 1
  • 1
user326608
  • 2,210
  • 1
  • 26
  • 33
1

You can also throw exceptions as documented here: http://blogs.msdn.com/b/youssefm/archive/2012/06/28/error-handling-in-asp-net-webapi.aspx

Note, to do what that article suggests, remember to include System.Net.Http

Christopher Davies
  • 4,461
  • 2
  • 34
  • 33
1

Put this in the startup.cs file

 services.AddMvc().ConfigureApiBehaviorOptions(options =>
        {
            options.InvalidModelStateResponseFactory = (context) =>
            {
                var errors = context.ModelState.Values.SelectMany(x => x.Errors.Select(p =>p.ErrorMessage)).ToList();                    
                var result = new Response
                {
                    Succeeded = false,
                    ResponseMessage = string.Join(", ",errors)
                };
                return new BadRequestObjectResult(result);
            };
        });
pelume_
  • 213
  • 2
  • 10