Enable data annotation validatotion for Web.Api .Net Core action parameters

The Problem

Data annotation validation attributes perfectly works for model validation but doesn't work at all for the single action parameter. For example:

 [HttpGet("{deviceId}")]
 public async Task<IActionResult> Get(string deviceId, [Required]string deviceName)
 {
            if (!this.ModelState.IsValid)
            {
                return new BadRequestObjectResult(this.ModelState);
            }

            return new OkResult();
 }

In this case device name is empty but property ModelState.IsValid is True, but should be False. This happened because validation attributes intended only for model validation and this logic defined in the default model binder. So this is by design.

The only explanation why this logic should work for action parameters is to have controllers more clean, reduce/remove code duplication (like excessive null checks in all actions, etc).

Solution

To enable validation attributes for parameters all validation logic can be moved to custom filter attribute, like in example below:

 /// <inheritdoc />
 public class ValidateActionParametersAttribute : ActionFilterAttribute
 {
    private const string RequiredAttributeKey = "RequiredAttribute";

    /// <inheritdoc />
    public override void OnActionExecuting(ActionExecutingContext context)
    {
            var descriptor = context.ActionDescriptor as ControllerActionDescriptor;

            if (descriptor != null)
            {
                var parameters = descriptor.MethodInfo.GetParameters();

                CheckParameterRequired(context, parameters);
            }

            base.OnActionExecuting(context);
    }

    private static void CheckParameterRequired(ActionExecutingContext context, IEnumerable<ParameterInfo> parameters)
    {
            foreach (var parameter in parameters)
            {
                if (parameter.CustomAttributes.Any() && parameter.CustomAttributes.Select(item => item.AttributeType
                        .ToString()
                        .Contains(RequiredAttributeKey)).Any())
                {
                    if (!context.ActionArguments.Keys.Contains(parameter.Name))
                    {
                        context.ModelState.AddModelError(parameter.Name, $"Parameter {parameter.Name} is required");
                    }
                }
            }
        }
    }

Than, just update action to use this attribute, like in example below:

 [HttpGet("{deviceId}")]
 [ValidateActionParameters]
 public async Task<IActionResult> Get(string deviceId, [Required]string deviceName)
 {
            if (!this.ModelState.IsValid)
            {
                return new BadRequestObjectResult(this.ModelState);
            }

            return new OkResult();
 }

This approach can be easily improved using Fluent Validation Library.

That is it.

Comments