I have asp.net web api rest service where I need to pass an array of integers. How can this be done in asp.net 4 web api.

public IEnumerable<Category> GetCategories(int[] categoryIds){
// code to retrieve categories from database
}

Url to access the above service

/Categories?categoryids=1,2,3,4
share|improve this question
1  
I was getting a "Can't bind multiple parameters to the request's content" error when using a querystring like "/Categories?categoryids=1&categoryids=2&categoryids=3". Hope this brings people here who were getting this same error. – Josh Noe Apr 1 '14 at 18:33
    
@Josh Did you use [FromUri] though? public IEnumerable<Category> GetCategories([FromUri] int[] categoryids){...} – Anup Kattel Apr 14 '15 at 4:41
1  
@FrankGorman No, I wasn't, which was my issue. – Josh Noe Apr 14 '15 at 19:49

You just need to add [FromUri] before parameter, looks like:

GetCategories([FromUri] int[] categoryIds)

And send request:

/Categories?categoryids=1&categoryids=2&categoryids=3 
share|improve this answer
1  
What if I don't know how much variables I have in the array? What if it's like 1000? The request shouldn't be like that. – Sahar Ch. May 30 '14 at 8:22
3  
This gives me an error "An item with the same key has already been added.". It does however accept categoryids[0]=1&categoryids[1]=2& etc... – Doctor Jones Jul 11 '14 at 14:03
4  
This should be the accepted answer - @Hemanshu Bhojak: isn't it about time to take your pick? – David Rettenbacher Mar 25 '15 at 14:21
6  
This reason for this is due to the following statement from the ASP.NET Web API website talking about parameter binding: "If the parameter is a “simple” type, Web API tries to get the value from the URI. Simple types include the .NET primitive types (int, bool, double, and so forth), plus TimeSpan, DateTime, Guid, decimal, and string, plus any type with a type converter that can convert from a string." an int[] is not a simple type. – Tr1stan Aug 6 '15 at 13:03
2  
This works well for me. One point. On the server code, the array parameter has to come first for it to work and any other parameters, after. When feeding in the parameters in the request, the order is unimportant. – Sparked Apr 23 at 13:47

As Filip W points out, you might have to resort to a custom model binder like this (modified to bind to actual type of param):

public IEnumerable<Category> GetCategories([ModelBinder(typeof(CommaDelimitedArrayModelBinder))]long[] categoryIds) 
{
    // do your thing
}

public class CommaDelimitedArrayModelBinder : IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        var key = bindingContext.ModelName;
        var val = bindingContext.ValueProvider.GetValue(key);
        if (val != null)
        {
            var s = val.AttemptedValue;
            if (s != null)
            {
                var elementType = bindingContext.ModelType.GetElementType();
                var converter = TypeDescriptor.GetConverter(elementType);
                var values = Array.ConvertAll(s.Split(new[] { ","},StringSplitOptions.RemoveEmptyEntries),
                    x => { return converter.ConvertFromString(x != null ? x.Trim() : x); });

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

                values.CopyTo(typedValues, 0);

                bindingContext.Model = typedValues;
            }
            else
            {
                // change this line to null if you prefer nulls to empty arrays 
                bindingContext.Model = Array.CreateInstance(bindingContext.ModelType.GetElementType(), 0);
            }
            return true;
        }
        return false;
    }
}

And then you can say:

/Categories?categoryids=1,2,3,4 and ASP.NET Web API will correctly bind your categoryIds array.

share|improve this answer
    
This may violate SRP and/or SoC, but you can easily make this also inherit from ModelBinderAttribute so it can be used directly instead of the laborious syntax using the typeof() argument. All you have to do is inherit like so: CommaDelimitedArrayModelBinder : ModelBinderAttribute, IModelBinder and then provide a default constructor that pushes the type definition down to the base class: public CommaDelimitedArrayModelBinder() : base(typeof(CommaDelimitedArrayModelBinder)) { }. – sliderhouserules Jan 11 at 22:49
    
Otherwise, I really like this solution and am using it in my project, so... thanks. :) – sliderhouserules Jan 11 at 22:50
    
