Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Web Api ignores RouteParameter.Optional when selecting the action

I found this issue by the following steps:

  1. Create a new WebApi project with the installed project template

  2. Go to Controllers/ValuesController.cs, there are two Get methods like this:

    public IEnumerable<string> Get() // this one provides GetAll function

    public string Get(int id) // this one is GetOneById

  3. I don't like this design because I think the two api methods can be combined into one:

    public IEnumerable<string> Get(string ids) when ids is null, it returns all the records, otherwise returns the results by ids(which looks like id1,id2,id3...)

I modified the route as well:

    config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{ids}",
            defaults: new { ids = RouteParameter.Optional }
        );

Now I think everything is ready. However when I visit /values in the browser(without specifying the ids parameter), I was told "no action was found", until I add a default value for ids:

public IEnumerable<string> Get(string ids = null)

From now on everything works as I expected. Still I can't move the defaults parameter in the route. Which means: the code declares the ids parameter as optional twice.

Looking into the source code of MVC4 located at System.Web.Http.Controllers.ActionSelectorCacheItem.FindActionUsingRouteAndQueryParameters method, I can see the optional parameters defined in the route are ignored when finding the right action. Personally I think it's really bad, even the workaround is quite easy. Or I misunderstood the relations between routes and action methods? I consider routes are rules used to help a request to find its corresponding action, while "a parameter is optional" is exactly part of a rule.

like image 485
Cheng Chen Avatar asked Nov 23 '12 09:11

Cheng Chen


2 Answers

You may well have an argument this feels like an awkward solution to something relatively straight forward, but I think the root cause is because you are trying to roll both 'GetAll' and 'GetOneById' into the same method.

You can leave the route config out of the problem if you are willing to split out the methods in the controller. IMHO this is not such a bad thing; it complies with the separation of concerns principle. It also does not mean any duplication of code as you can centralise the logic to access the object(s) you want to return.

These controller methods should give you the routes you want without having to adjust the route config:

[HttpGet]
public IEnumerable<string> GetAll()
{
    return GetStrings(new int[]{});
}

[HttpGet]
public string GetOneById(int id)
{
    return GetStrings(new int[]{id}).FirstOrDefault();
}

private IEnumerable<string> GetStrings(int[] ids)
{
     return // fetch strings using int array
}
like image 198
Nick Avatar answered Sep 25 '22 13:09

Nick


You've got the option to have the values as a Querystring

public class PaymentsController : ApiController {
        [HttpGet]
        public Person GetValues(string ids = null)
        {
            return new Person() { FirstName = "Michael" };
        }
    }

    public class Person
    {
        public string FirstName { get; set; }
    }
}

I was able to access this route with both:

  1. /api/payments/getvalues/?ids=123
  2. /api/payments/getvalues/

NOTE: without any special routing apart from the default

like image 41
Mike Avatar answered Sep 21 '22 13:09

Mike