Reputation: 135
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
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
Reputation: 372
A slight variation on Plamen's answer.
GenericTypeArguments
so added GetElementType()ArrayModelBinder
.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
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
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"
}
}
}
],
Upvotes: -2
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
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