Aa a side note, this solution doesn't work with generics like System.Collections.Generic.List<long> as bindingContext.ModelType.GetElementType() only support System.Array types – ViRuSTriNiTy Jan 12 at 10:02
    
@ViRuSTriNiTy: This question and the answer specifically talk about Arrays. If you need a generic list based solution, that's fairly trivial to implement. Feel free to raise a separate question if you're not sure how to go about that. – Mrchief Jan 12 at 17:06
    
If you're using model binding, you're thinking MVC and not WebApi. WebApi gracefully handles POST calls with json data, allows for passing complex objects with deeply nested properties while worrying about neither query parameters nor html encoding. – codeMonkey Jun 10 at 15:59

I recently came across this requirement myself, and I decided to implement an ActionFilter to handle this.

public class ArrayInputAttribute : ActionFilterAttribute
{
    private readonly string _parameterName;

    public ArrayInputAttribute(string parameterName)
    {
        _parameterName = parameterName;
        Separator = ',';
    }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (actionContext.ActionArguments.ContainsKey(_parameterName))
        {
            string parameters = string.Empty;
            if (actionContext.ControllerContext.RouteData.Values.ContainsKey(_parameterName))
                parameters = (string) actionContext.ControllerContext.RouteData.Values[_parameterName];
            else if (actionContext.ControllerContext.Request.RequestUri.ParseQueryString()[_parameterName] != null)
                parameters = actionContext.ControllerContext.Request.RequestUri.ParseQueryString()[_parameterName];

            actionContext.ActionArguments[_parameterName] = parameters.Split(Separator).Select(int.Parse).ToArray();
        }
    }

    public char Separator { get; set; }
}

I am applying it like so (note that I used 'id', not 'ids', as that is how it is specified in my route):

[ArrayInput("id", Separator = ';')]
public IEnumerable<Measure> Get(int[] id)
{
    return id.Select(i => GetData(i));
}

And the public url would be:

/api/Data/1;2;3;4

You may have to refactor this to meet your specific needs.

share|improve this answer
    
type int are hardcoded (int.Parse) in your solution . Imho, @Mrchief's solution is better – razon Jun 19 '15 at 10:21

In case someone would need - to achieve same or similar thing(like delete) via POST instead of FromUri, use FromBody and on client side(JS/jQuery) format param as $.param({ '': categoryids }, true)

c#:

public IHttpActionResult Remove([FromBody] int[] categoryIds)

jQuery:

$.ajax({
        type: 'POST',
        data: $.param({ '': categoryids }, true),
        url: url,
//...
});

The thing with $.param({ '': categoryids }, true) is that it .net will expect post body to contain urlencoded value like =1&=2&=3 without parameter name, and without brackets.

share|improve this answer
1  
No need to resort to a POST. See @Lavel answer. – Werlang Aug 5 '15 at 19:03

You may try this code for you to take comma separated values / an array of values to get back a JSON from webAPI

 public class CategoryController : ApiController
 {
     public List<Category> Get(String categoryIDs)
     {
         List<Category> categoryRepo = new List<Category>();

         String[] idRepo = categoryIDs.Split(',');

         foreach (var id in idRepo)
         {
             categoryRepo.Add(new Category()
             {
                 CategoryID = id,
                 CategoryName = String.Format("Category_{0}", id)
             });
         }
         return categoryRepo;
     }
 }

 public class Category
 {
     public String CategoryID { get; set; }
     public String CategoryName { get; set; }
 } 

Output :

[
{"CategoryID":"4","CategoryName":"Category_4"}, 
{"CategoryID":"5","CategoryName":"Category_5"}, 
{"CategoryID":"3","CategoryName":"Category_3"} 
]
share|improve this answer
public class ArrayInputAttribute : ActionFilterAttribute
{
    private readonly string[] _ParameterNames;
    /// <summary>
    /// 
    /// </summary>
    public string Separator { get; set; }
    /// <summary>
    /// cons
    /// </summary>
    /// <param name="parameterName"></param>
    public ArrayInputAttribute(params string[] parameterName)
    {
        _ParameterNames = parameterName;
        Separator = ",";
    }

