0

Ideally I would like to have an URL in following format:

/api/categories/1,2,3...N/products

And this would return all products for the specified categories. Having one API call with multiple category IDs saves me several database calls, thus improves performance.

I can easily implement this in a following way.

public HttpResponseMessage GetProducts(string categoryIdsCsv)
{
    // <1> Split and parse categoryIdsCsv
    // <2> Get products
}

However, this doesn't look like a clean clean solution, and possibly breaking SRP principle. I also tried using ModelBinder, however it adds parameters to query string.

Questions:

  1. Is there a clean way to implement such URL structure?
  2. Or is there a different/better approach to retrieve all products for multiple categories?

Please let me know if you need any further clarification.

1
  • What's wrong with /api/products?categories[]=1&categories[]=2&...? This is kind of the standard way such things are done. Also, Web API 2 parses those query parameters into the controller method parameter int[] categories automatically, so there is no need for a custom ModelBinder. Commented Aug 24, 2016 at 13:18

3 Answers 3

1

I've just found an answer to my question. Route attribute had missing parameter when using ModelBinder.

[Route("api/categories/{categoryIds}/products")]
public HttpResponseMessage GetProducts([ModelBinder(typeof(CategoryIdsModelBinder))] CategoryIds categoryIds)
{
    // <2> Get products using categoryIds.Ids
}

And CategoryIds would be

public class CategoryIds
{
    public List<int> Ids{ get; set; }
}

And CategoryIdsModelBinder would be

public class CategoryIdsModelBinder : IModelBinder
    {
        public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
            if (bindingContext.ModelType != typeof(CategoryIds))
            {
                return false;
            }

            var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (val == null)
            {
                return false;
            }

            var key = val.RawValue as string;
            if (key == null)
            {
                bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Wrong value type");
                return false;
            }

            var values = val.AttemptedValue.Split(',');
            var ids = new List<int>();
            foreach (var value in values)
            {
                int intValue;
                int.TryParse(value.Trim(), out intValue);
                if (intValue > 0)
                {
                    ids.Add(intValue);
                }
            }

            if (ids.Count > 0)
            {
                var result = new CategoryIds
                {
                    Ids= ids
                };

                bindingContext.Model = result;
                return true;
            }

            bindingContext.ModelState.AddModelError(
                bindingContext.ModelName, "Cannot convert value to Location");
            return false;
        }
Sign up to request clarification or add additional context in comments.

Comments

0

We can use Post methods

[RoutePrefix ( "api/categories" )] public class TestController { [HttpPost] [Route ( "getProducts" )]

    public HttpResponseMessage GetProducts ( HttpRequestMessage request )
    {
        HttpResponseMessage  message               = null;
        string               input                 = string.Empty;

        input  = request.Content.ReadAsStringAsync ().Result;
         var ids = Newtonsoft.Json.JsonConvert.DeserializeObject<List<string>> ( input );

    }
}

2 Comments

Thanks, but I would prefer to use GET to keep the API restful. I need to get data (read-only).
POST is not for retrieving data. According to Http specs POST method is for operation that will create new resource on the server and this new resource will have new unique ID. And GET method is when you perform querying of the data on the server and that will NOT change state of any resource of that query
0

Unfortunately Web API can not parse your data as array or as some kind of your custom object out of the box.

If you want to parse your url param as array you can try to do:

  1. Write your own route constraint which will read and convert your param from string to array of ints/strings/whatever;

  2. Write your custom type converter and use it with your data model;

  3. write your value provider and also use it with your data model

  4. Use parameter binding

Moreover you can always use query params which is never will break principles of REST :)

Please see more details about here and here

Hope that helps

2 Comments

Thanks! I've just posted one solution myself, and found a second one -- using ActionFilterAttribute I could even use List<int> ids as a parameter in controller.
Be careful with this attribute - it will execute on each request/response flow

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.