Query string not working while using attribute routing - c#

I'm using System.Web.Http.RouteAttribute and System.Web.Http.RoutePrefixAttribute to enable cleaner URLs for my Web API 2 application. For most of my requests, I can use routing (eg. Controller/param1/param2) or I can use query strings (eg. Controller?param1=bob&param2=mary).
Unfortunately, with one of my Controllers (and only one), this fails. Here is my Controller:
[RoutePrefix("1/Names")]
public class NamesController : ApiController
{
[HttpGet]
[Route("{name}/{sport}/{drink}")]
public List<int> Get(string name, string sport, string drink)
{
// Code removed...
}
[HttpGet]
[Route("{name}/{drink}")]
public List<int> Get(string name, string drink)
{
// Code removed...
}
}
When I make a request to either using routing, both work fine. However, if I use a query string, it fails, telling me that that path does not exist.
I have tried adding the following to my WebApiConfig.cs class' Register(HttpConfiguration config) function (before and after the Default route), but it did nothing:
config.Routes.MapHttpRoute(
name: "NameRoute",
routeTemplate: "{verId}/Names/{name}/{sport}/{drink}",
defaults: new { name = RouteParameter.Optional, sport = RouteParameter.Optional, drink = RouteParameter.Optional },
constraints: new { verId = #"\d+" });
So for clarity, I would like to be able to do both this:
localhost:12345/1/Names/Ted/rugby/coke
localhost:12345/1/Names/Ted/coke
and,
localhost:12345/1/Names?name=Ted&sport=rugby&drink=coke
localhost:12345/1/Names?name=Ted&drink=coke
but sadly the query string versions don't work! :(
Updated
I've removed the second Action altogether and now trying to use just a singular Action with optional parameters. I've changed my route attribute to [Route("{name}/{drink}/{sport?}")] as Tony suggested to make sport nullable, but this now prevents localhost:12345/1/Names/Ted/coke from being a valid route for some reason. Query strings are behaving the same way as before.
Update 2
I now have a singular action in my controller:
[RoutePrefix("1/Names")]
public class NamesController : ApiController
{
[HttpGet]
[Route("{name}/{drink}/{sport?}")]
public List<int> Get(string name, string drink, string sport = "")
{
// Code removed...
}
}
but still, using query strings does not find a suitable path, while using the routing method does.

I was facing the same issue of 'How to include search parameters as a query string?', while I was trying to build a web api for my current project. After googling, the following is working fine for me:
Api controller action:
[HttpGet, Route("search/{categoryid=categoryid}/{ordercode=ordercode}")]
public Task<IHttpActionResult> GetProducts(string categoryId, string orderCode)
{
}
The url I tried through postman:
http://localhost/PD/search?categoryid=all-products&ordercode=star-1932
http://localhost/PD is my hosted api

After much painstaking fiddling and Googling, I've come up with a 'fix'. I don't know if this is ideal/best practice/plain old wrong, but it solves my issue.
All I did was add [Route("")] in addition to the route attributes I was already using. This basically allows Web API 2 routing to allow query strings, as this is now a valid Route.
An example would now be:
[HttpGet]
[Route("")]
[Route("{name}/{drink}/{sport?}")]
public List<int> Get(string name, string drink, string sport = "")
{
// Code removed...
}
This makes both localhost:12345/1/Names/Ted/coke and localhost:12345/1/Names?name=Ted&drink=coke valid.

With the Attribute routing you need to specify default values so they would be optional.
[Route("{name}/{sport=Football}/{drink=Coke}")]
Assigning a value will allow it to be optional so you do not have to include it and it will pass the value to specify.
I have not tested the query string for this but it should work the same.
I just re-read the question and I see that you have 2 Get verbs with the same path, I believe this would cause conflict as routing would not know which one to utilize, perhaps using the optional params will help. You can also specify one can be null and do checking in the method as to how to proceed.
[Route("{name}/{sport?}/{drink?}")]
Then check the variables in the method to see if they are null and handle as needed.
Hope this helps, some? lol
If not perhaps this site will, it has more details about attribute routing.
http://www.asp.net/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
Clip from that site:
Optional parameters and default values You can specify that a
parameter is optional by adding a question mark to the parameter, that
is:
[Route("countries/{name?}")]
public Country GetCountry(string name = "USA") { }
Currently, a default value must be specified on the optional parameter
for action selection to succeed, but we can investigate lifting that
restriction. (Please let us know if this is important.)
Default values can be specified in a similar way:
[Route("countries/{name=USA}")]
public Country GetCountry(string name) { }
The optional parameter '?' and the default values must appear after
inline constraints in the parameter definition.

Just a side note from my part as well. In order for queryString params to work, you need to provide a default value for your method parameters to make it optional. Just as you would also do when normally invoking a C# method.
[RoutePrefix("api/v1/profile")]
public class ProfileController : ApiController
{
...
[HttpGet]
[Route("{profileUid}")]
public IHttpActionResult GetProfile(string profileUid, long? someOtherId)
{
// ...
}
...
}
This allows me to call the endpoint like this:
/api/v1/profile/someUid
/api/v1/profile/someUid?someOtherId=123

Using Route("search/{categoryid=categoryid}/{ordercode=ordercode}") will enable you to use both Querystrings and inline route parameters as answered by mosharaf hossain. Writing this answer as this should be top answer and best way. Using Route("") will cause problems if you have multiple Gets/Puts/Posts/Deletes.

Here's a slight deviant of #bhargav kishore mummadireddy's answer, but an important deviation. His answer will default the querystring values to an actual non-empty value. This answer will default them to empty.
It allows you to call the controller through path routing, or using the querystring. Essentially, it sets the default value of the querystring to empty, meaning it will always be routed.
This was important to me, because I want to return 400 (Bad Request) if a querystring is not specified, rather than having ASP.NET return the "could not locate this method on this controller" error.
[RoutePrefix("api/AppUsageReporting")]
public class AppUsageReportingController : ApiController
{
[HttpGet]
// Specify default routing parameters if the parameters aren't specified
[Route("UsageAggregationDaily/{userId=}/{startDate=}/{endDate=}")]
public async Task<HttpResponseMessage> UsageAggregationDaily(string userId, DateTime? startDate, DateTime? endDate)
{
if (String.IsNullOrEmpty(userId))
{
return Request.CreateResponse(HttpStatusCode.BadRequest, $"{nameof(userId)} was not specified.");
}
if (!startDate.HasValue)
{
return Request.CreateResponse(HttpStatusCode.BadRequest, $"{nameof(startDate)} was not specified.");
}
if (!endDate.HasValue)
{
return Request.CreateResponse(HttpStatusCode.BadRequest, $"{nameof(endDate)} was not specified.");
}
}
}

I use FromUri attribute as solution
[Route("UsageAggregationDaily")]
public async Task<HttpResponseMessage> UsageAggregationDaily([FromUri] string userId = null, [FromUri] DateTime? startDate = null, [FromUri] DateTime? endDate = null)

Since you have [Route("{name}/{drink}/{sport?}")] as attribute routing, this code will never be hit.
config.Routes.MapHttpRoute(
name: "NameRoute",
routeTemplate: "{verId}/Names/{name}/{sport}/{drink}",
defaults: new { name = RouteParameter.Optional, sport = RouteParameter.Optional, drink = RouteParameter.Optional },
constraints: new { verId = #"\d+" });
So only the attribute route [Route("{name}/{drink}/{sport?}")] is going to be honored here. Since your request localhost:12345/1/Names?name=Ted&sport=rugby&drink=coke, doesn't have name, sport or drink in the URL it is not going to match this attribute route. We do not consider the query string parameters when matching the routes.
To solve this, you need to make all 3 optional in your attribute route. Then it will match the request.

Related

Mvc routing. How can i set a default parameter in .net core 3.0?

My controller is
public partial class GridController : Controller
{
[Route("/grid/{name}")]
public IActionResult Index(string name)
{
}
}
Routing is setup correctly since if i visit /grid/something i get http ok.
But how can i set a default parameter in startup.cs?
I tried the following but on page load, i get http 404 error
endpoints.MapControllerRoute(
"default",
"{controller=Grid}/{action=Index}/{name}");
Okay. Ended up with a workaround
endpoints.MapControllerRoute(
"default",
"{controller=Grid}/{action=Index}");
and by adding an extra action overload.
public IActionResult Index()
{
return Index("something");
}
it simply works
[Route("/grid/{name}")]
public IActionResult Index(string name )
{
}
Use attribute routing at Controller level, So that it will be applicable to all the action method under that controller.
In your route template. The name parameter is not optional.
So, use {name?}.
First of all, you did not specify the name parameter in your endpoint configuration. So it should have been {controller=Grid}/{action=Index}/{name=something}. But if there are overloads, this does not seem to work anyway. I wonder if this is a bug or is it intended by design. It seems that if there is are multiple overloads of the same action, execution gets directed to the parameterless one.
Here's another workaround with the possibility to use parameters. I tried this with .NET Core 2.2. Create a new action with the same parameters, but without RouteAttribute. From there you can do a redirect to the necessary action. Here's an example where I use TestIndex for the default route.
[Route("/[controller]/{name}/{age}")]
public IActionResult Index(string name, int age)
{
// implementation
return Ok($"{name} is {age} years old.");
}
public IActionResult Index()
{
return Index("Mr. It", 7);
}
public IActionResult TestIndex(string name, int age)
{
return RedirectToAction(nameof(Index), new { name, age });
}
Then for default route use the new method, e.g. {controller=Grid}/{action=TestIndex}/{name=That}/{age=42}.
If you use this for testing, the new action could be omitted from production by adding a TypeFilterAttribute. See this answer for more details.

Cannot generate correct links

I have the following code in a razor page:
#Url.Action("ArticleDetails", "Information", new { slug = article.Slug })
The page url where this code is placed has the form of http://localhost/category/6/category-name where 6 is the ID of the category
In the InformationController I have the following actions:
[HttpGet("article/{id}/{slug}")]
public IActionResult ArticleDetails(int id, string slug)
{
// some code ...
return View(data);
}
[HttpGet("article/{slug}")]
public IActionResult ArticleDetails(string slug)
{
// some code ...
return View(data);
}
How can I reach URL of form article/article-slug because #Url.Action(...) that I have in the page always try to reach controller action with id even if ID is not supplied as an anonymous type.
Links take the form of article/6/article-slug instead I want them to be article/article-slug without removing action with id in the controller.
I have noticed that 6 is from the id of the category. Also if I delete the controller action with Id i get the correct format of URL.
When resolving the action you're linking to, the IUrlHelper instance is using the current value of id in your current route (http://localhost/category/6/category-name), which has a value of 6, as you stated in your OP. Because there exists an ArticleDetails action that takes both an id and a slug (which you provide explicitly), the ArticleDetails action that takes both of these parameters is selected.
In order to resolve this, there are a couple of options. The first option is to clear out the RouteData value once you've used it in the action invoked when reaching http://localhost/category/6/category-name. In order to do that, you can use the following code within said action:
RouteData.Values.Remove("id");
I'm not a fan of doing it this way, but it does work. IMO, a better approach would be to simply use different names for the id parameter: e.g. categoryId and articleId in the respective controllers. This both fixes your issue and makes the code more readable in the corresponding actions. Your ArticleDetails action would simply change to:
[HttpGet("article/{articleId}/{slug}")]
public IActionResult ArticleDetails(int articleId, string slug)
{
// some code ...
return View(data);
}
Routing can be finicky when trying to do overloads like this. Your best bet is to use named routes:
[HttpGet("article/{id}/{slug}", Name = "ArticleDetailsIdSlug")]
public IActionResult ArticleDetails(int id, string slug)
[HttpGet("article/{slug}", Name = "ArticleDetailsSlug")]
public IActionResult ArticleDetails(string slug)
Then, in your view:
#Url.RouteUrl("ArticleDetailsSlug", new { slug = article.Slug })
Now, the routing framework doesn't have to try to figure out which route you actually want (and guess incorrectly apparently), as you'd told it exactly which route to use.

How to create a route attribute which handles missing middle parameters?

If I have the code below and when the firstname is missing in the url, I get a 404 error. See first example below. I am not sure why the route doesn't work in this case when the attribute names are in the url are there are placeholders for the values. My solution is to create another route and use another url format but that's more work. What if I have many parameters and some in the middle are missing their values, isn't there a single route that can handle all of the instances?
How do I create a single route which can handle the four combinations in the below example? Both are missing. Both are present. One of them is missing.
[HttpGet]
[Route("getcustomer/firstname/{firstname?}/status/{status?}")]
public IHttpActionResult GetCustomer(string firstname = null, string status = null)
{
... some code ...
}
**Example URLs:**
http://.../getcustomer/firstname//status/valid" causes 404
http://.../getcustomer/firstname/john/status/active" good
http://.../getcustomer/firstname/john/status/" good
That is by design. You may need to create another action to allow for that. Also in terms of how the framework is built. the end segments are the ones that should be optional.
You need to rethink your design.
For example
[RoutePrefix("api/customers")]
public class CustomersController : ApiController {
[HttpGet]
[Route("")] // Matches GET api/customers
public IHttpActionResult GetCustomer(string firstname = null, string status = null) {
... some code ...
}
}
Example URLs:
http://...api/customers
http://...api/customers?status=valid
http://...api/customers?firstname=john&status=active
http://...api/customers?firstname=john

No HTTP resource was found that matches the request URI - and - No action was found on the controller that matches the request

I am trying to build out a new endpoint in API app that already has a lot of other endpoints working just fine. Not sure what I'm doing wrong. I am getting two errors:
Message: No HTTP resource was found that matches the request URI 'http://localhost:62342/api/VoiceMailStatus
and
MessageDetail: No action was found on the controller 'VoiceMailStatus' that matches the request.
Here's the controller:
public class VoiceMailStatusController : ControllerBase
{
[HttpPost]
[Route("api/VoiceMailStatus")]
public string VoiceMailStatus(string var)
{
...
}
}
And here's the route:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
I'm using PostMan:
There are a LOT of threads here about both of these error messages. I've read many of them, but have yet to find a solution. One of them said to change this:
public string VoiceMailStatus(string var)
to this:
public string VoiceMailStatus(string var = "")
And while that did get the error to go away and I was able to get inside of the method while in debug, var was always just an empty string.
EDIT: GOT IT WORKING
In addition to adding [FromBody] as per Andrii Litvinov's answer, I also had to do one more thing. What had been this:
public string VoiceMailStatus(string var)
{
...
}
Is now this:
public string VoiceMailStatus([FromBody] VMStatus request)
{
...
}
And then VMStatus is just a small little class with a single string property:
public class VMStatus
{
public string var { get; set; }
}
Most likely you need to apply FromBody attribute to your parameter:
public string VoiceMailStatus([FromBody] string var)
If that does not help try to rename parameter to something else, e.g.: var1, because var is reserved work in C# and could cause some binding issues, but I doubt that's the case.
You probably want to add this to your controller since you have api in the route config:
[RoutePrefix("API/VoiceMailStatus")]
and then add this to your action:
[Route("VoiceMailStatus", Name = "VoiceMailStatus")]
This should tie the action to the url localhost/api/voicemailstatus/voicemailstatus

call to web api with string parameter

I have a web api where I have 2 methods, one without parameter and two with different types of parameter (string and int). When calling the string method it doesnt work...what am I missing here?
public class MyControllerController : ApiController
{
public IHttpActionResult GetInt(int id)
{
return Ok(1);
}
public IHttpActionResult GetString(string test)
{
return Ok("it worked");
}
}
WebApiConfig.cs:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
My Call:
/api/MyController/MyString //Doesnt work
/api/MyController/1 //work
I get following error:
The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.Web.Http.IHttpActionResult GetInt(Int32)' in 'TestAngular.Controllers.MyControllerController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
What am I missing in my request?
Here is my solution:
without changing default route in webapiconfig.cs file
add just a route to your string function:
[Route("Api/MyController/GetString/{test}")]
public IHttpActionResult GetString(string test)
http://localhost:49609/api/MyController/GetString/stringtest
Also this uri's should work:
api/MyController/GetAll
api/MyController/GetString?param=string
api/MyController/GetInt?param=1
I think this is much clearer and should always work.
You use the routing behavior.
See here:
http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-and-action-selection
You have to change your uri's
/api/MyController
/api/MyController/string
/api/MyController/1
You don't have to specify the methods.
You could take look at this tutorial on asp.net for further clarification.
It's been a while since you posted this, but I think I have the answer.
First off, there are two issues. First, as Pinback noted, you can't use the same route for two different endpoints.
However if you just eliminate the int method, you'll still run into the problem.
Remember: the default route looks like this: api/{controller}/{id}
In order to bind the parameter, it has to be called "id", and not "test".
Change the signature to this:
public IHttpActionResult GetString(string id)
and it will work.
(you can also change {id} to {test} in the webapiconfig.cs file).
You can also take string parameter in body
string body;
using (var sr = new StreamReader(Request.Body))
body = sr.ReadToEnd();

Categories