    /// <summary>
    /// 
    /// </summary>
    public void ProcessArrayInput(HttpActionContext actionContext, string parameterName)
    {
        if (actionContext.ActionArguments.ContainsKey(parameterName))
        {
            var parameterDescriptor = actionContext.ActionDescriptor.GetParameters().FirstOrDefault(p => p.ParameterName == parameterName);
            if (parameterDescriptor != null && parameterDescriptor.ParameterType.IsArray)
            {
                var type = parameterDescriptor.ParameterType.GetElementType();
                var parameters = String.Empty;
                if (actionContext.ControllerContext.RouteData.Values.ContainsKey(parameterName))
                {
                    parameters = (string)actionContext.ControllerContext.RouteData.Values[parameterName];
                }
                else
                {
                    var queryString = actionContext.ControllerContext.Request.RequestUri.ParseQueryString();
                    if (queryString[parameterName] != null)
                    {
                        parameters = queryString[parameterName];
                    }
                }

                var values = parameters.Split(new[] { Separator }, StringSplitOptions.RemoveEmptyEntries)
                    .Select(TypeDescriptor.GetConverter(type).ConvertFromString).ToArray();
                var typedValues = Array.CreateInstance(type, values.Length);
                values.CopyTo(typedValues, 0);
                actionContext.ActionArguments[parameterName] = typedValues;
            }
        }
    }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        _ParameterNames.ForEach(parameterName => ProcessArrayInput(actionContext, parameterName));
    }
}

Usage:

    [HttpDelete]
    [ArrayInput("tagIDs")]
    [Route("api/v1/files/{fileID}/tags/{tagIDs}")]
    public HttpResponseMessage RemoveFileTags(Guid fileID, Guid[] tagIDs)
    {
        _FileRepository.RemoveFileTags(fileID, tagIDs);
        return Request.CreateResponse(HttpStatusCode.OK);
    }

Request uri

http://localhost/api/v1/files/2a9937c7-8201-59b7-bc8d-11a9178895d0/tags/BBA5CD5D-F07D-47A9-8DEE-D19F5FA65F63,BBA5CD5D-F07D-47A9-8DEE-D19F5FA65F63
share|improve this answer
    
can you explain your code? – Sahar Ch. Jul 16 '14 at 8:31
    
@Elsa Could you please point out which piece you can't understand? I think the code is quite clear to explanation it self. It's hard for me to explain this all in English, sorry. – Waninlezu Jul 17 '14 at 1:41
    
@Steve Czetty here's my reconstructed version, thanks for your idea – Waninlezu Jul 17 '14 at 1:46
    
Will it work with / as the seperator? Then you could have: dns/root/mystuff/path/to/some/resource mapped to public string GetMyStuff(params string[] pathBits) – RoboJ1M Oct 20 '14 at 9:01

If you want to list/ array of integers easiest way to do this is accept the comma(,) separated list of string and convert it to list of integers.Do not forgot to mention [FromUri] attriubte.your url look like:

...?ID=71&accountID=1,2,3,289,56

public HttpResponseMessage test([FromUri]int ID, [FromUri]string accountID)
{
    List<int> accountIdList = new List<int>();
    string[] arrAccountId = accountId.Split(new char[] { ',' });
    for (var i = 0; i < arrAccountId.Length; i++)
    {
        try
        {
           accountIdList.Add(Int32.Parse(arrAccountId[i]));
        }
        catch (Exception)
        {
        }
    }
}
share|improve this answer
    
why do you use List<string> instead of just string? it will only have one string in it which is 1,2,3,289,56 in your example. I will suggest an edit. – Daniël Tulp May 20 at 14:20
    
Worked for me. I was surprised my controller wouldn't bind to a List<Guid> automatically though. Note in Asp.net Core the annotation is [FromQuery], and it is not needed. – kitsu.eb Jun 28 at 17:56

I addressed this issue this way.

I used a post message to the api to send the list of integers as data.

Then I returned the data as an ienumerable.

The sending code is as follows:

