zoran djipanov
zoran djipanov

Reputation: 135

passing an array to a asp net core web api action method HttpGet

I am trying to send an array of integers to my action method the code looks like so:

[HttpGet]
    public async Task<IActionResult> ServicesByCategoryIds([FromQuery] int[] ids)
    {
        var services = await _accountsUow.GetServiceProfilesByCategoryIdsAsync(ids);
        return Ok(services);
    }

I call the method like so: https://localhost:44343/api/accounts/servicesbycategoryids?ids=1&ids=2

but always get en empty array when I call this method even tho I pass the ids in the query string. I am using .net core 2.1.

everything I have googled suggests that this is in fact the way this is done. . . is there something I am missing here?

Thank you!

Upvotes: 13

Views: 31950

Answers (6)

Nicholas Petersen
Nicholas Petersen

Reputation: 9558

The answer is that simply decorating the array with the [FromQuery] attribute is all that's needed to make the binding work. Without that attribute it fails to bind. That's it, and @kennyzx's answer above is best, but I feel like the point needed to be as simply stated as this: [FromQuery] is all you need. I don't know why these other answers went the ModelBinder route, maybe that is needed for some scenarios, but in my case and I'm sure with many others, the key was to not forget to apply the [FromQuery] attribute.

public ActionResult GetFoo(int id, [FromQuery] Guid[] someIds) { ... }

Upvotes: 0

yshehab
yshehab

Reputation: 372

A slight variation on Plamen's answer.

  • Arrays seem to have an empty GenericTypeArguments so added GetElementType()
  • Renamed class to avoid clashing with the framework class ArrayModelBinder.
  • Added a check on the element type as it's required.
  • More options for surrounding the array with brackets.
public class CustomArrayModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (!bindingContext.ModelMetadata.IsEnumerableType)
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return Task.CompletedTask;
        }

        var value = bindingContext.ValueProvider
            .GetValue(bindingContext.ModelName)
            .ToString();

        if (string.IsNullOrWhiteSpace(value))
        {
            bindingContext.Result = ModelBindingResult.Success(null);
            return Task.CompletedTask;
        }

        var elementType = bindingContext.ModelType.GetElementType() ??
            bindingContext.ModelType.GetTypeInfo().GenericTypeArguments.FirstOrDefault();

        if (elementType == null)
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return Task.CompletedTask;
        }

        var converter = TypeDescriptor.GetConverter(elementType);

        var values = value.Split(',', StringSplitOptions.RemoveEmptyEntries)
            .Select(x => converter.ConvertFromString(Clean(x)))
            .ToArray();

        var typedValues = Array.CreateInstance(elementType, values.Length);
        values.CopyTo(typedValues, 0);
        bindingContext.Model = typedValues;

        bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
        return Task.CompletedTask;
    }

    private static string Clean(string str)
    {
        return str.Trim('(', ')').Trim('[', ']').Trim();
    }
}

Then use with an IEnumerable<T>, IList<T> or array T[]

[ModelBinder(BinderType = typeof(CustomArrayModelBinder))] IEnumerable<T> ids
                                                       ... T[] ids
                                                       ... IList<T> ids

The parameter could be in path or query with optional brackets.

[Route("resources/{ids}")]

resource/ids/1,2,3
resource/ids/(1,2,3)
resource/ids/[1,2,3]

[Route("resources")]

resource?ids=1,2,3
resource?ids=(1,2,3)
resource?ids=[1,2,3]

Upvotes: 7

Plamen Yovchev
Plamen Yovchev

Reputation: 63

You can implement custom model binder and the ids to be part of the URI, not in the query string.

Your endpoint could look like this: /api/accounts/servicesbycategoryids/(1,2)

public class ArrayModelBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        // Our binder works only on enumerable types
        if (!bindingContext.ModelMetadata.IsEnumerableType)
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return Task.CompletedTask;
        }

        // Get the inputted value through the value provider
        var value = bindingContext.ValueProvider
            .GetValue(bindingContext.ModelName).ToString();

        // If that value is null or whitespace, we return null
        if (string.IsNullOrWhiteSpace(value))
        {
            bindingContext.Result = ModelBindingResult.Success(null);
            return Task.CompletedTask;
        }

        // The value isn't null or whitespace,
        // and the type of the model is enumerable.
        // Get the enumerable's type, and a converter
        var elementType = bindingContext.ModelType.GetTypeInfo().GenericTypeArguments[0];
        var converter = TypeDescriptor.GetConverter(elementType);

        // Convert each item in the value list to the enumerable type
        var values = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
            .Select(x => converter.ConvertFromString(x.Trim()))
            .ToArray();

        // Create an array of that type, and set it as the Model value
        var typedValues = Array.CreateInstance(elementType, values.Length);
        values.CopyTo(typedValues, 0);
        bindingContext.Model = typedValues;

        // return a successful result, passing in the Model
        bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
        return Task.CompletedTask;
    }
}

Then use it in your action:

[HttpGet("({ids})", Name="GetAuthorCollection")]
public IActionResult GetAuthorCollection(
    [ModelBinder(BinderType = typeof(ArrayModelBinder))] IEnumerable<int> ids)
{
    //enter code here
}

Learned this from a pluralsight course: Building RESTful API with ASP.NET Core

Upvotes: 4

zmechanic
zmechanic

Reputation: 1990

Rather than [ab]using query string (consider 1000s of IDs), you can use [FromBody] instead, and pass list of IDs as a JSON array:

public IActionResult ServicesByCategoryIds([FromBody] int[] ids)

As long as OpenAPI/Swagger is concerned, a proper specification will be generated:

    "parameters": [
      {
        "name": "ids",
        "in": "body",
        "required": true,
        "schema": {
          "type": "array",
          "items": {
            "type": "integer",
            "format": "int32"
          }
        }
      }
    ],

enter image description here

Upvotes: -2

Edward
Edward

Reputation: 29976

Binding failed for Array parameter is a known issue under Asp.Net Core 2.1 which has been recorded Array or List in query string does not get parsed #7712.

For a tempory workaround, you could set the FromQuery Name Property like below:

        [HttpGet()]
    [Route("ServicesByCategoryIds")]
    public async Task<IActionResult> ServicesByCategoryIds([FromQuery(Name = "ids")]int[] ids)
    {            
        return Ok();
    }

Upvotes: 15

kennyzx
kennyzx

Reputation: 12993

I create a new web api class, with only one action.

[Produces("application/json")]
[Route("api/accounts")]
public class AccountsController : Controller
{
    [HttpGet]
    [Route("servicesbycategoryids")]
    public IActionResult ServicesByCategoryIds([FromQuery] int[] ids)
    {
        return Ok();
    }
}

Then use the same url as yours:

http://localhost:2443/api/accounts/servicesbycategoryids?ids=1&ids=2

It is working.

Upvotes: 5

Related Questions