Query string not working while using attribute routing
Asked Answered
B

8

69

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.

Bruyn answered 25/3, 2014 at 18:8 Comment(3)
This will save you hours of frustration. nuget.org/packages/routedebugger/2.1.4Indoiranian
Nice, I will have to check this out as wellSansom
Thanks for the pointer @IanP. Unfortunately, if it cannot find a path it cannot tell you what route it's using. Useful tool that I didn't know about though! I think Glimpse does something similar.Bruyn
B
46

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.

Bruyn answered 9/4, 2014 at 12:25 Comment(7)
"pain-stacking" <snigger>Hertha
Looks good to me - the route is the bit before the query string. You're just saying that can be emptyGoingover
Sorry Kieren, I don't understand your comment - could you please elaborate?Troy
this won't work if [Route("")] is already marked on another method within the same controllerAllium
I downvoted that answer because if it's working in some cases, the good answer for all cases is from Tony (see below).Neurologist
Had me all excited, then I tried it and I'm STILL getting 404s [when I call my endpoint with query string parameter(s)]. :(Plea
Nearly 10 years later, had me all excited too, then I tried the [Route("")] trick and it didn't fix the issue (of using query-string form vs controller/action/parm1/parm2 form)Hexad
I
74

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
Ichneumon answered 3/11, 2015 at 17:21 Comment(2)
GetPproduct shall not be used as API endpoint as per REST standard. It should be noun alone like Products.Edmund
@Kenta GetProduct will not appear in the actual URI, see that the [Route()] attribute means the URI is /PD/search? which is acceptable. Don't forget there is no "REST standard" or specification and leeway is given.Pedology
B
46

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.

Bruyn answered 9/4, 2014 at 12:25 Comment(7)
"pain-stacking" <snigger>Hertha
Looks good to me - the route is the bit before the query string. You're just saying that can be emptyGoingover
Sorry Kieren, I don't understand your comment - could you please elaborate?Troy
this won't work if [Route("")] is already marked on another method within the same controllerAllium
I downvoted that answer because if it's working in some cases, the good answer for all cases is from Tony (see below).Neurologist
Had me all excited, then I tried it and I'm STILL getting 404s [when I call my endpoint with query string parameter(s)]. :(Plea
Nearly 10 years later, had me all excited too, then I tried the [Route("")] trick and it didn't fix the issue (of using query-string form vs controller/action/parm1/parm2 form)Hexad
S
22

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.

Sansom answered 25/3, 2014 at 18:14 Comment(6)
Thanks Tony. I've updated my question based on your advice. I've removed one of the functions so now I only have one function to sort. Issues still arise though! :(Bruyn
Looking at what you have it looks like it should be working to my knowledge. unfortunately I am not in a place I can further test and help you. That route debug package Ian commented on may be able to further assist.Sansom
My only other thought is to try and do {sport=None} and see if you can at least get into the method with that, you will at least know it gets in there and you could do a check if it is none. It will get you rolling until a better answer comes I am assuming.Sansom
Unfortunately, I'm still stuck. If I remove the routing attributes, I can use query strings fine, but not route parameters! It would be nice if I could do both...Bruyn
I don't think this is directly related as its mainly talking about webapi but they say the optional param is not supported this way. connect.microsoft.com/VisualStudio/feedback/details/451296 I did however find another link referring to optional querystring params and they are using a "string myParam = null" instead of an empty string in their method signature. try switching that up and see if it helps. ie: public List<int> Get(string name, string drink, string sport = null)Sansom
An important note is that you must provide the default value even if it's null. I wrongly assumed (prior to reading this answer) that a nullable type would already default to null for the purpose of routes, but it needs to be explicit (e.g. int? year = null instead of just int? year).Estimative
P
13

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
Psychotic answered 21/10, 2016 at 7:16 Comment(1)
By far the easiest way to do this. Thanks!Danelledanete
A
6

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.

Alfi answered 15/8, 2016 at 11:37 Comment(0)
A
6

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.");
            }
        }
    }
Ambrosial answered 2/12, 2016 at 18:48 Comment(1)
OMG, nearly 7 years later, what a difference an = sign makes, it allows both Route and QueryString style. Upvoted !Hexad
C
4

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)
Continue answered 29/9, 2017 at 17:58 Comment(0)
A
1

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.

Accomplished answered 14/5, 2014 at 0:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.