Search code examples
asp.net-web-apiroutesasp.net-web-api-routingasp.net-core-2.0

Web Api Core 2 distinguishing GETs


Why can't Web API Core 2 tell these apart?

    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

    // GET api/values?name=dave
    [HttpGet]
    public string Get(string name)
    {
        return $"name is {name}";
    }

Here's what happens -

Both http://localhost:65528/api/values and http://localhost:65528/api/values?name=dave cause the first Get() method to execute.

This exact code works fine in Web Api 2.

I know multiple ways of getting around this, but I don't know why it happens.

Can someone explain why this has changed?


Solution

  • I don't think you can even compile your code in ASP.NET Core Mvc 2.0 since you have 2 actions mapped to same route [HttGet] api/values:

    AmbiguousActionException: Multiple actions matched.
    

    Remember, ASP.NET Web API uses the HTTP verb as part of the request to figure which action to call. Although it uses conventional routing (you name your actions Get, Post, Put and Delete, etc) if you don't have route attribute specify, I would highly recommend to always use routing attribute to annotate your controllers and actions.

    Api Design time

    Now it's up to you to design the route, as a developer. Remember the route is supposed to be a Uri that can identify a resource / resources.

    • Use the name as identifier along with the route

      [Route("api/[controller]")]
      public class CustomersController : Controller
      {
          // api/customers
          [HttpGet]
          public IActionResult Get()
          {
             ...
          }
      
          // api/customers/dave
          [HttpGet("{name:alpha}")]     // constraint as a string 
          public IActionResult GetByName(string name)
          {
              ...
          }
      }
      
    • Use the name as filter, against the resource collection

      [Route("api/[controller]")]
      public class CustomersController : Controller
      {
          // api/customers
          // api/customers?name=dave
          [HttpGet]
          public IActionResult Get(string name)
          {
              ...
          }
      }
      

    To confuse you more

    api/customers/dave will still execute GetById first!

    [Route("api/[controller]")]
    public class CustomersController : Controller
    {
        [HttpGet]
        public IActionResult Get()
        {
            ...
        }
    
        [HttpGet("{name}")]
        public IActionResult GetByName(string name)
        {
            ...
        }
    
        [HttpGet("{id}")]
        public IActionResult GetById(int id)
        {
            ...
        }
    }
    

    Both methods GetByName and GetById are potential candidates but MVC picks GetById method first because MVC compares the method/template name {name} and {id} through case-insensitive string comparison, and i comes before n.

    That's when you want to put constraints.

    [Route("api/[controller]")]
    public class CustomersController : Controller
    {
        [HttpGet]
        public IActionResult Get()
        {
            ...
        }
    
        // api/customers/dave
        [HttpGet("{name:alpha}")]
        public IActionResult GetByName(string name)
        {
            ...
        }
    
        // api/customers/3
        [HttpGet("{id:int}")]
        public IActionResult GetById(int id)
        {
            ...
        }
    }
    

    You can also specify the Ordering too!

    [Route("api/[controller]")]
    public class CustomersController : Controller
    {
        [HttpGet]
        public IActionResult Get()
        {
            ...
        }
    
        // api/customers/portland
        [HttpGet("{city:alpha}", Order = 2)]
        public IActionResult GetByCity(string city)
        {
            ...
        }
    
        // api/customers/dave
        [HttpGet("{name:alpha}", Order = 1)]
        public IActionResult GetByName(string name)
        {
            ...
        }
    
        // api/customers/3
        [HttpGet("{id:int}")]
        public IActionResult GetById(int id)
        {
            ...
        }
    }
    

    Without the Order, the method GetByCity will be in favor than GetByName because character c of {city} comes before the character n of {name}. But if you specify the order, MVC will pick the action based on the Order.

    Sigh the post is too long....