13

When performing model binding to objects, it seems that the framework will return null if there are type mismatches for any of the object's properties. For instance, consider this simple example:

public class Client
{
    public string Name { get; set; }
    public int Age { get; set; }
    public DateTime RegistrationDate { get; set; }
}

public class ClientController : Controller
{
    [HttpPatch]
    public IActionResult Patch([FromBody]Client client)
    {
        return Ok("Success!");
    }
}

If I submit a value of "asdf" for the Age property in an HTTP request, the entire client parameter will be null in the Patch method, regardless of what's been submitted for the other properties. Same thing for the RegistrationDate property. So when your FromBody argument is null in your controller action, how can you know what errors caused model binding to fail (in this case, which submitted property had the wrong type)?

4
  • You can find some information on that here: learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding Commented Mar 12, 2018 at 21:31
  • Don't quite understand where you're coming from here. Having one invalid post value does not make the entire model null, just that one property it's bound to. The only reason the entire model would be null is if nothing in the post could be bound. Commented Mar 13, 2018 at 15:36
  • 1
    My experience is that, indeed, having one invalid post value will make the entire model null in ASP.NET Core MVC (as opposed to ASP.NET MVC where the behavior is as you described). Try the example yourself and you'll see. Commented Mar 13, 2018 at 15:54
  • I find that usually when this happens, I have made the endpoint's parameter something like [FromRoute] instead of what it should be [FromBody]. :-) Commented Mar 22, 2024 at 19:25

3 Answers 3

24

As you stated, ASP.NET MVC core has changed the way MVC API handles model binding by default. You can use the current ModelState to see which items failed and for what reason.

[HttpPatch]
[Route("Test")]
public IActionResult PostFakeObject([FromBody]Test test)
{
    foreach (var modelState in ViewData.ModelState.Values)
    {
        foreach (var error in modelState.Errors)
        {
            //Error details listed in var error
        }
    }
    return null;
}

The exception stored within the error message will state something like the following:

Exception = {Newtonsoft.Json.JsonReaderException: Could not convert string to integer: pie. Path 'age', line 1, position 28. at Newtonsoft.Json.JsonReader.ReadInt32String(String s) at Newtonsoft.Json.JsonTextReader.FinishReadQuotedNumber(ReadType readType) ...

However, as posted in the comments above, the Microsoft docs explains the following:

If binding fails, MVC doesn't throw an error. Every action which accepts user input should check the ModelState.IsValid property.

Note: Each entry in the controller's ModelState property is a ModelStateEntry containing an Errors property. It's rarely necessary to query this collection yourself. Use ModelState.IsValid instead. https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding

Sign up to request clarification or add additional context in comments.

1 Comment

However, in the system I'm working on at the moment, ModelState.Values was always empty when there were model binding errors. In that system, we were using a custom controller factory, but weirdly enough, when we go back to using the default controller factory, then we start to actually find entries in ModelState.Values when we have model binding errors. So I think we'll just switch to using the default controller factory. And anyway, with the new built-in dependency injection system, I think it does everything (or almost everything!) we want. So anyway, thanks!
2

Just a note, you can write the same logic with

var errors = ViewData.ModelState.Values.SelectMany(x => x.Errors );
foreach(var err in errors) { 
    // Error details listed in err (type is Microsoft.AspNetCore.Mvc.ModelBinding.ModelError)
}

and it's more compact and efficient than the nested foreach loops in the answer by PaulBinder.

5 Comments

So how do you get actual useful information here? If I write err or err.ToString() to the debug/console window it just returns the type (Microsoft.AspNetCore.Mvc.ModelBinding.ModelError) instead of the actual error.
That's not what happens for me. I get a string error message. Then I log it inside the foreach: Logger.LogError("MyFunc - error {err}", err); The fact that you're getting a type means you have to serialize the result to a string before it's useful. So, something like this: JsonSerializer.Serialize(err); Good luck!
foreach(var err in errors) { Logger.LogError("MyFunc - error {err}", JsonSerializer.Serialize(err)); }
I'll give you the benefit of the doubt and remove my downvote since I can't easily test that anymore, although I am fairly certain I did try enumerating the value and the other normal requirements for getting values from lists/arrays of things in C#. I don't see what serialization would have to do with anything since there's no JSON at play here. Microsoft.AspNetCore.Mvc.ModelBinding.ModelError is not a JSON object nor does it necessarily contain JSON data.
Thanks. Serialization can be done on all sorts of objects. The output of serialization is usually a JSON string. The benefit of serializing is that you can then see the contents of the object instead of just the type. At least that has been my experience.
0

well, when ModelState.IsValid is false, you can extract detailed model binding errors like this: if you want to return a structured list of binding errors in the response:

if (!ModelState.IsValid)
{
    var errors = ModelState
        .Where(x => x.Value.Errors.Count > 0)
        .Select(x => new 
        { 
            Property = x.Key, 
            Errors = x.Value.Errors.Select(e => e.ErrorMessage) 
        })
        .ToList();
    return BadRequest(errors);
}

or if you want to log the errors:

var errors = ModelState.Values.SelectMany(v => v.Errors);
foreach (var error in errors)
{
    Logger.LogError("Model binding error: {Error}", JsonSerializer.Serialize(error));
}

using Jsonserializer.Serialiazer() ensures that the actual error message (not just the object type) is logged

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.