Your problem intrigued me so I wanted to come up with a solution that was a bit more generic than the answer Nkosi provided. While Nkosi's answer will work, I'm not a fan of the ModelBinder syntax as well as defining a new ModelBinder for each type. I've been playing around with ParameterBindingAttribute before and really like the syntax so I wanted to start with this. This allows you to define a [FromUri] or [FromBody] like syntax. I also wanted to be able to use different "array" types such as int[] or List or HashSet or best yet, IEnumerable.
Step 1: Create a HttpParameterBinding
Step 2: Create a ParameterBindingAttribute
Step 3: Put it all together
An HttpParameterBinding allows you to parse any RouteData and pass them to your methed by setting the actionContext's ActionArguments dictionary. You simply inherit from HttpParameterBinding and override the ExecuteBindingAsync method. You can throw exception here if you want, but you can also just let it flow through and the method will receive null if it wasn't able to parse the RouteData. For this example, I am creating a JSON string of an array made from the RouteData. Since we know Json.NET is amazing at parsing data types, it seemed natural to use it. This will parse the RouteData for a CSV value. This works best for ints or dates.
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Controllers;
using System.Web.Http.Metadata;
using Newtonsoft.Json;
public class CsvParameterBinding : HttpParameterBinding
{
public CsvParameterBinding(HttpParameterDescriptor descriptor) : base(descriptor)
{
}
public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext, CancellationToken cancellationToken)
{
var paramName = this.Descriptor.ParameterName;
var rawParamemterValue = actionContext.ControllerContext.RouteData.Values[paramName].ToString();
var rawValues = rawParamemterValue.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
//To convert the raw value int a true JSON array we need to make sure everything is quoted.
var jsonString = $"[\"{string.Join("\",\"", rawValues)}\"]";
try
{
var obj = JsonConvert.DeserializeObject(jsonString, this.Descriptor.ParameterType);
actionContext.ActionArguments[paramName] = obj;
}
catch
{
//There was an error casting, the jsonString must be invalid.
//Don't set anything and the action will just receive null.
}
return Task.FromResult<object>(null);
}
}
A ParameterBindingAttribute allows us to use the clean syntax of declaring the binding right in the method signature. I decided I wanted to use [FromUriCsv] as the syntax so the class is named appropriately. The only thing to overrid is the GetBinding method in which we wire up the CsvParameterBinding class we just made.
using System.Web.Http;
using System.Web.Http.Controllers;
public class FromUriCsvAttribute : ParameterBindingAttribute
{
public override HttpParameterBinding GetBinding(HttpParameterDescriptor parameter)
{
return new CsvParameterBinding(parameter);
}
}
Now to put it on the controller and use it.
[Route("WorkPlanList/{clientsId}/{date:datetime}")]
public async Task<IHttpActionResult> WorkPlanList([FromUriCsv] List<int> clientsId, [FromUri] DateTime date)
{
//matches WorkPlanList/2,3,4/7-3-2016
}
[Route("WorkPlanList/{clientsId}")]
public async Task<IHttpActionResult> WorkPlanList([FromUriCsv] HashSet<int> clientsId)
{
//matches WorkPlanList/2,3,4,5,2
//clientsId will only contain 2,3,4,5 since it's a HashSet the extra 2 won't be included.
}
[Route("WorkPlanList/{clientsId}/{dates}")]
public async Task<IHttpActionResult> WorkPlanList([FromUriCsv] IEnumerable<int> clientsId, [FromUriCsv] IEnumerable<DateTime> dates)
{
//matches WorkPlanList/2,3,4/5-2-16,6-17-16,7-3-2016
}
I really liked how this turned it. It works really well for ints and dates, but failed with decimals because the period in the route really jacks it up. For now, this solves your problem very well. If decimal numbers are needed, the routing should be able to be tweaked using regex or mvc style routing. I've used a similar method to this in order to pull complex types out of header values which worked really well with [FromHeader("headerName")]
syntax.