4

I use OpenAPI (Swagger) in a .NET Core project and when using multiple methods that have similar get requests, I encounter "Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints." error during runtime. I look at several pages on the web and SO and tried to apply the workarounds like The request matched multiple endpoints but why?, but it does not fix the problem. Here are the API methods and route definitions that I use.

[Route("get", Name="get")]
public IEnumerable<DemoDto> Get()
{
    //
}

[Route("get/{id}", Name="getById")]
public DemoDto GetById(int id)
{
    //
}

[Route("get/{query}", Name="getWithPagination")]
public IEnumerable<DemoDto> GetWithPagination(DemoQuery query)
{
    //
}

I use Name property in order to fix the problem but not solved. Any idea to make changes on the routes to differentiate Get() and GetWithPagination()?

Jack
  • 1
  • 21
  • 118
  • 236
  • 1
    @PavelAnikhouski Do you ask these questions in order to understand the problem and help? – Jack Oct 05 '20 at 14:02
  • Can I use `[HttpGet]` for `[Route("get/{query}", Name="getWithPagination")]`? – Jack Oct 05 '20 at 14:11
  • @Jack, in ASP.NET Core, we use [HttpGetAttribute](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpgetattribute) to define the route from method. – vernou Oct 05 '20 at 14:11
  • @Vernou Then you suggest to use like `[HttpGet("get/{query}", Name="getWithPagination")]`? – Jack Oct 05 '20 at 14:16

3 Answers3

5

You have two endpoints with equals routes: get/{id} and get/{query}.

If you write in browser line: get/123, the system can't understand what route to use, because they have the same pattern.

You need to distinguish them and I suggest you use restful style for routes, like: item/{id}, items?{your query}

Yaroslav Bres
  • 1,029
  • 1
  • 9
  • 20
  • You explained the problem weel. So, I can fix the problem by using `[Route("get/{query}", Name="getWithPagination")]`, but I am wondering if I can use this by `[HttpGet]`? I mean like `[HttpGet("get/{query}", Name="getWithPagination")]` – Jack Oct 05 '20 at 14:12
  • 1
    Yes, sure. [HttpGet("Route")] is actually syntax sugar for [HttpGet][Route("Route")] – Yaroslav Bres Oct 05 '20 at 14:14
  • 1
    Note, that even if you write [Route("get/{query}", Name="getWithPagination")] - the default method is still implicitly GET – Yaroslav Bres Oct 05 '20 at 14:17
  • What about the PUT, POST and DELETE? I think it is good idea to use the same annotation format for these methods in order to make standard. **1)** Should I also use `[HttpPost("post/{command}", Name="create")]` for **Create**, `[HttpPut("put/{command}", Name="update")]` for **Update** and `[HttpGet("delete/{id:int}", Name="delete")]`? **2)** And what about the `Name` parameter? I have used it to fix the problem, but I think it does not make any sense. Should I still use it? – Jack Oct 05 '20 at 14:24
  • 1
    It is good practice to use attributes, but I suggest you don't write words "get, put, post" in your routes. There is a restful standards for these cases, for example, your entity is User. [Post("users")] and user model is in body - create user, [Put("users/{id}")] and user model is in body - update user, [Delete("users/{id}")] - delete user, [Get("users/{id}")] - get user by id, [Get("users?{your query}")] - get and filter users. 2) "Name" is used in specific cases, but seems in your situation it is useless – Yaroslav Bres Oct 05 '20 at 14:32
  • `[HttpGet("{id:int}")]` instead of `[HttpGet("get/{id:int}")]` (without "get" word). That's ok, but regarding to the other point I have a question. Assume that I have EmployeeController that has API methods like Get, GetById, Create and Delete. As I use `[Route("api/[controller]")]` in my ApiController, then the routes will be like that: /api/employees/{id} (when I use `[HttpGet("{id:int}")]`). – Jack Oct 05 '20 at 14:45
  • But if I use `[HttpGet("employees/{id:int}")]` then the route will be /api/employees/employees/{id}. So, do you suggest this approach in case there are some other models in the EmployeeController that I need to get like: GetSalary(int employeeId) and then I will need to use like `[HttpGet("salary/{id:int}")]`? Am I wrong? Otherwise it would be ugly an url has repeating words like `/api/employees/employees/{id}`. Any idea? – Jack Oct 05 '20 at 14:45
  • 1
    Well, if all methods in the controller start with a shared part (like "employees"), you can move this part to controller level like[Route("api/[controller]")]. Then you DON'T write "employees" word in action methods, it will start with it automatically. One small remark, I prefer to explicitly specify the route above controller [Route("api/employees")] (Note, this is plural). – Yaroslav Bres Oct 05 '20 at 14:56
  • 1
    In the case with salary, the route might be like this: Route["employees/{id}/salary"] or if "employees" part above controller then just Route["{id}/salary"] – Yaroslav Bres Oct 05 '20 at 14:59
  • 1
    Also, 1) in [HttpGet("salary/{id:int}")] - ":int" is not nessasary. 2) I recommend to write explicitly attributes near each action's input parameter - [FromRoute], [FromQuery], [FromBody] – Yaroslav Bres Oct 05 '20 at 15:03
  • Thanks, I also use it on the Controller level as I mentioned like `[Route("api/[controller]")]` in my ApiController. But I could not explain my question well. **1)** Assuming that I use `[Route("api/[controller]")]` in my ApiController, then there is no need to use `[HttpGet("employees/{id:int}")]` and I would better to use [HttpGet("{id:int}")]. Is that true? – Jack Oct 05 '20 at 15:23
  • **2)** Regarding to the GetSalary() method, is there any need to use employees in the HttpGet annotation? Can I type it `[HttpGet("{id}/salary{id}")]` as the "employees" part will also be added as prefix to the route? **3** Normally I do not use int part, but I have to use it to fix the "The request matched multiple endpoints" error :) – Jack Oct 05 '20 at 15:23
  • 1
    1) yes, 2) final route = controller route + action route. If you use "employee" on the controller level, then you don't need to write it at action. In case with salary use just `[HttpGet("{id}/salary")]` – Yaroslav Bres Oct 05 '20 at 18:31
  • Many thanks for all of your perfect explanations and helps. Voted up for all your comments also ;) – Jack Oct 05 '20 at 19:52
