Query String Not Working While Using Attribute Routing

Query string not working while using attribute routing

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.

Attribute Routing for querystring

You need to modify the routes a bit as query string are not normally used in attribute routes. They tend to be used for inline route parameters.

[RoutePrefix("subroute")]
public class HomeController : ApiController {
//Matches GET subroute/GetInfo?param1=somestring¶m2=somestring
[HttpGet]
[Route("GetInfo")]
public IHttpActionResult GetInfo(string param1, string param2) {
//...
}
}

Also

Enabling Attribute Routing


To enable attribute routing, call MapHttpAttributeRoutes during configuration. This extension method is
defined in the System.Web.Http.HttpConfigurationExtensions class.

using System.Web.Http;

namespace WebApplication
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API routes
config.MapHttpAttributeRoutes();

// Other Web API configuration not shown.
}
}
}

Reference Attribute Routing in ASP.NET Web API 2

Query string and attribute routing together for controller .NET core web API

You can solve this by defining multiple routes on top of the controller method

[HttpGet("GetValues")]
[HttpGet("GetValues/{name}/{surname}")]
public string GetValue(string name, string surname)
{
return "Hi" + name;
}

This will work with http://localhost:11979/api/values/GetValues/John/lawrance and http://localhost:11979/api/values/GetValues?name=john&surname=lawrance

To add more:

[HttpGet]
[Route("GetValues")]
[Route("GetValues/{name}/{surname}")]
public string GetValue(string name,string surname)
{
return "Hello " + name + " " + surname;
}

This also works.

How to use Route attribute to bind query string with web API?

Make sure attribute routing is enabled in WebApiConfig.cs

config.MapHttpAttributeroutes();

ApiController actions can have multiple routes assigned to them.

[RoutePrefix("api/Default")]
public class DefaultController : ApiController {

[HttpGet]
//GET api/Default
//GET api/Default?name=John%20Doe
[Route("")]
//GET api/Default/John%20Doe
[Route("{name}")]
public string Get(string name) {
return $"Hello " + name;
}
}

There is also the option of making the parameter optional, which then allow you to call the URL with out the inline parameter and let the routing table use the query string similar to how it is done in convention-based routing.

[RoutePrefix("api/Default")]
public class DefaultController : ApiController {

[HttpGet]
//GET api/Default
//GET api/Default?name=John%20Doe
//GET api/Default/John%20Doe
[Route("{name?}")]
public string Get(string name = null) {
return $"Hello " + name;
}
}

Attribute route shows up as query string

Check the following:

  1. Make sure you have placed a call to routes.MapMvcAttributeRoutes() in your RouteConfig (or somewhere in your application startup).
  2. Make sure you have placed the call to routes.MapMvcAttributeRoutes() before the default route.

public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

// Add this to get attribute routes to work
routes.MapMvcAttributeRoutes();

routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}

Also, you are shooting yourself in the foot by making the parameters optional. Only 1 optional parameter can be used on a route and it must be the right most parameter. The second route here is an unreachable execution path because all 3 of the first parameters are optional.

[Route("Create/{companyId:guid?}/{branchId:guid?}/{departmentId:guid?}")]
[Route("Create/{companyId:guid?}/{departmentId:guid?}")]

Technically, you can do what (I think) you want with a single route.

[Route("Create/{companyId:guid}/{departmentId:guid}/{branchId:guid?}")]
public async Task<ActionResult> Create(Guid companyId, Guid departmentId, Guid? branchId)

Which will match

/Create/someCompanyId/someDepartmentId/someBranchId
/Create/someCompanyId/someDepartmentId

If you do in fact want all parameters to be optional, the best way is to use the query string. Routes aren't designed for that sort of thing, but if you really want to go there using routing, see this answer.

Answer to Edit 2

There is nothing wrong with removing the default route. However, your business logic is a bit too complex for the default routing to work. You are correct in your assessment that the 2nd and 3rd routes are indistinguishable from one another (even if you made the parameters required) because the patterns are the same.

But .NET routing is more flexible than what MapRoute or the Route attribute can provide alone.

Also, if these URLs are Internet facing (not behind a login), this is not an SEO friendly approach. It would be far better for search engine placement to use company, branch, department, and subdepartment names in the URL.

Whether or not you decide to stick with the GUID scheme, you have a unique set of URLs, which is all that is required to get a match. You would need to load each of the possible URLs into a cache that you can check against the incoming request, and then you can supply the route values manually by creating a custom RouteBase as in this answer. You just need to change the PageInfo object to hold all 3 of your GUID values and update the routes accordingly. Do note however it would be better (less logic to implement) if you did a table join and came up with a single GUID to represent each URL as in the linked answer.

