I have a problem with all my controller endpoints, whenever I use sort and filter as query parameter in a way similar to this example:
[Microsoft.AspNetCore.Mvc.HttpGet,
Microsoft.AspNetCore.Mvc.Route("{tenantId}/archive/retrievals", Name = "getRetrievalList")]
public System.Threading.Tasks.Task<RetrievalListResult> GetRetrievalList([Microsoft.AspNetCore.Mvc.FromQuery] int? limit = null, [Microsoft.AspNetCore.Mvc.FromQuery] int? offset = null, [Microsoft.AspNetCore.Mvc.FromQuery] System.Collections.Generic.IEnumerable<string> sort = null, [Microsoft.AspNetCore.Mvc.FromQuery] System.Collections.Generic.IDictionary<string, string> filter = null)
{
throw new NotImplementedException();
}
It works perfectly fine with these calls:
{{baseUrl}}/:tenantId/archive/retrievals?filter[fields]=Text 01
{{baseUrl}}/:tenantId/archive/retrievals?sort=name&filter[fields]=Text 01
But when I only use sort…
{{baseUrl}}/:tenantId/archive/retrievals?sort=name
...then suddenly both sort and filter variables are filled, creating a false filter parameter.
Any solutions for that?
P.S. using only filter=id results in none of the variables being filled which is as I expect.
For Dictionary targets, model binding looks for matches to parameter_name or property_name. If no match is found, it looks for one of the supported formats without the prefix.
In other words, "sort=name" matches the format of the supported target parameter Dictionary<string, string> named filter.
Therefore, it will also be automatically bound to the filter.
You can specify the prefix to solve the problem.
public IEnumerable<WeatherForecast> Get(
[FromQuery] int? limit = null,
[FromQuery] int? offset = null,
[FromQuery] IEnumerable<string> sort = null,
[FromQuery][Bind(Prefix = "filter")]IDictionary<string, string> filter = null
)
Related
I have a querystring with a number of optional values eg.
/filter?location=scotland&minprice=100&maxprice=500
I have a filter method with a number of optional arguments
public List<result> Filter(
bool isVisible = false,
string location = null,
int? minPrice = null,
int? maxPrice = null,
)
I would like to use the querystring values as arguments on the filter method, but I can't think of a good way to do this.
The best way I can come up with is to use ?: conditions for each argument
var results = Filter(
(Request.QueryString["isvisible"] != null ? Request.QueryString["isvisible"] == "true" : false)
and so on...
but using this method, if a particular querystring value hasn't been set, I have to pass in a default value, but I don't want to pass in a default, I want defaults to be set from within the filter method.
Is there a way to do this? Or is there a better way to achieve the same thing?
Sounds like you just need to map the defaults.
There are actually two "interfaces" here: the interface from the caller to the Filter() method and the interface from the Filter() method to the data source. They don't have to have the same defaults.
public List<Whatever> Filter(string criteria1 = null, string criteria2 = null)
{
criteria1 = criteria1 ?? SYSTEM_DEFAULT_FOR_CRITERIA1;
criteria2 = criteria2 ?? SYSTEM_DEFAULT_FOR_CRITERIA2;
return _dataSource.GetData(criteria1, criteria2);
}
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.
I have seen this answer describing ASP.NET support for keyless (not valueless) parameters, like http://some.url?param1¶m2, and confirmed them to be viewable on Request.QueryString like:
var values = this.Request.QueryString.GetValues(null);
values.Any(o => o == "param1");
This is fine and dandy but now I want to generate urls like this. My first intuition was to use the RouteValueDictionary: routeValues parameter of Url.Action with null as a key:
#{
var dict = new RouteValueDictionary();
dict.Add(null, "param1");
dict.Add(null, "param2");
}
Very link, amaze
But apparently C# forbids nulls as dictionary keys because of reasons.
I have also tried the empty string as the key, but it results in a query string like: ?=param1,=param2 which contains 2 more equal signs that I want it to.
Of course I can string manipulate the heck out of my URL and add the ¶m1 part to the query string, but I was hoping for a concise solution.
You want to add the key values, but leaving the value null isn't allowed.
RouteValueDictionary ignores empty values
You could add a value like 1 for instance, but you lose your fine and dandy solution.
#{
var dict = new RouteValueDictionary();
dict.Add("param1",1);
}
Very link, amaze
For another solution you will have to write some custom code.
Since there's no built-in helper for this why don't you roll your own:
public static class UrlHelperExtensions
{
public static string MyAction(this UrlHelper urlHelper, string actionName, IList<string> parameters)
{
string url = urlHelper.Action(actionName);
if (parameters == null || !parameters.Any())
{
return url;
}
return string.Format("{0}?{1}", url, string.Join("&", parameters));
}
}
and then:
#{
var parameters = new List<string>();
parameters.Add("param1");
parameters.Add("param2");
}
#Url.MyAction("ActionName", parameters)
I want to handle POST of the following API-Call:
/v1/location/deviceid/appid
Additional Parameter are coming from the Post-Body.
This all works fine for me. Now I wnat to extend my code by allowing "deviceid" and/or "appid" and/or BodyData to be null:
/v1/location/deviceid
/v1/location/appid
/v1/location/
These 3 URLs should responded by the same route.
My first approach (BodyData required):
[Route("v1/location/{deviceid}/{appid}", Name = "AddNewLocation")]
public location_fromuser Post(string deviceid = null, string appid = null, [FromBody] location_fromuser BodyData)
{
return repository.AddNewLocation(deviceid, appid, BodyData);
}
This does not work and returns a compile error:
"optional Parameters must be at the end"
Next try:
[Route("v1/location/{deviceid}/{appid}", Name = "AddNewLocation")]
public location_fromuser Post([FromBody] location_fromuser BodyData, string deviceid = null, string appid = null)
Now my function AddNewLocation() get always an BodyData=null - even if the call send the Body.
Finally I set all 3 Parameter optional:
[Route("v1/location/{deviceid}/{appid}", Name = "AddNewLocation")]
public location_fromuser Post(string deviceid = null, string appid = null, [FromBody location_fromuser BodyData = null)
Don´t work:
Optional parameter BodyData is not supported by FormatterParameterBinding.
Why do I want a solution with optional Parameters? My Controller handles just the "adding of a new Location" via a POST.
I want to send on wrong data my own exceptions or error messages. Even if the call has missing values. In this case I want to be able to decide to throw an exception or Setting Defaults by my code.
For an incoming request like /v1/location/1234, as you can imagine it would be difficult for Web API to automatically figure out if the value of the segment corresponding to '1234' is related to appid and not to deviceid.
I think you should change your route template to be like
[Route("v1/location/{deviceOrAppid?}", Name = "AddNewLocation")] and then parse the deiveOrAppid to figure out the type of id.
Also you need to make the segments in the route template itself optional otherwise the segments are considered as required. Note the ? character in this case.
For example:
[Route("v1/location/{deviceOrAppid?}", Name = "AddNewLocation")]
Another info: If you want use a Route Constraint, imagine that you want force that parameter has int datatype, then you need use this syntax:
[Route("v1/location/**{deviceOrAppid:int?}**", Name = "AddNewLocation")]
The ? character is put always before the last } character
For more information see: Optional URI Parameters and Default Values
An additional fact to complement #Kiran Chala's answer -
When we mark any parameter (appid) as optional in the action URI using ? character(for nullable value types) then we must provide default value to the parameter in the method signature as shown below:
[Route("v1/location/{deviceid}/{appid}", Name = "AddNewLocation")]
public location_fromuser Post(string deviceid, int? appid = null)
Ok, I fallen here with my internet research and I continue my way, because the accepted solution not working with dotnet core 3.1.
So here is my solution, following this doc
[HttpPost]
[Route("{name}")]
[Route("{name}/parent/{parentId}")]
public async Task<IActionResult> PostSomething(string name, Guid? parentId = null)
{
return Ok(await Task.FromResult(new List<string>()));
}
By this way many routes go to this single API function
I followed this example:
ASP.NET MVC - Pass array object as a route value within Html.ActionLink(...)
But, my Action is always called with null. What am I doing wrong?
foreach (OrderDetail od in order.OrderDetails)
{
rvd.Add("key" + count++, productID);
rvd.Add("key" + count++, productName);
}
#Html.ActionLink(linkText, "Renew", "Orders", rvd, new Dictionary<string, object>())
The query string is correctly generated, like ?key0=dog&key1=cat&key2=fish..., but I get a null parameter in my Action below:
public ActionResult Renew(RouteValueDictionary rvd)
{
// 'rvd' is null here!
}
Please note: I don't know the number of parameters in advance.
The query string is correctly generated, like ?key0=dog&key1=cat&key2=fish...
No, this is not a correct url. A correct url would have looked like this:
?%5B0%5D.Key=123&%5B0%5D.Value=dog&%5B1%5D.Key=456&%5B1%5D.Value=cat...
which would have mapped to:
public ActionResult Renew(Dictionary<int, string> rvd)
{
...
}
You could write a custom ActionLink to generate this url:
public static class LinkExtensions
{
public static IHtmlString MyActionLink(
this HtmlHelper html,
string linkText,
string actionName,
string controllerName,
IDictionary<string, string> parameters
)
{
var a = new TagBuilder("a");
var urlHelper = new UrlHelper(html.ViewContext.RequestContext);
var query = string.Join("&", parameters.Select((x, i) => string.Format("[{0}].Key={1}&[{0}].Value={2}", i, urlHelper.Encode(x.Key), urlHelper.Encode(x.Value))));
var url = string.Format(
"{0}?{1}",
urlHelper.Action(actionName, controllerName, null, html.ViewContext.HttpContext.Request.Url.Scheme),
query
);
a.Attributes["href"] = url;
a.SetInnerText(linkText);
return new HtmlString(a.ToString());
}
}
which you could use like this in your view:
#Html.MyActionLink(
linkText,
"Renew",
"Orders",
order.OrderDetails.ToDictionary(x => x.ProductID.ToString(), x => x.ProductName)
)
You can read more about the correct wire format for binding to various collections in this blog post.
I imagine what is happening is you are expecting the model binder to bind your array to a RouteValueDictionary, but the model binder doesn't know that key0=dog&key1=cat&key2=fish is supposed to be a dictionary. I would recommend changing your code to accept a string array. To do this, your query string needs to look something like this: ?rvd=dog&rvd=cat&rvd=fish
And your Action...
public ActionResult Renew(string[] rvd)
{
// 'rvd' is no longer null here!
}
The important part is rvd is the parameter name in your action, as well as the name of each element in the querystring: ?rvd=dog&rvd=cat&rvd=fish. If you really want to use a dictionary instead of a string array, then your querystring should look like this: ?rvd[0]=dog&rvd[1]=cat&rvd[2]=fish, giving each item an array index, but you would probably have to change your parameter from RouteValueDictionary to Dictionary<string,string>, I'm not quite sure. More info here. EDIT: See Darin's comment about binding to a dictionary, as I believe his is correct.
You may have to write your own extension for Html.ActionLink that accepts an array (or whatever OrderDetails is) and creates the querystring as an array. This looks like a pretty good starting place.