5

[Route("get/{query}", Name="getWithPagination")]

This doesn't make sense. DemoQuery is an object, it can't be represented by a single part of a url. You can tell the ModelBinder to build your object from multiple query parameters, though.

The routing engine is getting this route confused with the [Route("get/{id}", Name="getById")] route. They both appear to match get/blah.

In addition to fixing your DemoQuery route, try adding a route constraint on the id route -

[Route("get/{id:int}", Name="getById")]

to better help the engine.


To get DemoQuery to work, assume it looks something like:

public class DemoQuery
{ 
     public string Name { get; set; }
     public int Value { get; set; }
}

Then change your action to

[Route("getPaged/{query}", Name="getWithPagination")]
public IEnumerable<DemoDto> GetWithPagination([FromQuery] DemoQuery query)

and call then endpoint like /getPaged?name=test&value=123. And the ModelBinder should build your object for you.

Jonesopolis
  • 25,034
  • 12
  • 68
  • 112
  • Yes, that is exactly what I have found so far and fixed the problem by apply as on [this](https://learn.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2) link. I use `[Route("get/{query}", Name="getWithPagination")]` and `[Route("get/{id:int}", Name="getById")]`. But, I am wondering if I can use , but I am wondering if I can use this by `[HttpGet]`? I mean like `[HttpGet("get/{query}", Name="getWithPagination")]`? – Jack Oct 05 '20 at 14:14
  • @Jonesopolis That should be `FromQuery`. – John H Oct 05 '20 at 14:23
  • 1
    Thanks @JohnH. Doing this from memory – Jonesopolis Oct 05 '20 at 14:23
  • Actually I use exactly as you give in yuour answer, but just wanted to omit in order for brevity. Thanks a lot, voted up. – Jack Oct 05 '20 at 14:26
  • What about the PUT, POST and DELETE? I think it is good idea to use the same annotation format for these methods in order to make standard. **1)** Should I also use `[HttpPost("post/{command}", Name="create")]` for **Create**, `[HttpPut("put/{command}", Name="update")]` for **Update** and `[HttpGet("delete/{id:int}", Name="delete")]` for **Delete**? **2)** And what about the `Name` parameter? I have used it to fix the problem, but I think it does not make any sense. Should I still use it? – Jack Oct 05 '20 at 14:26
  • PUT and POST expect a body (`[FromBody]`), you probably don't want a Url parameter on those. The `Name` parameter on the route isn't really necessary unless you need to reference this route specifically by name somewhere else in the code. – Jonesopolis Oct 05 '20 at 14:28
  • @Jack The `Name` parameter doesn't do anything in terms of action selection for an incoming request. What it does is allow you to use `@Url.RouteUrl(routeName)` in a view, or `RedirectToRoute(routeName)` in a controller, to give you a nice way of generating links/performing redirects without hardcoding routes. It's helpful when you have to refactor routes, because if you're only ever using routes by their names, your application will still work properly. Otherwise, you have to manually update every place you hardcoded your route. – John H Oct 05 '20 at 14:30
  • Thanks, what about for my **1st** question? – Jack Oct 05 '20 at 14:33
  • 1
    @Jack I never use [`Route`]. I always define the route within [`HttpXXX`], but that's personal preference. – John H Oct 05 '20 at 14:36
  • Thanks a lot, then I will use HttpXxx :) – Jack Oct 05 '20 at 15:00
0

ASP.NET Web API 2 supports a new type of routing. Offical Doc

Route constraints let you restrict your parameters type and matched with these types (int, string, even date etc). The general syntax is "{parameter:constraint}"

[Route("users/{id:int}")]
public User GetUserById(int id) { ... }

[Route("users/{name}")]
public User GetUserByName(string name) { ... }

I tested at API;

//match : api/users/1 
[HttpGet("{id:int}")]
public IActionResult GetUserById(int id){ ... }

//match : api/users/gokhan
[HttpGet("{name}")]
public IActionResult GetUserByName(string name){ ... }
Gokhan Turkben
  • 101
  • 1
  • 3
  • 9