Trimming strings in action parameters in ASP.Net Web API

In standard MVC it's very easy to create a model binder to trim all of the strings passed into action methods. The approach I used for that was from here: http://blogs.taiga.nl/martijn/2011/09/29/custom-model-binders-and-request-validation/. It works well and it trims the properties of models too.

The problem came when I tried to do the same thing for Web API. I added a similar StringTrimmingBinder and registered it as a service using the very non-discoverable API:

var provider = new SimpleModelBinderProvider(
    typeof(string), 
    new StringTrimmingModelBinder());
GlobalConfiguration.Configuration.Services.Insert(
    typeof(ModelBinderProvider), 
    0, 
    provider);

As a side note, I have no idea how you're supposed to find out how to register a model binder without opening up Bing and Googling it.

Anyway, this worked great when the parameters to the action were actually strings but doesn't work with complex types as parameters. Now, I've not really delved into the details of how MVC Web API binding differs from normal MVC as it has always "just worked" - which is great - but is was definitely time to start reading...

From this introduction I got the basics but I still didn't feel that I fully understood. After a bit more looking about I stumbled on this answer on Stack Overflow.

If you are using the [FromBody] attribute then the FormatterParameterBinding will be invoked and a MediaTypeFormatter will be used to construct your model.
The model binders for the URI path and Url (Query Params) will invoke ModelBinderParameterBinding which will pass on to either a IValueProvider or a IModelBinder...
Basically what I took from the introduction and that SO answer was that there is no central object responsible for getting values out of the request. My model is most likely to be rehydrated from the body of an http request by a MediaTypeFormatter whereas my strings are most likely to come from the URI. I didn't want to have to provide custom implementations for all of the possible ways that the values could be retrieved - ActionFilterAttribute to the rescue.

My general idea was to just trim all of the strings I could reasonably do without having to deal with recursively going through complex properties. This suites our API fine as it's quite simple and we don't have nested object structures in our parameters.

public class TrimStringsFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var changes = new Dictionary<string, object>();
        foreach (var arg in actionContext.ActionArguments.Where(a => a.Value != null))
        {
            var type = arg.Value.GetType();
            if (IsEnumerable(type))
            {
                changes[arg.Key] = TrimEnumerable((IEnumerable)arg.Value);
            }
            else if (IsComplexObject(type))
            {
                changes[arg.Key] = TrimObject(arg.Value);
            }
        }
        foreach (var change in changes)
        {
            actionContext.ActionArguments[change.Key] = change.Value;
        }
    }

    private static IEnumerable TrimEnumerable(IEnumerable value)
    {
        var enumerable = value as object[] ?? value.Cast<object>().ToArray();
        return enumerable.OfType<string>().Any() ?
                    enumerable.Cast<string>().Select(s => s == null 
                            ? null 
                            : s.Trim()) 
                    : enumerable.Select(TrimObject);
    }

    private static bool IsEnumerable(Type t)
    {
        return t.IsAssignableFrom(typeof(IEnumerable));
    }

    private static bool IsComplexObject(Type value)
    {
        return value.IsClass && !value.IsArray;
    }

    private static object TrimObject(object argValue)
    {
        if (argValue == null) return null;
        var argType = argValue.GetType(); 
        if (IsEnumerable(argType))
        {
            TrimEnumerable((IEnumerable)argValue);
        }
        var s = argValue as string;
        if (s != null)
        {
            return s.Trim();
        }
        if (!IsComplexObject(argType))
        {
            return argValue;
        }
        var props = argType
                .GetProperties(BindingFlags.Instance | BindingFlags.Public)
                .Where(prop => prop.PropertyType == typeof(string))
                .Where(prop => prop.GetIndexParameters().Length == 0)
                .Where(prop => prop.CanWrite && prop.CanRead);

        foreach (var prop in props)
        {
            var value = (string)prop.GetValue(argValue, null);
            if (value != null)
            {
                value = value.Trim();
                prop.SetValue(argValue, value, null);
            }
        }
        return argValue;
    }
}

All this does is go through all of the arguments passed into the action and trim the strings. It works on IEnumerable, string or all of the readable/writeable properties of an object passed in.

Handling IEnumerable means that it also works on actions which take arrays of information.

It also works on Actions that take values from a variety of sources:

[TrimStringsFilter]
public IHttpActionResult Put(string pageId, string siteId, [FromBody] ContentPageNgEditModel model)
{
   // ContentPageNgEditModel is just a DTO with some string and int properties 
   // Do some stuff.
}

I actually ended up registering this as a global filter (GlobalConfiguration.Configuration.Filters.Add(new TrimStringsFilterAttribute());).

I'd love to know if there's a better way to do this.

Comments

  1. Nice post. It actually helped me to solve an exception in production code. Because the inputted e-mail address wasn't trimmed, the regex to check the e-mail address failed and gave an ArgumentException.

    ReplyDelete

Post a Comment

Popular posts from this blog

Full text search in Entity Framework 6 using command interception

Composing Expressions in C#