Share this post on:

Hey there! There are often times when you want in a controller action to bind a set of values, more specifically an array. So if you have the following situation:


HttpGet("byIds/{ids}")]
public async Task<IActionResult> GetByIds(int[] ids)

You’ll notice that ids will always be null. This happens because there’n no model binder for arrays in ASP.NET Core. That’s why we are going to build one that works for primitive types separated by commas. If you need more adjustments, it’s then easy to adapt the code to your needs.

The code has been adapted from this blog post to ASP.NET Core 2.2. So let’s get straight to business:


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)
    };
    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 Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (!IsSupportedModelType(bindingContext.ModelType))
        {
            return Task.CompletedTask;
        }

        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        var stringArray = valueProviderResult.Values.FirstOrDefault()
                ?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

        if (stringArray == null)
        {
            return Task.CompletedTask;
        }

        var elementType = bindingContext.ModelType.GetElementType();
        if (elementType == null)
        {
            return Task.CompletedTask;
        }

        bindingContext.Result = ModelBindingResult.Success(CopyAndConvertArray(stringArray, elementType));

        return Task.CompletedTask;
    }
}

I think the implementation for BindModelAsync method is pretty straightforward. The interesting method is the CopyAndConvertArray which uses activators to create an instance of an array based on the requested type but also uses a converter to convert the values of the array from string to the required type. This is actually very useful, because it keeps the code clean without lots of switch cases 😊

Good, having this class ready, we just need a few things to have. First we need the model binder provider, which looks like so:


public class CommaSeparatedArrayModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        return CommaSeparatedArrayModelBinder.IsSupportedModelType(context.Metadata.ModelType)
                ? new CommaSeparatedArrayModelBinder() : null;
    }
}

This class will actually decide whether to use our custom model binder or not. The next step is to register our model binder provider in ConfigureServices method in Startup:


services.AddMvc(options => 
{
     options.ModelBinderProviders.Insert(0, new CommaSeparatedArrayModelBinderProvider());
})

So this is quite about it, now your action should receive the parameter correctly. Hope this is useful for you!

Thanks for reading, I hope you found this article useful and interesting. If you have any suggestions don’t hesitate to contact me. I also invite you to share and subscribe to the newsletter by using the buttons below! Cheers  😉

Share this post on:
Avatar afivan

Author: afivan

Enthusiast adventurer, software developer with a high sense of creativity, discipline and achievement. I like to travel, I like music and outdoor sports. Because I have a broken ligament, I prefer safer activities like running or biking. In a couple of years, my ambition is to become a good technical lead with entrepreneurial mindset. From a personal point of view, I’d like to establish my own family, so I’ll have lots of things to do, there’s never time to get bored 😂

2 Comments

  1. Avatar afivan

    Chris

    There were some errors in the code. Fixed version:

    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)
    };
    private static Array CopyAndConvertArray(string[] sourceArray, Type elementType)
    {
    var targetArray = Array.CreateInstance(elementType, sourceArray.Length);
    if (sourceArray.Length > 0)
    {
    var converter = TypeDescriptor.GetConverter(elementType);
    for (var i = 0; i < sourceArray.Length; 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 Task BindModelAsync(ModelBindingContext bindingContext)
    {
    if (!IsSupportedModelType(bindingContext.ModelType))
    {
    return Task.CompletedTask;
    }

    var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

    var stringArray = valueProviderResult.Values.FirstOrDefault()
    ?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

    if (stringArray == null)
    {
    return Task.CompletedTask;
    }

    var elementType = bindingContext.ModelType.GetElementType();
    if (elementType == null)
    {
    return Task.CompletedTask;
    }

    bindingContext.Result = ModelBindingResult.Success(CopyAndConvertArray(stringArray, elementType));

    return Task.CompletedTask;
    }
    }

Leave a Reply