Using DisplayName attribute for model validation output in ASP.NET MVC Core

Returning DisplayName attribute value in model validation response in MVC Core

Model validation is an important part of request life-cycle and pipeline in MVC Core regardless whether you are building Web API project or ASP.NET MVC web application. Both project templates rely on the common ASP.NET Core platform therefore validation is pretty much the same in both type of projects.

Because Web API and micro services are more popular nowadays, I am going to use Web API sample project to demonstrate how to modify validation response in MVC Web API Core project.

Auto generated validation response

If you are working with MVC Core for some time, you probably noticed that if your model of the action is not valid, your action code is not even reached. Before, you had to use ModelState.IsValid condition check to decide whether to process the request or to return BadRequest response. Now this is not necessary as validation is taken care by the MVC framework. This makes life a lot easier as you do not have to care about model validation.

Let's take sample Person model and see what will be the default invalid model response when we try to post the model JSON from POSTMAN

    public class Person
    {
        [Required]
        [MaxLength(10)]
        [DisplayName("Firts name")]
        public String FirtsName { get; set; }

        [Required]
        [MaxLength(20)]
        [MinLength(5)]
        [DisplayName("Last name")]
        public String LastName { get; set; }

        [DisplayName("Email address")]
        [EmailAddress]
        public String Email { get; set; }

    }
    

Now we'll post some intentionally invalid JSON data to see what will be the output

{
"FirstName": "Some long string here",
    "LastName": "Hi",
    "Email": "this.is!email"
}
    

And we'll get the 400 BadRequest response with the following payload

{
    "Email": [
        "The Email address field is not a valid e-mail address."
    ],
    "LastName": [
        "The field Last name must be a string or array type with a minimum length of '5'."
    ],
    "FirtsName": [
        "The Firts name field is required."
    ]
}
    

Validation Fail

Although it is pretty convenient to relly on, this limits you to have standard validation output which is basically JSON with invalid property names and list of error messages for property returned to invoker of controller action. Also, you can see from the sample above that DisplayName attributes on the model properties do not appar in the output.

In most cases this will be just fine, but in case you need to create a custom response which is not exactly the way MVC Core is producing it you need to give up of auto generated validation response. To do this, first thing you need to do is to switch off SuppressModelStateInvalidFilter option from the DI setup in Startup class.

public void ConfigureServices(IServiceCollection services)
	{
		...
		services.Configure<ApiBehaviorOptions>(options =>
			{
				options.SuppressModelStateInvalidFilter = true;
			});
		...
	}
    

By setting SuppressModelStateInvalidFilter to false, MVC will not take care of the validation response for you and if you do not handle it with your own custom ActionFilterAttribute implementation you will keep getting an empty BadRequest response for every request with invalid model.

And one more thing, not to be forgotten, with this option switched of, you have to put your ModelState.IsValid check in your controller action as you are now taking care of the validation result and handling it, though model binding and actual validation is still taken care of by the MVC Core.

Replacing the property name with DisplayName attribute value in validation response

Since we have our SuppressModelStateInvalidFilter set to false, we can add ActionFilterAttrbute to our project and later on apply it to the action where we want to customize invalid model response.

The problem with generating the exact same response as the default MVC validation response (but replace property name with DisplayName) is that we need to dynamically define properties of our output object. Luckily, we can use ExpandoObject class to build our type in the runtime and then in the end just send it back to the client.

I re-used the code I described in Dictionary to object in C# article where I transform IDictionary instance to a runtime built object type using ExpandoObject and dynamic classes.

    public class DisplayNameValidationFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            if (context.ModelState.ErrorCount > 0)
            {
                var modelType = context.ActionDescriptor.Parameters
                    .FirstOrDefault(p => p.BindingInfo.BindingSource.Id.Equals("Body", StringComparison.InvariantCultureIgnoreCase))?.ParameterType; //Get model type

                var expandoObj = new ExpandoObject();
                var expandoObjCollection = (ICollection<KeyValuePair<String, Object>>)expandoObj; //Cannot convert IEnumrable to ExpandoObject

                var dictionary = context.ModelState.ToDictionary(k => k.Key, v => v.Value)
                    .Where(v => v.Value.ValidationState == ModelValidationState.Invalid)
                    .ToDictionary(
                    k =>
                    {
                        if (modelType != null)
                        {
                            var property = modelType.GetProperties().FirstOrDefault(p => p.Name.Equals(k.Key, StringComparison.InvariantCultureIgnoreCase));
                            if (property != null)
                            {
                                //Try to get the attribute
                                var displayName = property.GetCustomAttributes(typeof(DisplayNameAttribute), true).Cast<DisplayNameAttribute>().SingleOrDefault()?.DisplayName;
                                return displayName ?? property.Name;
                            }
                        }
                        return k.Key; //Nothing found, return original vaidation key
                    },
                    v => v.Value.Errors.Select(e => e.ErrorMessage).ToList() as Object); //Box String collection
                foreach (var keyValuePair in dictionary)
                {
                    expandoObjCollection.Add(keyValuePair);
                }
                dynamic eoDynamic = expandoObj;
                context.Result = new BadRequestObjectResult(eoDynamic);
            }
            base.OnActionExecuting(context);
        }
    }
    

