ASP.NET Core Pass Enumerable of objects to Get Action on Controller

Viewed 238

I am trying to define a Controller Action in ASP.NET Core 2.2.

The tricky part is that I prefer this to be a GET endpoint, and the data that it must recieve is a collection of custom objects. Here is my sample code:

[Route("api/example")]
[ApiController]
public class ExampleController : ControllerBase
{
    [HttpGet("getData")]
    [ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetMarketData([FromQuery] MyObject[] queryData)
    {
        return this.Ok(0);
    }
}

public class MyObject
{
    public int A { get; set; }

    public int B { get; set; }
}

I am expecting this to bind to something like

http://localhost/api/example/getData/?queryData=[A=1,B=1],queryData=[A=2,B=2]

However I can't seem to get it to work.

(Sending a request to the URL, does not parse the objects, and I receive an empty array in my controller)

I'm not sure if this is the best way to approach this, and maybe I need to change the place where I bind the data from?

The only thing I care about is being able to recieve an array (or some kind of a collection) of MyObject that I can process and return a response. I would also prefer for this to be a GET request, as, after all, I am trying to query this API and get data from it.

I know I can get it to work with using a [FromBody] attribute, but as far as I know GET requests should not use the body.

Any help is gladly appreciated.

3 Answers

Your GET request must be constructed as follows:

GET: /api/example/getData?queryData[0].A=1&queryData[0].B=2&queryData[1].A=3

Very similar to model binding when using <form>s :)

Your QueryString should look like:

/TestMe?queryData[0].A=1&queryData[0].B=1&queryData[1].A=2&queryData[1].B=2

If your code looks like:

public class MyObject
{
    public int A { get; set; }

    public int B { get; set; }
}

[Route("/TestMe")]
public IActionResult TestMe([FromQuery] MyObject[] queryData)
{ 
    return Json(queryData);
}

Note that [FromQuery] isn't even required.

it's not going to work since there's not a default binder for collection types, you'd have to use a custom binder. I made my own implementation of a generic array model binder, here it goes:

// generic array model binder
public class ArrayModelBinder<TType> : IModelBinder {
    public Task BindModelAsync(ModelBindingContext bindingContext) {
        if (bindingContext.ModelMetadata.IsEnumerableType) {
            var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ToString();
            if (!string.IsNullOrEmpty(value)) {
                var elementType = typeof(TType); 
                var typeConverter = TypeDescriptor.GetConverter(elementType);
                var splittedValues = value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
                var values = splittedValues.Select(t => typeConverter.ConvertFromString(t.Trim())).ToArray();
                var typedValues = Array.CreateInstance(elementType, values.Length);
                values.CopyTo(typedValues, 0);
                bindingContext.Model = typedValues;
                return SuccessBinding(bindingContext, typedValues);
            }
            return SuccessBinding(bindingContext, null);
        }
        return FailedBinding(bindingContext);
    }

    private static Task SuccessBinding(ModelBindingContext bindingContext, Array typedValues) {
        bindingContext.Result = ModelBindingResult.Success(typedValues);
        return Task.CompletedTask;
    }


    private static Task FailedBinding(ModelBindingContext bindingContext) {
        bindingContext.Result = ModelBindingResult.Failed();
        return Task.CompletedTask;
    }

To use it on your Action you'll just have to use this piece of code:


public async Task<IActionResult> GetMarketData([ModelBinder(BinderType = typeof(ArrayModelBinder<object>))] MyObject[] queryData)
{
    return this.Ok(0);
}

I have the source of this implementation and other things in a repo of my own Library feel free to check it out CcLibrary.AspNetCore

Related