December 22, 2024

Bind array of values in ASP.NET Core

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. If you found my content useful please consider a small donation. Any support is greatly appreciated! Cheers  😉

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 😂

View all posts by afivan →