How to specify route with query-string? - c#

I'm trying to add AttributeRouting to my WebAPI project.
On one controller I currently have three GET methods defined:
[GET("dictionaries")]
public IEnumerable<Dictionary> Get()
[GET("dictionaries/{id}")]
public Dictionary GetByID(int id)
[GET("dictionaries/{dictionaryID}/{page}")]
public Dictionary Browse(long dictionaryID, int page)
The first two routes are working as I expect them to, but the third always returns a 405 METHOD NOT ALLOWED.
I've tried sending the parameters in the URL, and the query-string, and it's the same response for both. When I've tried the query-string version, I've modified the route to be
[GET("dictionaries?dictionaryID={dictionaryID}&page={page}
I've also tried changing the initial word from dictionaries to dictionary to avoid any ambiguity with the other two GET routes, but still no success.
The documentation for AttributeRouting only mentions query-strings in relation to parameter constraints (which aren't available for me because of the WebHost framework) and doesn't mention how query-strings can be used in routing.
Can anyone tell me how I can achieve this third route with those two parameters, or do I have to drop AttributeRouting or try on a controller of its own?

Web API action selector implicitly thinks that the third action here is a POST, since it doesn't start with verbs like GET, POST, PUT etc. Try adding HttpGet attribute also and see if this works.
[HttpGet, GET("dictionaries/{dictionaryID}/{page}")]
public Dictionary Browse(long dictionaryID, int page)

Related

C# Core MVC How to apply custom URL to controller action

Question:
Edit: It seems my question is actually not a routing issue but an anchoring issue.
If I have assigned a route:
[Route("~/Envelope/List/AcademicYear/{year}")]
public IActionResult AcademicYear(string year)
{
}
How would I correctly use an asp-action to call this route?
using
<a asp-action="List/AcademicYear/" asp-route-id="#Model.AcademicYear">#Model.AcademicYear</a>
returns a url with a %2f (Envelopes/List%2fAcademicYear/2122) instead of / (Envelopes/List/AcademicYear/2122) and thus results in a 404 error
How do I use Custom URL with asp-action to call a specific Action in my Controller?
or
How do I change the routing so I can call an action from a controller with a non default route mapping?
Context:
I've read https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-5.0
and yet i'm still confused on the whole concept of routing and how to interacts with controllers and actions.
In my application I have a controller called Envelope - it controls everything to do with my Envelopes.
I have a class in the Envelopes controller called
public class EnvelopeController : Controller {
public IActionResult List() {... return View()}
and it returns the List.cshtml view. The current url as set by default route mapping: /Envelope/List
In the List.cshtml I have a link that is intended to filter the List on a year parameter
<a asp-action="AcademicYear" asp-route-academicYear="#Model.AcademicYear"> #Model.AcademicYear</a>
My intention is to pass this into a method in the Envelopes controller called "AcademicYear" that gathers the Envelope data stored in temp data, deseralises it and then returns a filtered version based on the parameter:
public IActionResult AcademicYear(string academicYear) { return View("List", newViewModel)}
The return url after this point is correctly: /Envelope/AcademicYear?academicYear=21%2F22
However I would Like to know how to change this so even though I call the Action
<a asp-action="AcademicYear" asp-route-academicYear="#Model.AcademicYear"/>
the URL returned would look like this /Envelope/List/AcademicYear/2122/
Is there a way of doing this? Am I looking at the problem the wrong way? I have thought about simply passing a parameter in the List action and running some form of control to do either set of operations depending on the parameters existence but realistically the List method and the filtering AcademicYear method aren't really doing the same thing and I'd like to seperate out the code into different methods if possible.
Even if its not the appropriate solution I would still like to know if it is possible to change the URL routing for an action after it has been called.
Edit :
I have tried using HttpGet(List/AcademicYear/{academicYear:[a-zA-Z]} however when I do this I can't actually call List/AcademicYear as an asp-action due to the "/" and how that encodes to %2f
Answer:
With the help of the below solutions I realised I was looking at the problem wrong and was actually having issues creating correct anchors.
Reading: https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-5.0#url-generation-and-ambient-values-1
I realised the answer was staring me in the face and I used this alongside the Routes provided for in the answers
<a id="#academicYear.Name" href="#Url.Action("AcademicYear", "Envelope",new {year= academicYear.Name})">
Maybe you just need to decorate your action like that:
[HttpGet("List/AcademicYear/{year:int}")] // or HttpPost it depends on you
public IActionResult AcademicYear(string year) { }
You can add several attribute routes to the action.
[Route("~/Envelope/List/AcademicYear/{year}", Name="ListRoute")]
[Route("~/Envelope/AcademicYear/{year}")]
public IActionResult AcademicYear(string year) { }
and if you need this url
http://localhost:xxxx/Envelope/List/AcademicYear/2021
you can use this html helper, that will create a right anchor tag for you using an attribute route name.
#Html.RouteLink(#Model.AcademicYear, "ListRoute", new { year = #Model.AcademicYear })
I added Name="ListRoute" to the attribute routing (see above)

WebApi - UrlHelper.Route to GET with [FromUri] object parameter

I have an API action defined as the following:
[Route(Name="GetMembersTest"), HttpGet, ResponseType(typeof(MemberHeadersDto))]
public IHttpActionResult GetMembers[FromUri]MemberFilterDto filter, [FromUri]PagingOptionsDto paging)
This method works as expected, routing and all, requests are flowing through just fine. However, I'd like to supply a "NextUri" for paging so that the caller can just keep following NextUri until it is null to get all the results. I have to send back a uri to the same action, 1 page ahead, if that makes sense.
So I tried using UrlHelper.Route. This route is named "GetMembers" for the purpose of this example.
NextUri = Url.Route("GetMembers", new { filter, paging });
The problem is that instead of getting something like
/v1/members?filter.q=&filter.otherproperty=&paging.count=10&paging.startRow=11
I get
/v1/members?filter=WebApi.Models.MemberFilterDto&paging=WebApi.Models.PagingOptionsDto
It looks like UrlHelper.Route doesn't support complex types in the [FromUri] parameter of a GET Request. Is there anything I can do to get this functionality? My workaround right now is to take in all the Dto properties as individual parameters then build my Dtos from them on the server. This isn't ideal because if I ever add any more options I'd have to add more parameters to the action, which also makes the route value dictionary more fragile as well because it has to match with the method signature in UrlHelper.Route(routeName,routeValues).
Unfortunately, there is no way to pass in complex object to routing. Instead, you will need to pass in the simple properties individually.
I was not able to find a way to extend Url.Route, but that would be/have been your best option.

webapi 5.2.3 Multiple controller types were found that match the URL - Attribute Routing

I've hit an issue with webapi attribute routing. I am calling the following route: assessment/assessments/assessmenttypes as an HttpPost. I'm getting the error that multiple controller types match the url. The problem is that they definitely don't match. Initially when I first got this issue I was using different route prefixes, however I read that webapi can ignore route prefixes, so I changed all the routes to make them unique, so the only HttpPosts which are in the "matching controllers" are defined as follows:
[Route("assessments"), HttpPost]
public async Task<System.Int32> PostCreate([FromBody]Assessment assessment)
and
[Route("assessments/assessmenttypes"), HttpPost]
public async Task<System.Int32> PostCreate([FromBody]AssessmentType assessmentType)
Both controllers have the same RoutePrefix of: [RoutePrefix("assessment")]
Can anyone help please, this is very frustrating.
Thanks in advance!
What you need to understand first at all is how routing works, that's the key to solve your problem. I can update my answer later if you give me more information (at this moment it looks like you didn't post all the required information)
As you know, routing is the process of matching requests to routes.
The handler that does the magic is the HttpRoutingDispatcher with some help of extra libraries, but basically the dispatcher resolves the request. The final goal of processing the route is to identify two elements:
Route
Values
You have two different ways of resolving routes, convention-based (templates) or by attributes. It's a good idea to be consistent to avoid confusions specially when the application grows.
The routing has two different parts, fixed segments (they must match exactly) and variables (denoted as you know by {})
The request verbs are matched in two different ways, with the attribute
[HttpGet] [HttpPost] [HttpPut]
Or confusingly in my opinion using the first part of the method, if you have 3 different methods not marked with attributes the WebApi assumes the first letters try to indicate the verb:
GetList --> HttpGet of List
GetDetail --> HttpGet of Detail
PostDetail --> HttpPost of Detail
From the official documentation if you try the following template:
routeTemplate: "api/{controller}/{id}"
And you make a request from Ajax like the following:
$.ajax("/api/today/dayofweek", {
The following controller will fail (nearly always)
public class TodayController : ApiController {
[HttpGet]
public string DayOfWeek() {
return DateTime.Now.ToString("dddd");
}
[HttpGet]
public int DayNumber() {
return DateTime.Now.Day;
}
Because it's nearly impossible to establish which one is the correct one (a human being can of course, but unfortunately machines need more programming to work with fuzzy logic) :)
The problem in your case it's not both start with the same fixed route, the problem is that it's not possible to identify whether you're saying the second part belongs to a variable or it's a fixed part (it's ambiguous)
Giving you an example; how could we establish the meaning of this?
/segmenta/segmentb
/segmenta/segmentb/segmentc (is segment c a fixed route or a parameter?)
From an architectural point of view the reality is (based on Api design) that I don't think /assestmenttypes is a subordinated resource of /assestments. If you want to design a RESTful ROA Api assestmenttypes are correctly indicated as a resource if the type depends on the assestment (as a child, not as an attribute).
I would suggest you to review your paths too.

WebAPI Controller Methods that Differ By Value Types

I have an existing API I'm moving over to WebAPI, so I'm not free to change the URL. Breaking existing clients is not an option for me.
Knowing that, the original API would accept either a Guid (an ID) or a string (a name) for a given action method. The old API handler would decipher the URL parameter and send the request to a controller action designed to accept the given parameter type.
As an example:
Get(Guid id)
versus
Get(string name)
With WebAPI, the parameter binding is greedy across value types, so depending on which is first in the controller source file, that action is the one invoked. For my needs, that's not working. I was hoping the binder would realize the conversion to a Guid would fail for a name and then select the more generic string-based action. No dice. The Guid simply comes across as a null value (interestingly since it's a value type, but that's what I'm getting in the debugger at a certain point in the processing).
So my question is how best to handle this? Do I need to go as far as implementing a custom IHttpActionSelector? I tried the attribute routing approach (with constraints), but that isn't working quite right (bummer as it looks cool). Is there a mechanism in WebAPI that accounts for this I don't (yet) know about? (I know I can hack it in by testing the string for Guid-ness and invoking the other controller method, but I'm hoping for a more elegant, WebAPI-based solution...)
I'd spent a lot of time trying to fit attribute-based routing in, but I've not got that to work. However, I did solve my particular issue using route constraints. If you register the more-constrained route first, WebAPI (like MVC) will apply the constraints and skip over more-constrained routes until it finds one that it can select, if any.
So, using my example, I'd set up routes like so:
_config.Routes.MapHttpRoute(name: "ById",
routeTemplate: "product/{id}",
defaults: new { controller = "ProductDetails" },
constraints: new { id = #"^\{?[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\}?$" });
_config.Routes.MapHttpRoute(name: "ByName",
routeTemplate: "product/{name}",
defaults: new { controller = "ProductDetails" });
The first route accepts a constraint in the form of a regular expression for a Guid. The second accepts all other values, and the controller will deal with non-product names (returns a 404). I tested this in the self-hosted WebAPI server and it works fantastically.
I am sure attribute-based routing would be more elegant, but until I get that to work, it's routing the old way for me. At least I found a reasonable WebAPI-based solution.

How does ASP.NET Web.api handle two methods with names starting with GET?

I am looking at the following tutorial from Microsoft. As per this tutorial,
In the first example, "products" matches the controller named
ProductsController. The request is a GET request, so the framework
looks for a method on ProductsController whose name starts with
"Get...". Furthermore, the URI does not contain the optional {id}
segment, so the framework looks for a method with no parameters. The
ProductsController::GetAllProducts method meets all of these
requirements.
What happens if there are two methods like GetAllProducts() and GetSoldProducts()? Both have no parameters.
Your First Web API Tutorial
There are two possible solutions to this specific problem:
Alter MapHttpRoute calls to require specifying the name of the action. (I'm using Self-hosting syntax):
config.Routes.MapHttpRoute(
"API Route 1",
"api/{controller}/{action}");
config.Routes.MapHttpRoute(
"API Route 2",
"api/{action}",
new { controller = "products" });
So your http client would call:
api/products/GetAllProducts OR api/GetAllProducts
api/products/GetSoldProducts OR api/GetSoldProducts
See:
http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api
Place each method in a separate controller (ProductsController, SoldProductsController). So you would call api/products and api/soldproducts to get your results.
Related topic... in the situation where you have a multiple Get actions that have a single primitive argument of the same type, ASP.NET Web API will look at the name of the argument to resolve which overloaded action to call.
For example, if you have two actions:
GetProductByName(string name)
GetProductByCategory(string category)
your http client can call
api/products?name=hammer
api/products?category=toys
and the routing engine will call the correct action.
Assuming you're using the default routes the short answer is : the method defined first (at the top) of your class will be called. the other method is inaccessible.
NOTE : the beta behaved as above for 'matching multiple methods' - the RC & Release version is a bit more OCD. It throws an error if there are multiple potential matches. This change removes the confusion of multiple ambiguous matches. At the same time, it reduces our ability to mix REST and RPC style interfaces in the same controller, relying on the order & overlapping routes.
Stealing liberally from another post I wrote on the topic:
WebAPI Matching Semantic
The matching semantic used by WebAPI is fairly simple.
It matches the name of the action with the verb (verb = get? look for method starting with "get")
if a parameter is passed, the api seeks an action with a parameter
So in your code sample a GET request without a parameter matches the Get*( ) function without an parameters. A Get containing and ID looks for a Get***(int id).
Examples
While the matching semantic is simple, it creates some confusion for MVC developers (well at least this developer). Lets look at some examples :
Odd Names - Your get method can be named anything, so long as it starts with "get". So in the case of a widget controller you can name your functions GetStrawberry() and it will still be matched. Think of the matching as something like : methodname.StartsWith("Get")
Multiple Matching Methods - What happens if you have two Get methods with no parameters? GetStrawberry() and GetOrange(). As best I can tell, the function defined first (top of the file) in your code wins ...strange. This has the side effect of making some methods in your controller unreachable (at least with the default routes)....stranger.
UPDATE
#WinFXGuy - This was a bit long to put in a comment, but ...
Don't jump to conclusions! I tried to answer the question you posed, but that's only half the story. There is plenty you can do to change the default behavior.
First, WebAPI supports much of the oData spec. If you bubble an IQueryable up to your controller, oData paramaters are automatically integrated with the query object. It takes parameters like $filter, $top, and $skip. So you in your case you can write one method and pass something like $filter=sale_date neq null.
Additionally, you can apply the [ResultLimit] attribute to prevent people asking for 15 billion records.
Second you can modify the routes. The default routes aim towards a RESTful api, where you generally have 1 controller per entity. You can change the routes and make it RPC style.
If you look at my linked post I explain how I kept the default route binding, added 'sub folders' and also allowed additional method calls for scenarios where i needed GetAllProducts() and GetSoldProducts().
Adding an answer to reflect that the latest version of Web API supports [Route] attribute natively
[Route("api/products")]
public IEnumerable<Product> GetAllProducts(){}
[Route("api/products/sold")]
public IEnumerable<Product> GetSoldProducts(){}

Categories