2

How do I create a custom validation message on a model passed into a controller action method of a c# web API?

Here is the model:

[DataContract]
public class TestDto //: IValidatableObject
{
    [DataMember]
    [LongValidation("Its not a long!")]
    public long? ID { get; set; }
    [DataMember]
    public string Description { get; set; }

    public string DescriptionHidden { get; set; }
}

Here is my controller class:

public class ValuesController : ApiController
{
    // GET api/values
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

    // GET api/values/5
    public string Get(int id)
    {
        return "value";
    }

    // POST api/values
    public string Post([FromBody]TestDto testDto)
    {
        //todo: should a post return any data?
        if (ModelState.IsValid)
            return "success!";
        else
        {
            var ret = string.Empty;
            foreach (var modelState in ModelState)
            {
                ModelErrorCollection errorCollection = modelState.Value.Errors;
                var errors = string.Empty;
                foreach (var error in errorCollection)
                {
                    errors = errors + "exception message: " + error.Exception.Message + ", errorMessage: " + error.ErrorMessage;
                }
                ret = ret + "Error: " + modelState.Key + ", " + modelState.Value.Value + ", errors: " + errors;
            }
            return ret;
        }
    }

    // PUT api/values/5
    public void Put(int id, [FromBody]string value)
    {
    }

    // DELETE api/values/5
    public void Delete(int id)
    {
    }
}

If I post this object into the Post action:

{
"ID" : "1aaa","Description": "sample string 2",
}

In my LongValidation's valid method, I get the default value for a long not : "1aaa", so I cannot perform the correct validation in the validator.

Here is the code for the Long Validator:

public class LongValidationAttribute : ValidationAttribute
{
    public LongValidationAttribute(string errorMessage) : base(errorMessage)
    {
    }

    public override string FormatErrorMessage(string name)
    {
        return base.FormatErrorMessage(name);
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        long ret;
        bool success = long.TryParse(value.ToString(), out ret);
        //return base.IsValid(value, validationContext);
        if (success == false)
            return new ValidationResult(this.ErrorMessage);
        else
        {
            return ValidationResult.Success;
        }
    }
}

3 Answers 3

1

1aaa is not a long value, but a string value. So when you submit the data to the endpoint, the default model binder will not be able to map this string value to your long? property.

If you absolutely want to get this string value mapped to your clas property and get validated, you need to change your property from long? to string type. But ofcourse now, you need to convert it to long in other parts of your code !

I suggest simply use the appropriate numeric type if you are expecting numeric value in normal use case. In your code you can check whether it is null or not and use it as needed.

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

Comments

1

See this post: https://blog.markvincze.com/how-to-validate-action-parameters-with-dataannotation-attributes/

Basically you need to hook into the MVC pipeline with a custom filter attribute:

public class ValidateActionParametersAttribute : ActionFilterAttribute  
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var descriptor = context.ActionDescriptor as ControllerActionDescriptor;

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

            foreach (var parameter in parameters)
            {
                var argument = context.ActionArguments[parameter.Name];

                EvaluateValidationAttributes(parameter, argument, context.ModelState);
            }
        }

        base.OnActionExecuting(context);
    }

    private void EvaluateValidationAttributes(ParameterInfo parameter, object argument, ModelStateDictionary modelState)
    {
        var validationAttributes = parameter.CustomAttributes;

        foreach (var attributeData in validationAttributes)
        {
            var attributeInstance = CustomAttributeExtensions.GetCustomAttribute(parameter, attributeData.AttributeType);

            var validationAttribute = attributeInstance as ValidationAttribute;

            if (validationAttribute != null)
            {
                var isValid = validationAttribute.IsValid(argument);
                if (!isValid)
                {
                    modelState.AddModelError(parameter.Name, validationAttribute.FormatErrorMessage(parameter.Name));
                }
            }
        }
    }
}

1 Comment

Thanks for show-casing the proper way to do things in the given context. The code snippet given works for asp.net core. I have refactored the code in my answer below to help people struggling to achieve the same functionality in mvc4+. Thanks again.
0

Based on Kevin's excellent answer I have created the following class to achieve the desired effect under MVC4 / MVC5:

public sealed class ValidateActionParametersAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var descriptor = context.ActionDescriptor;
        if (descriptor != null)
        {
            var modelState = context.Controller.ViewData.ModelState;
            foreach (var parameterDescriptor in descriptor.GetParameters())
            {
                EvaluateValidationAttributes(
                    suppliedValue: context.ActionParameters[parameterDescriptor.ParameterName],
                    modelState: modelState,
                    parameterDescriptor: parameterDescriptor
                );
            }
        }

        base.OnActionExecuting(context);
    }

    static private void EvaluateValidationAttributes(ParameterDescriptor parameterDescriptor, object suppliedValue, ModelStateDictionary modelState)
    {
        var parameterName = parameterDescriptor.ParameterName;
        parameterDescriptor
            .GetCustomAttributes(inherit: true)
            .OfType<ValidationAttribute>()
            .Where(x => !x.IsValid(suppliedValue))
            .ForEach(x => modelState.AddModelError(parameterName, x.FormatErrorMessage(parameterName)));
    }
}

Note: The .ForEach method used above is a simple extension method which you can write on your own.

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.