There is a lot of code in the snippet above, so I'll try to break it down to a list of things that it is doing with the ModelState entries to generate custom response:

  • Find type of the model (assuming there is only one model)
  • Try to get the property of the validation entry key
  • Try to get DisplayName of the property
  • If any of the above fail use original validation entry key
  • Add key and error messages to dictionary
  • Build type from dictionary
  • Return BadRequestObjectResult with runtime build type object instance as dynamic

All this logic is necessary to maintain the same message structure, so that your clients, do not need to change any piece of code and be able to read DisplayName instead of property name from the validation result. Let's decorate our controller action with our new ActionFilterAttribute derived class and see what will be the output for the same input payload.

    [Route("api/[controller]")]
    [ApiController]
    public class PersonsController : ControllerBase
    {
        [HttpPost]
        [DisplayNameValidationFilter]
        public void Post([FromBody] Person person)
        {
            if (ModelState.IsValid)
            {
                //Process model
            }
        }
    }
    

Note that we now have to add ModelState.IsValid condition as our action will be always executed since we decided to take care of the validation response by setting SuppressModelStateInvalidFilter to false in the Startup class

Validation Fail Display

{
    "Email address": [
        "The Email address field is not a valid e-mail address."
    ],
    "Last name": [
        "The field Last name must be a string or array type with a minimum length of '5'."
    ],
    "Firts name": [
        "The Firts name field is required."
    ]
}
    
Note

In case you are using JavaScript as your client for the Web API application, you might want to have a look in how to handle this message structure. One of the options is using Object.keys() methods. Reference of this method can be found at Mozilla developer network (MDN) documentation https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys. For browser support of Object.keys method, check ECMAScript references http://kangax.github.io/compat-table/es5/#test-Object.keys

Including both property name display name in the validation response message

The previous example was related to the case where you already have your client setup to deal with default validation message structure, so for that reason we generated same message structure as the default validation response, but this is not always the most convenient validation message structure.

Ideally you would like to have both property name and property display name along with list of errors for that property. This makes implementation of generating the response even more easier as we do not need to generate properties on the runtime with ExpandoObject. I am also not a big fan of creating new files for the small model classes, so for this I wil use anaonymous type generated from the new ActionFilterAttribute implementation.

    public class DetailedValidationFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            if (context.ModelState.ErrorCount > 0)
            {

                List<Object> list = new List<Object>();

                var modelType = context.ActionDescriptor.Parameters
                    .FirstOrDefault(p => p.BindingInfo.BindingSource.Id.Equals("Body", StringComparison.InvariantCultureIgnoreCase))?.ParameterType; //Get model type

                foreach (var e in context.ModelState)
                {
                    var property = modelType.GetProperties().FirstOrDefault(p => p.Name.Equals(e.Key, StringComparison.InvariantCultureIgnoreCase));
                    String propertyName = property != null ? property.Name : e.Key;
                    String displayName = propertyName;
                    if (property != null)
                    {
                        var displayNameAttributeValue = property.GetCustomAttributes(typeof(DisplayNameAttribute), true).Cast<DisplayNameAttribute>().SingleOrDefault()?.DisplayName;
                        displayName = displayNameAttributeValue ?? displayName;
                    }

                    list.Add(new
                    {
                        Property = propertyName,
                        Display= displayName,
                        Errors= e.Value.Errors.Select(r => r.ErrorMessage).ToArray()
                    });

                }

                context.Result = new BadRequestObjectResult(list);
            }

            base.OnActionExecuting(context);
        }
    }

    

Now we also need to apply this new ActionFilterAttribute to controller action instead of the previous one in our PersonsController.

    [Route("api/[controller]")]
    [ApiController]
    public class PersonsController : ControllerBase
    {
        [HttpPost]
        [DetailedValidationFilterAttribute]
        public void Post([FromBody] Person person)
        {
            if (ModelState.IsValid)
            {
                //Process model
            }
        }
    }
    

And finally let's test this new action filter with the same invalid payload and see what is the output.

Validation Fail Details


        "errors": [
            "The Firts name field is required."
        ]
    }
]
    

This model is a lot more useful for the client. If your client is a JavaScript application, it has enough data to change the appearance of the HTML elements associated to the property name, message details and descriptive name of the property which is most likely the label for the HTML input and in that way improve user experience for you web application users.

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article