2

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?

Bryan
  • 5,065
  • 10
  • 51
  • 68

2 Answers2

6

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....

David Liang
  • 20,385
  • 6
  • 44
  • 70
  • For the record, one of the catch-22's of attribute routing is the fact that in routing order is a significant design factor and .NET attributes order is undefined by default. This means you always have to be aware that if you have similar routes they need to be ordered (or constrained). Convention-based routing typically requires less configuration, is more extensible and powerful, and specifying the order is as simple as declaring them in the right order. – NightOwl888 Oct 11 '17 at 00:18
  • Thanks David, very helpful – Bryan Oct 11 '17 at 04:12
0

Because in your case the best match in the route pipeline is the default httpget attribute (the one that get all). The query is a regular string so if you don't ask what you want from the query the best match is still the one that get all.

[HttpGet]
public string Get([FromQuery]string name)
{
    return $"name is {name}";
}

The [FromQuery] is pointing to the key "name" in the query string to get the value.
Your should read Routing in asp.net core

Sinan
  • 898
  • 1
  • 9
  • 23