Search code examples
asp.net-web-api2customvalidator

Custom validator for input parameter


I currently have this controller

/// <summary>
/// API methods for working with test
/// </summary>
[RoutePrefix("api/terminal")]
public class TerminalController : ApiController
{
    [HttpGet]
    [Route("{terminalId}/validation")]
    public IHttpActionResult ValidateTerminal([MinUnsignValue(10)] long terminalId)
    {
        return Ok();
    }
}

And custom validator for input parameter

public class MinUnsignValueAttribute : ValidationAttribute
{
    private readonly ulong _minValue;

    public MinUnsignValueAttribute(ulong minValue)
    {
        _minValue = minValue;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        //if value < _minValue return new ValidationResult("false")

        return ValidationResult.Success;
    }
}

When I send 6 the validator ignored and action invoked. It`s caused because web api pipeline consist of IActionFilter[] IAuthenticationFilter[] IAuthorizationFilter[] authorizationFilters IExceptionFilter[].

Is there a way to integrate my custom attribute to the pipeline?


Solution

  • ok, I didn't found an answer. wrote it myself

    public class ParametersFilter : ActionFilterAttribute
    {
        class Wrapper
        {
            public Wrapper(MethodInfo methodInfo, object validationAttributeInstance)
            {
                MethodInfo = methodInfo;
                ValidationAttributeInstance = validationAttributeInstance;
            }
    
            public MethodInfo MethodInfo { get; }
            public object ValidationAttributeInstance { get; }
        }
    
        static readonly ConcurrentDictionary<string, Wrapper> Cache = new ConcurrentDictionary<string, Wrapper>();
        static readonly HashSet<string> Registry = new HashSet<string>();
    
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            if (!Registry.Contains(actionContext.ActionDescriptor.ActionName))
            {
                Registry.Add(actionContext.ActionDescriptor.ActionName);
    
                //first invokation, have to check
                Type controllerType = actionContext.ControllerContext.Controller.GetType();
                foreach (MethodInfo methodInfo in controllerType.GetMethods())
                {
                    if (actionContext.ActionDescriptor.ActionName != methodInfo.Name) continue;
    
                    ParameterInfo[] parameters = methodInfo.GetParameters();
                    if (parameters.Length != actionContext.ActionArguments.Count) continue;
    
                    foreach (ParameterInfo parameterInfo in parameters)
                    {
                        //The primitive types are Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, and Single.
                        if (!parameterInfo.ParameterType.IsPrimitive) continue;
                        if (!actionContext.ActionArguments.ContainsKey(parameterInfo.Name)) continue;
    
                        var customAttributesData = parameterInfo.GetCustomAttributesData();
                        foreach (CustomAttributeData customAttributeData in customAttributesData)
                        {
                            if (!customAttributeData.AttributeType.IsSubclassOf(typeof(ValidationAttribute))) continue;
    
                            var validationAttributeInstance = customAttributeData.Constructor.Invoke(customAttributeData.ConstructorArguments.Select(x => x.Value).ToArray()) as ValidationAttribute;
                            if (validationAttributeInstance == null) continue;
    
                            MethodInfo method = validationAttributeInstance
                                .GetType()
                                .GetMethod("IsValid", BindingFlags.NonPublic | BindingFlags.Instance);
    
                            Cache.TryAdd(
                               $"{actionContext.ActionDescriptor.ActionName}{parameterInfo.Name}",
                               new Wrapper(method, validationAttributeInstance));
                        }
                    }
                }
            }
    
            foreach (var actionArgument in actionContext.ActionArguments)
            {
                Wrapper wrapper;
                if (!Cache.TryGetValue(
                    $"{actionContext.ActionDescriptor.ActionName}{actionArgument.Key}", out wrapper))
                {
                    continue;
                }
    
                object instance = actionArgument.Value;
                var context = new ValidationContext(instance)
                {
                    DisplayName = actionArgument.Key,
                    MemberName = actionArgument.Key
                };
    
                var results = wrapper.MethodInfo.Invoke(wrapper.ValidationAttributeInstance, new[] { instance, context }) as ValidationResult;
                if (results != ValidationResult.Success)
                {
                    var response = new Utils.WebApi.Common.Response.WebResponse
                    {
                        IsSuccess = false,
                        ErrorMessage = results?.ErrorMessage
                    };
    
                    actionContext.Response = new HttpResponseMessage
                    {
                        StatusCode = HttpStatusCode.BadRequest,
                        Content = new ObjectContent(
                            response.GetType(),
                            response,
                            new JsonMediaTypeFormatter())
                    };
                    break;
                }
            }
    
            base.OnActionExecuting(actionContext);
        }
    }