9

How do you model bind an array from the URI with GET in ASP.NET Core 1 Web API (implicitly or explicitly)?

In ASP.NET Web API pre Core 1, this worked:

[HttpGet]
public void Method([FromUri] IEnumerable<int> ints) { ... }

How do you do this in ASP.NET Web API Core 1? The docs have nothing.

0

3 Answers 3

18
+100

The FromUriAttribute class combines the FromRouteAttribute and FromQueryAttribute classes. Depending the configuration of your routes / the request being sent, you should be able to replace your attribute with one of those.

However, there is a shim available which will give you the FromUriAttribute class. Install the "Microsoft.AspNet.Mvc.WebApiCompatShim" NuGet package through the package explorer, or add it directly to your project.json file:

"dependencies": {
  "Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-rc1-final"
}

While it is a little old, I've found that this article does a pretty good job of explaining some of the changes.

Binding

If you're looking to bind comma separated values for the array ("/api/values?ints=1,2,3"), you will need a custom binder just as before. This is an adapted version of Mrchief's solution for use in ASP.NET Core.

public class CommaDelimitedArrayModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelMetadata.IsEnumerableType)
        {
            var key = bindingContext.ModelName;
            var value = bindingContext.ValueProvider.GetValue(key).ToString();

            if (!string.IsNullOrWhiteSpace(value))
            {
                var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
                var converter = TypeDescriptor.GetConverter(elementType);

                var values = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
                    .Select(x => converter.ConvertFromString(x.Trim()))
                    .ToArray();

                var typedValues = Array.CreateInstance(elementType, values.Length);

                values.CopyTo(typedValues, 0);
                
                bindingContext.Result = ModelBindingResult.Success(typedValues);
            }
            else
            {
                // change this line to null if you prefer nulls to empty arrays 
                bindingContext.Result = ModelBindingResult.Success(Array.CreateInstance(bindingContext.ModelType.GetElementType(), 0));
            }

            return TaskCache.CompletedTask;
        }

        return TaskCache.CompletedTask;
    }
}

You can either specify the model binder to be used for all collections in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc().AddMvcOptions(opts =>
        {
            opts.ModelBinders.Insert(0, new CommaDelimitedArrayModelBinder());
        });
}

Or specify it once in your API call:

[HttpGet]
public void Method([ModelBinder(BinderType = typeof(CommaDelimitedArrayModelBinder))] IEnumerable<int> ints)
Sign up to request clarification or add additional context in comments.

6 Comments

The question is about implicit array model binding specifically. Using FromQuery or FromRoute in ASP.NET Core 1 does not enable comma separated implicit array model binding for HttpGet (which it does in ASP.NET Web API and ASP.NET MVC).
@bzlm There's no mention of binding comma separated array values in the question. Where are you getting that from?
The word "array" is in the question title ("model binding array"), and the code example has IEnumerable<int> ints as the parameter to the action. :) Edited the question for clarity. Do you know the answer (using FromQuery or not)?
There is no mention of the phrase "comma separated", which is where I'm asking for clarity. A request like /api/values?ints=1&ints=2&ints3 will work without the need for any attribute. A request like /api/values?ints=1,2,3 will need a custom model binder, just as before. I have added a binder to the answer.
This doesnt work anymore, i have tried and tried....and although modelBinder runs, the parameter on the controller says it is null
|
7

ASP.NET Core 1.1 Answer

@WillRay's answer is a little outdated. I have written an 'IModelBinder' and 'IModelBinderProvider'. The first can be used with the [ModelBinder(BinderType = typeof(DelimitedArrayModelBinder))] attribute, while the second can be used to apply the model binder globally as I've show below.

.AddMvc(options =>
{
    // Add to global model binders so you don't need to use the [ModelBinder] attribute.
    var arrayModelBinderProvider = options.ModelBinderProviders.OfType<ArrayModelBinderProvider>().First();
    options.ModelBinderProviders.Insert(
        options.ModelBinderProviders.IndexOf(arrayModelBinderProvider),
        new DelimitedArrayModelBinderProvider());
})

public class DelimitedArrayModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.Metadata.IsEnumerableType && !context.Metadata.ElementMetadata.IsComplexType)
        {
            return new DelimitedArrayModelBinder();
        }

        return null;
    }
}

public class DelimitedArrayModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var modelName = bindingContext.ModelName;
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
        var values = valueProviderResult
            .ToString()
            .Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
        var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];

        if (values.Length == 0)
        {
            bindingContext.Result = ModelBindingResult.Success(Array.CreateInstance(elementType, 0));
        }
        else
        {
            var converter = TypeDescriptor.GetConverter(elementType);
            var typedArray = Array.CreateInstance(elementType, values.Length);

            try
            {
                for (int i = 0; i < values.Length; ++i)
                {
                    var value = values[i];
                    var convertedValue = converter.ConvertFromString(value);
                    typedArray.SetValue(convertedValue, i);
                }
            }
            catch (Exception exception)
            {
                bindingContext.ModelState.TryAddModelError(
                    modelName,
                    exception,
                    bindingContext.ModelMetadata);
            }

            bindingContext.Result = ModelBindingResult.Success(typedArray);
        }

        return Task.CompletedTask;
    }
}

3 Comments

I went ahead and updated my original answer, just to be functional in 1.1. Really like the solution you've created for the provider!
bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0]; throws an exception when binding against string[] using this instead var elementType = bindingContext.ModelType.GetElementType();
Unfortunately this code doesn't allow a single element, only assumes that there is always a delimiter present.
-1

There are some changes in the .NET Core 3.

Microsoft has split out the functionality from the AddMvc method (source).

As AddMvc also includes support for View Controllers, Razor Views and etc. If you don't need to use them in your project (like in an API), you might consider using services.AddControllers() which is for Web API controllers.

So, updated code will look like this:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
            .AddMvcOptions(opt =>
            {
                var mbp = opt.ModelBinderProviders.OfType<ArrayModelBinderProvider>().First();
                    opt.ModelBinderProviders.Insert(opt.ModelBinderProviders.IndexOf(mbp), new DelimitedArrayModelBinderProvider());
            });
}

1 Comment

OP specifically asked for .NET Core 1. Answers in newer versions don't address the question that was asked. Please only share answers that address the question that was asked.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.