public class PageInfo
{
public PageInfo()
{
CompanyId = Guid.Empty;
BranchId = Guid.Empty;
DepartmentId = Guid.Empty;
}

// VirtualPath should not have a leading slash
// example: events/conventions/mycon
public string VirtualPath { get; set; }
public Guid CompanyId { get; set; }
public Guid BranchId { get; set; }
public Guid DepartmentId { get; set; }
}

In GetRouteData

result.Values["controller"] = "CustomPage";
result.Values["action"] = "Details";

if (!page.CompanyId.Equals(Guid.Empty))
{
result.Values["companyId"] = page.CompanyId;
}

if (!page.BranchId.Equals(Guid.Empty))
{
result.Values["branchId"] = page.BranchId;
}

if (!page.DepartmentId.Equals(Guid.Empty))
{
result.Values["departmentId"] = page.DepartmentId;
}

In TryFindMatch

Guid companyId = Guid.Empty;
Guid branchId = Guid.Empty;
Guid departmentId = Guid.Empty;

Guid.TryParse(Convert.ToString(values["companyId"]), out companyId);
Guid.TryParse(Convert.ToString(values["branchId"]), out branchId);
Guid.TryParse(Convert.ToString(values["departmentId"]), out departmentId);

var controller = Convert.ToString(values["controller"]);
var action = Convert.ToString(values["action"]);

if (action == "Details" && controller == "CustomPage")
{
page = pages
.Where(x => x.CompanyId.Equals(companyId) &&
x.BranchId.Equals(branchId) &&
x.DepartmentId.Equals(departmentId))
.FirstOrDefault();
if (page != null)
{
return true;
}
}
return false;

Route attribute routing with query strings when there are multiple routes

You can merge the two actions in question into one

[HttpGet]
[Route("Cats")]
public IHttpActionResult GetCats(int? catId = null, string name = null) {

if(catId.HasValue) return GetByCatId(catId.Value);

if(!string.IsNullOrEmpty(name)) return GetByName(name);

return GetAllCats();
}

private IHttpActionResult GetAllCats() { ... }

private IHttpActionResult GetByCatId(int catId) { ... }

private IHttpActionResult GetByName(string name) { ... }

Or for more flexibility try route constraints

Referencing Attribute Routing in ASP.NET Web API 2 : Route Constraints

Route Constraints

Route constraints let you restrict how the parameters in the route
template are matched. The general syntax is "{parameter:constraint}".
For example:

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

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

Here, the first route will only be selected if the "id" segment of the
URI is an integer. Otherwise, the second route will be chosen.

MVC routing - Query strings are not applied when default is present

To explain the behavior, the 3rd argument of MapRoute is (my emphasis)

An object that contains default route values.

By specifying new { controller = "Product", action = "List", page = 1 } you are defining a route value for page (even though its not a segment in your url definition) and giving it a default value of 1.

Now when you navigate to ../chess?page=2 it matches your Default23 route, and the value of 'chess' is assigned to the {category} segment, but nothing is assigned to page because there is no segment for {page} (its a query string value).

When your List(string category, int page = 1) method is executed, the DefaultModelBinder evaluates values for binding in the following order

  1. Previously bound action parameters, when the action is a child
    action
  2. Form values
  3. JSON Request body (ajax calls)
  4. Route data
  5. Query string parameters
  6. Posted files

For a GET, 1, 2, 3 and 6 are not applicable, so the DefaultModelBinder first evaluates the Route data (RouteData.Values) and finds a value of "chess" for category (from the url). It also finds a value of "1" for page (because you defined a default value for it in the route definition).

At this point you have category="chess", page=1.

The DefaultModelBinder then evaluates the Query string parameters (Request.QueryString) and finds a value of "2" for page, but because page already has been set, its ignored. By default, the DefaultModelBinder binds the first match it finds and ignores all subsequent matches (unless binding to an IEnumerable property).

So at this point (the end of the binding process) you still have category="chess", page=1.

Attribute routing recognize optional query string parameters

Use the FromUri attribute and make to optional


[HttpGet, Route("{id}/overview/")]
public async Task Overview(string id, [FromUri]DateTime from, [FromUri]DateTime? to = null)
{
...
}

To expand on this the id parameter is picked up because you have specified it in your route, the framework has seen a matching route and tried to call the method which matches, even though the route is missing the remaining parameters it has tried to pull them from the query string.

You then get your 404 as no method matched your call, this was due to the to DateTime being nullable but not optional.

Hope this helps



Related Topics



Leave a reply



Submit