I have a route in Microsoft.AspNet.WebApi.Core.5.2.7 which incorrectly matches a query parameter with a route value.
My controller and action is:
[RoutePrefix("v0/departments/{projectId}")]
[Resource("Collections")]
public class ExampleController : V0BaseContentController
{
[Route("collections/{identifier}")]
[HttpGet]
public async Task<IHttpActionResult> QueryCollection(int projectId, string identifier)
{
var result = Request.RequestUri.Query;
return await Run(projectId, (p, v) => QueryInternal(p, v, identifier, LangCode));
}
When my query parameter (?id=1234) doesn't match the name of the route it works as expected:
http://localhost:5171/v0/departments/66/collections/projects?id=1234
In this case the values in my controller action are as follows:
projectId = 66
identifier = "projects"
Request.RequestUri.Query = "?id=1234"
Correctly routed values
If my query parameter has the name matching the route value of 'identifier' this matches the /{identifier} route. IE:
http://localhost:5171/v0/departments/66/collections/projects?identifier=1234
In this case the values in my controller action are as follows:
projectId = 66
identifier = 1234
Request.RequestUri.Query = "?identifier=1234"
In this instance how do I make sure the identifier route value is set to the value of 'projects' ?
Incorrectly matching
Related
I have the following API controller class.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
namespace Core31Test.Controllers
{
[ApiController]
[Route("[controller]")]
public class DataController : ControllerBase
{
[HttpGet]
[Route("query/{idr:int?}")]
public string Get(int idr, [FromQuery] int id)
{
var idx = id == 0 ? idr : id;
return $"Value: {idx}";
}
[HttpGet]
[Route("query/{cityr}")]
public string GetByCity(string cityr, [FromQuery] string city)
{
var cityx = string.IsNullOrEmpty(city) ? cityr : city;
return cityx;
}
}
}
When I attempt to query by an id, both the route based path and the query string work. When I attempt to query by city, only the route based path works. The querystring path ends up taking the incorrect path.
For example:
http://localhost:51123/data/query/1
Result: value: 1
http://localhost:51123/data/query?id=1
Result: value: 1
http://localhost:51123/data/query/mycity
Result: mycity
http://localhost:51123/data/query?city=acity
Result: value: 0
In the last case, the incorrect route is being selected. Why is this happening and how can I fix it?
Edit 1
If I modify the route for the GetByCity method to be the one given below, the Get method is still selected. In this case, both methods have an optional route parameter and a querystring. Since the Get route specifies the input is an integer, I do not understand why the GetByCity method is going to that one. What I would like to know is how to make this work.
[Route("query/{cityr?}")]
In the last case, the incorrect route is being selected. Why is this happening and how can I fix it?
No, it's not. The correct and expected route is being selected. Let's take a closer look:
The route is query?city=acity
The available actions are:
query/ + optional int parameter (query/{idr:int?})
query/ + required string parameter ("query/{cityr}")
If you think about it, query/ does not satisfy query/ + required string parameter, as no value for cityr was given, so the correct route is the idr overload since that parameter is optional.
How can I make the possibility to accept the requests in different orders and with some optional parameters?
https://localhost:44314/api/courses/page=2&pageSize=6&language=test&institution=test&area=test
https://localhost:44314/api/courses/page=2&pageSize=6&institution=test&area=test
https://localhost:44314/api/courses/page=2&pageSize=6&area=test&language=test
I have tried as below:
[HttpGet]
[Route("page={page:int}&pageSize={pageSize:int}&language={language?}&institution={institution?}&area={area?}")]
public async Task<ActionResult<CourseViewModel>> ListCourses(int page, int pageSize, string language="", string institution="", string area="")
And I have the error as:
System.ArgumentException: 'An optional parameter must be at the end of the segment. In the segment
'page={page}&pageSize={pageSize}&language={language?}&institution={institution?}&area={area?}',
optional parameter 'language' is followed by '&institution='.
Parameter name: routeTemplate'
Remove the route template and the route table will use the parameters of the action for matching the route via query string in the requested URL
//GET api/courses?page=2&pageSize=6&language=test&institution=test&area=test
//GET api/courses?page=2&pageSize=6&institution=test&area=test
//GET api/courses?page=2&pageSize=6&area=test&language=test
[HttpGet]
[Route("")]
public async Task<ActionResult<CourseViewModel>> ListCourses(int page, int pageSize, string language = "", string institution = "", string area = "")
In this case the order does not matter. Once they are present to be matched.
[HttpGet]
[Route("students")]
[SwaggerOperation(Tags = new[] { "Student" })]
[SwaggerResponse(HttpStatusCode.OK, Type = typeof(ResponseModel<IList<Student>>))]
[SwaggerResponseExample(HttpStatusCode.OK, typeof(StudentResponseExample))]
[SwaggerResponse(HttpStatusCode.InternalServerError)]
public IHttpActionResult SearchStudent()
{
IDictionary<string, string> searchParams = null;
searchParams = ControllerContext.GetQueryStrings();
.
.
.
}
The above API has three optional parameters which will be pass as query string
SyncDate - Long
OffSet - int
Limit - int
There is no option for user to enter these optional query parameters in swagger UI. Please guide me to implement the optional query parameters.
I am using swashbuckle and I prefer to use annotations rather than having a lengthy comment section over each API method for swagger functionalities.
I referred the following Adding Query String Params to my Swagger Specs and created the SwaggerParameterAttribute class in Filters folder of Web API and when trying to add the OperationFilter in GlobalConfiguration.Configuration
.EnableSwagger as given, it throws type or the namespace name SwaggerParametersAttributeHandler could not be found. I even added the Filters folder namespace but still the error exists.
Please guide on how to implement the optional query parameters in swagger
The way Swagger works it pulls out parameters based on your signature of Action i.e parameters to your Action, but here you are getting these value from ControllerContext which obviously Swagger will never be aware of.
So You need to change the signature of the Action and pass your parameters there.
They will be treated as optional if you make them of nullable type -
[HttpGet]
[Route("students")]
[SwaggerOperation(Tags = new[] { "Student" })]
[SwaggerResponse(HttpStatusCode.OK, Type = typeof(ResponseModel<IList<Student>>))]
[SwaggerResponseExample(HttpStatusCode.OK, typeof(StudentResponseExample))]
[SwaggerResponse(HttpStatusCode.InternalServerError)]
public IHttpActionResult SearchStudent(long? SyncDate = null,int? OffSet = null,int? Limit = null)
{
// Use the variables here
.
.
.
}
This worked for me:
[System.Web.Http.HttpGet]
[Route("api/DoStuff/{reqParam}")]
[Route("api/DoStuff/{reqParam}/{optParam1:alpha?}/{optParam2:datetime?}")]
public string Get(string reqParam, string optParam1= "", string optParam2= "")
It did create two sections in my Swagger UI but that works for me.
Have this Web API controller:
[RoutePrefix("api/echo")]
public class EchoController : ApiController
{
[HttpGet]
[Route("{userId}/{message}")]
public async Task<IHttpActionResult> Echo(string userId, string message, string queryString)
{
await Task.Delay(150);
return Ok(new {Action = "Echo", UserId = userId, Message = message, QueryString = queryString});
}
}
A legit route executing this API would look like this: /api/echo/johni/hello?querystring=1
As you can see, this dummy API receives 3 inputs:
userId from the route
message from the route
querystring from the query params
All 3 of them, are available as dictionary here: httpActionContext.ActionArguments.
Question is, given the HttpActionContext, how would one distinguish between the route / query parameters?
Found it.
X = httpActionContext.ActionArguments - all parameters (route / query).
Y = httpActionContext.RequestContext.RouteData.Values - route parameters only.
Which means, X - Y - query params only.
I am new to attribute routing, and I am not sure if this is even possible.
I have an attribute route, which works fine like this:
[HttpGet]
[Route("GetIssuesByFlag/{flag:int=3}")]
public IEnumerable<IssueDto> GetIssuesByFlag(int flag)
Now I want to add some extra optional parameters to narrow down my search, so I want to add 2 extra optional parameters.
What I have tried:
[HttpGet]
[Route("GetIssuesByFlag/{flag:int=3?}/{categoryId:int?}/{tagIds?}")]
public IEnumerable<IssueDto> GetIssuesByFlag(int flag , int? categoryId = null, int?[] tagIds = null)
This works fine if my call is /api/controller/1/2, but fails with 404 when it comes to /api/controller/1.
How can I achieve this?
Edit 1: Nkosi's answer below worked, however an extra modification was needed.
[HttpGet]
[Route("GetIssuesByFlag/{flag:int=3}/{tagIds?}/{categoryId:int?}")]
public IEnumerable<IssueDto> GetIssuesByFlag(int flag , List<int> tagIds, int? categoryId = null )
The list or array must be second as it is automatically null if no value is provided and cant be marked as optional with = null.
{flag:int=3?} is the problem. it is either optional {flag:int?} with the default value in the action or {flag:int=3}.
[HttpGet]
Route("GetIssuesByFlag/{flag:int=3}/{categoryId:int?}/{tagIds?}")]
public IEnumerable<IssueDto> GetIssuesByFlag(int flag , int? categoryId = null, int?[] tagIds = null)
You currently have 3 optional parameters. when you have just the 1 value routing table wont know which optional parameter you are referring to, hence the 404
Use a query string.
[HttpGet]
[Route("GetIssuesByFlag/{flag:int=3?}")]
public IEnumerable<IssueDto> GetIssuesByFlag(int flag, List<int> tagIds, int? categoryId = null)
Url: /getissuesbyflag/1?tagIds=2,5,6&categoryId=56
You really should use query strings for optional parameters and path parameters if they are required.