public override IEnumerable<Contact> Fill(IEnumerable<int> ids)
{
    IEnumerable<Contact> result = null;
    if (ids!=null&&ids.Count()>0)
    {
        try
        {
            using (var client = new HttpClient())
            {
                client.BaseAddress = new Uri("http://localhost:49520/");
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                String _endPoint = "api/" + typeof(Contact).Name + "/ListArray";

                HttpResponseMessage response = client.PostAsJsonAsync<IEnumerable<int>>(_endPoint, ids).Result;
                response.EnsureSuccessStatusCode();
                if (response.IsSuccessStatusCode)
                {
                    result = JsonConvert.DeserializeObject<IEnumerable<Contact>>(response.Content.ReadAsStringAsync().Result);
                }

            }

        }
        catch (Exception)
        {

        }
    }
    return result;
}

The receiving code is as follows:

// POST api/<controller>
[HttpPost]
[ActionName("ListArray")]
public IEnumerable<Contact> Post([FromBody]IEnumerable<int> ids)
{
    IEnumerable<Contact> result = null;
    if (ids != null && ids.Count() > 0)
    {
        return contactRepository.Fill(ids);
    }
    return result;
}

It works just fine for one record or many records. The fill is an overloaded method using DapperExtensions:

public override IEnumerable<Contact> Fill(IEnumerable<int> ids)
{
    IEnumerable<Contact> result = null;
    if (ids != null && ids.Count() > 0)
    {
        using (IDbConnection dbConnection = ConnectionProvider.OpenConnection())
        {
            dbConnection.Open();
            var predicate = Predicates.Field<Contact>(f => f.id, Operator.Eq, ids);
            result = dbConnection.GetList<Contact>(predicate);
            dbConnection.Close();
        }
    }
    return result;
}

This allows you to fetch data from a composite table (the id list), and then return the records you are really interested in from the target table.

You could do the same with a view, but this gives you a little more control and flexibility.

In addition, the details of what you are seeking from the database are not shown in the query string. You also do not have to convert from a csv file.

You have to keep in mind when using any tool like the web api 2.x interface is that the get, put, post, delete, head, etc., functions have a general use, but are not restricted to that use.

So, while post is generally used in a create context in the web api interface, it is not restricted to that use. It is a regular html call that can be used for any purpose permitted by html practice.

In addition, the details of what is going on are hidden from those "prying eyes" we hear so much about these days.

The flexibility in naming conventions in the web api 2.x interface and use of regular web calling means you send a call to the web api that misleads snoopers into thinking you are really doing something else. You can use "POST" to really retrieve data, for example.

share|improve this answer

Make the method type [HttpPost], create a model that has one int[] parameter, and post with json:

/* Model */
public class CategoryRequestModel 
{
    public int[] Categories { get; set; }
}

/* WebApi */
[HttpPost]
public HttpResponseMessage GetCategories(CategoryRequestModel model)
{
    HttpResponseMessage resp = null;

    try
    {
        var categories = //your code to get categories

        resp = Request.CreateResponse(HttpStatusCode.OK, categories);

    }
    catch(Exception ex)
    {
        resp = Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex);
    }

    return resp;
}

/* jQuery */
var ajaxSettings = {
    type: 'POST',
    url: '/Categories',
    data: JSON.serialize({Categories: [1,2,3,4]}),
    contentType: 'application/json',
    success: function(data, textStatus, jqXHR)
    {
        //get categories from data
    }
};

$.ajax(ajaxSettings);
share|improve this answer
    
You're wrapping your array in a class - that will work fine (MVC/WebAPI notwithstanding). The OP was about binding to array without a wrapper class. – Mrchief Jun 10 at 16:02
    
The original problem doesn't say anything about doing it without a wrapper class, just that they wanted to use query params for complex objects. If you go down that path too far you'll get to a point where you need the API to pick up a really complex js object, and query param's will fail you. Might as well learn to do it the way that will work every time. – codeMonkey Jun 10 at 16:05
    
public IEnumerable<Category> GetCategories(int[] categoryIds){ - yeah you could interpret in different ways I suppose. But many a times, I do not want to create wrapper classes for the sake of creating wrappers. If you have complex objects, then that will just work. Supporting these simpler cases is what doesn't work out of the box, hence the OP. – Mrchief Jun 10 at 16:08

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.