Comma-separated parameter values in WebAPI

The model binding mechanism in ASP.NET is pretty slick - it’s clever and highly extensible and built on TypeDescriptor system for all sorts of re-use that lets you get out of having to write boilerplate code to map between CLR objects and their web representations.

One surprising thing however is that out of the box neither WebAPI or MVC support comma-separated parameter values when bound to an array, e.g.

public class MyController : Controller {
    public string Page([FromUri]int[] ids) {
        return String.Join(" ; ", ids);
    }
}

Will only return 1 ; 2 ; 3 when supplied with /my/page?ids=1&ids=2&ids=3 and if you instead give it /my/page?ids=1,2,3 it will fail.

The reason for this was likely because there isn’t a standard for this at all and that the former - supported - scenario maps to what forms do when they post multiple value selections such as that in a select list box. The latter however is much more readable and is expected by some client frameworks and supported by some other web frameworks such as the Java Spring MVC framework.

Of course that extensible system lets us easily extend this behavior so that we can support both transparently - and interestingly enough - even mix-and-match on the same URL. So for example;

/my/page?ids=1,2&ids=3 will now return 1 ; 2 ; 3 in our example.

Although this supports both types if you are currently using commas in your number format this would break your app. e.g. ?ids=1,200&ids=3,500 would have been correctly received as 1200, 500 but now would be incorrectly received as 1, 200, 3, 500

CommaSeparatedArrayModelBinder class

The source is available in the DamienGKit project but also here.

Out of the box it supports integer types and Guid’s although you could extend it to floats and decimals – again just be careful with that formatting!

public class CommaSeparatedArrayModelBinder : IModelBinder {
    private static readonly Type[] supportedElementTypes = {
        typeof(int), typeof(long), typeof(short), typeof(byte),
        typeof(uint), typeof(ulong), typeof(ushort), typeof(Guid)
    };

    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) {
        if (!IsSupportedModelType(bindingContext.ModelType)) return false;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        var stringArray = valueProviderResult?.AttemptedValue
            ?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
        if (stringArray == null) return false;
        var elementType = bindingContext.ModelType.GetElementType();
        if (elementType == null) return false;

        bindingContext.Model = CopyAndConvertArray(stringArray, elementType);
        return true;
    }

    private static Array CopyAndConvertArray(IReadOnlyList<string> sourceArray, Type elementType) {
        var targetArray = Array.CreateInstance(elementType, sourceArray.Count);
        if (sourceArray.Count > 0) {
            var converter = TypeDescriptor.GetConverter(elementType);
            for (var i = 0; i < sourceArray.Count; i++)
                targetArray.SetValue(converter.ConvertFromString(sourceArray[i]), i);
        }
        return targetArray;
    }

    internal static bool IsSupportedModelType(Type modelType) {
        return modelType.IsArray && modelType.GetArrayRank() == 1
                && modelType.HasElementType
                && supportedElementTypes.Contains(modelType.GetElementType());
    }

}

public class CommaSeparatedArrayModelBinderProvider : ModelBinderProvider {
    public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType) {
        return CommaSeparatedArrayModelBinder.IsSupportedModelType(modelType)
            ? new CommaSeparatedArrayModelBinder() : null;
    }
}

To register

It’s necessary to register ModelBinderProviders with your ASP.NET application at start-up - usually in the WebApiConfig.cs file.

public static class WebApiConfig {
    public static void Register(HttpConfiguration config) {
        // All your usual configuration up here
        config.Services.Insert(typeof(ModelBinderProvider), 0, new CommaSeparatedArrayModelBinderProvider());
    }
}

[)amien

No responses yet