I'm trying to understand attribute routing in ASP.NET MVC. I understand how routing matches on url elements, but not query parameters.
For example, say I have a rest-style book lookup service that can match on title or ISBN. I want to be able to do something like GET /book?title=Middlemarch or GET /book?isbn=978-3-16-148410-0 to retrieve book details.
How do I specify [Route] attributes for this? I can write:
[HttpGet]
[Route("book/{title}")]
public async Task<IActionResult> LookupTitle(string title)
but as far as I can tell this also matches /book/Middlematch and /book/978-3-16-148410-0. If I also have an ISBN lookup endpoint with [Route("book/{isbn}")] then the routing engine won't be able to disambiguate the two endpoints.
So how do I distinguish endpoints by query parameter name?
The following Route() attribute will meet your requirements:
[HttpGet]
// /book?title=Middlemarch
// /book?isbn=978-3-16-148410-0
// /book?title=Middlemarch&isbn=978-3-16-148410-0
[Route("book/")]
public IActionResult LookupTitle(string isbn, string title)
{
if (isbn != null) { /* TODO */ }
if (title != null) { /* TODO */ }
return View();
}
When the ASP.NET MVC parser does not find any matching parameters in the route pattern it trying to interpret these as query parameters.
Related
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()?
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}
[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.
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){ ... }
I have 2 pages which can be accessed via these actions:
public class SearchEngineController : Controller
{
[Route("/search/{k}")]
public IActionResult Search(string k = "")
{
return View();
}
}
public class ChannelController : Controller
{
[Route("{name}")]
public IActionResult Index(string name = "")
{
return View();
}
}
Now, when I search something with a key (somekey), I want to redirect to url localhost:5000/search?k=somekey
Because we're working with channels (like Youtube's channels), so we need to classify the channel names, it should be unique. Example, a channel with a name mobifone can be accessed through localhost:5000/mobifone.
Everything may look ok until the name parameter (inside the Index action) cannot classify when a searched request is called. So, everytime I type localhost:5000/search?k=somekey, it will hit the Index action.
So, my temporary solution looks like this:
public class ChannelController : Controller
{
[Route("{name}")]
public IActionResult Index(string name = "")
{
if (name.ToLower() == "search")
{
// ~/Views/Shared/Search.cshtml
return View("Search");
}
return View();
}
}
It may solve the problem but.... I don't like it. Because I don't want to nest and excute the search query inside ChannelController. It's not a part of channel. A channel may contain:
- Id
- Name
- DisplayName
- FounderId
- ...
In the middleware, _channelManager shouldn't have a searched engine member which can return everything in the world, such as:
- Channel information
- List of channels
- User profile
- A post content
- List of posts
- ...
Is there any way better than mine?
The issue is that you're not actually using a correct route. You've defined the search route as "/search/{k}", which then means that you need something like /search/somekey to actually hit it. What you're requesting is /search?k=somekey, which doesn't match the search route, and is likely simply falling back to your default route, which just so happens to be this ChannelController.Index action. If you're wanting to pass the k param via the querystring, then you should remove it from the route definition, i.e. [Route("/search")].
We are using .NET Core to build a Web API. We need to support "GetBy" functionality, e.g. GetByName, GetByType, etc. but the issue we are running into is how to depict this through routes in a Restful way as well as the method overloading not properly working with how we think the routes should be. We are using MongoDB so our IDs are strings.
I'm assuming our routes should be something like this:
/api/templates?id=1
/api/templates?name=ScienceProject
/api/templates?type=Project
and the issue is that all our methods in our controller have a single string parameter and isn't properly mapped. Should the routes me different or is there a way to properly map these routes to the proper method?
If the parameters are mutually exclusive, i.e. you only search by name or type but not by name and type, then you can have the parameter be a part of the path instead of the query-params.
Example
[Route("templates")]
public class TemplatesController : Controller
{
[HttpGet("byname/{name}")]
public IActionResult GetByName(string name)
{
return Ok("ByName");
}
[HttpGet("bytype/{type}")]
public IActionResult GetByType(string type)
{
return Ok("ByType");
}
}
This example would lead to routes like:
/api/templates/byname/ScienceProject
/api/templates/bytype/Project
If there parameters are not mutually eclusive then you should do it like suggested in the answer by Fabian H.
You can make a TemplatesController with a single get method, that can take all the arguments.
[Route("api/templates")]
public class TemplatesController : Controller
{
[HttpGet]
public IActionResult Get(int? id = null, string name = null, string type = null)
{
// now handle you db stuff, you can check if your id, name, type is null and handle the query accordingly
return Ok(queryResult);
}
}
Let's assume I have a following action method:
[Route("option/{id:int}")] // routing is incomplete
public ActionResult GetOption(int id, bool advanced)
{
...
return View();
}
This action should be associated with two different routes:
the one that fetches results in a simple form: .../option/{id},
and its advanced version: .../option/{id}/advanced.
It is important to represent these routes as two separate urls, and not the same URL with optional query string parameter. The only difference between these URLs is in the last term, which is basically some form of designation. What I need is a way to setup the routing rules to tell framework that it should call same method for both types of requests passing true as a second parameter in case of 'advanced' requests, or false otherwise. There are substantial reasons why I need to encapsulate both routes into one action method. So, no, I can't add second method to handle 'advanced' requests separately.
The question is: How to setup such routing?
If you are able to change the type of the second parameter
[HttpGet]
[Route("option/{id:int}")] // GET option/1
[Route("option/{id:int}/{*advanced}")] // GET option/1/advanced
public ActionResult GetOption(int id, string advanced) {
bool isAdvanced = "advanced".Equals(advanced);
//...
return View();
}
And as much as you are apposed to having separate actions you can simpy have one call the other to avoid repeating code (DRY)
// GET option/1
[HttpGet]
[Route("option/{id:int}")]
public ActionResult GetOption(int id, bool advanced = false) {
//...
return View("Option");
}
// GET option/1/advanced
[HttpGet]
[Route("option/{id:int}/advanced")]
public ActionResult GetAdvancedOption(int id) {
return GetOption(id, true);
}
I am trying to add a Get() function in a MVC 6 (Asp .Net 5) Web Api to pass a configuration option as a query string. Here are the two functions that I already have:
[HttpGet]
public IEnumerable<Project> GetAll()
{
//This is called by http://localhost:53700/api/Project
}
[HttpGet("{id}")]
public Project Get(int id)
{
//This is called by http://localhost:53700/api/Project/4
}
[HttpGet()]
public dynamic Get([FromQuery] string withUser)
{
//This doesn't work with http://localhost:53700/api/Project?withUser=true
//Call routes to first function 'public IEnumerable<Project> GetAll()
}
I've tried several different ways to configure the routing, but MVC 6 is light on documentation. What I really need is a way to pass some configuration options to the list of Projects for sorting, custom filtering etc.
You can't have two [HttpGet]s with the same template in a single controller. I'm using asp.net5-beta7 and in my case it even throws the following exception:
Microsoft.AspNet.Mvc.AmbiguousActionException
Multiple actions matched. The following actions matched route data and had all constraints satisfied:
The reason for this is that [From*] attributes are meant for binding, not routing.
The following code should work for you:
[HttpGet]
public dynamic Get([FromQuery] string withUser)
{
if (string.IsNullOrEmpty(withUser))
{
return new string[] { "project1", "project2" };
}
else
{
return "hello " + withUser;
}
}
Also consider using Microsoft.AspNet.Routing.IRouteBuilder.MapRoute() instead of attribute routing. It may give you more freedom defining the routes.
Accessing the query string is very doable by using either the RESTful routing conventions (enforced by ASP.NET 5 / MVC 6 by default) or by defining custom routes, as explained in this answer.
Here's quick example using custom, attribute-based routes:
[HttpGet("GetLatestItems/{num}")]
public IEnumerable<string> GetLatestItems(int num)
{
return new string[] { "test", "test2" };
}
For more info about custom routing, read the following article on my blog.