I need to be able to have the same route to get two different kinds of output.
/api/v1/items?ids=1,2,3
should retrieve the list of items containing those three entries, however
/api/v1/items?from=142523&limit=4
should retrieve a cursor paginated response, where from would be the id of the item.
I know that in the past it was possible to create route constraints based on querystring, but that has been removed according to the answer posted here:
Query parameter route constraints
What would be the best way to tackle this? Attribute routing is a no-go, as we don't want to have a items/{list-of-ids} routes in the application. I could merge the methods into one with optional parameters, but then the API output is inconsistent in the automatically generated documentation (paginated vs non-paginated response). Is it possible to achieve what I want using custom route constraints?
You can use Optional Parameter to define your API endpoint like below. Your query string values will automatically get parameter binded if you keep the parameter name same and don't have to define separate route for them
[HttpGet]
public IActionResult items(string ids, int from = 0, int limit = 0)
{
//code here
}
Related
I have a requirement to inspect routes in .Net Core middleware and perform different actions based on different route. For this I have to do route comparisons for below sample routes returned from HttpContext.Request.Path:
/api/employees/2d50f670c72911ec861760be36a6c36a/authorise
/api/employees/9d50f670c72911ec861760be36a6c36a/decline
Because of dynamic ID present in the path, I have to split and join the string using below code:
route = "/api/employees";
var pathSegments = request.Path.Value.Split('/');
if (pathSegments.Length > 3 && Guid.TryParse(pathSegments[3], out Guid employeeId))
{
route += #"/{employeeId}/"+string.Join("/", pathSegments.Skip(4));
}
While this works okay for URL comparison, I wanted to know if this can be done in a simpler way like UriTemplate class provides in WCF?
Instead of the first two lines you wrote which you tried to partition the url, You can use the property Segments of the HttpContext.Url object. It gives you a string array contains all segments.
BUT, I guess you're performing a wrong logic. Why don't you just do the desired action in an actual Action?
I mean the urls you mentioned to compare, surely call an Action(Controller Method), right? If so, why don't you just execute your desired actions in them?
For instance, if the /api/employees/2d50f670c72911ec861760be36a6c36a/authorise request must do a logic before the requested action (and this logic is done for every request), why should we do it in a middleware?
Because as far as I know, the middlewares are used for cross-cutting concerns.
Given Attribute Routing in ASP.Net Core (but I guess MVC and WebAPI work the same way), I want to be able to do something like this:
[Route("api/[controller]")]
public class SampleController : Controller {
// GET api/sample/123e4567-e89b-12d3-a456-426655440000/folder/subfolder/file.css
[HttpGet("{id}")] // this is wrong, how should it be written correctly?
public string Get(Guid id, string urlSuffix) {
return null; // return stuff based on the id and the full url
}
}
in the URL taken as example in the comment (api/sample/123e4567-e89b-12d3-a456-426655440000/folder/subfolder/file.css) the SampleController.Get method should be called with the following parameters:
id: 123e4567-e89b-12d3-a456-426655440000
urlSuffix: folder/subfolder/file.css or /folder/subfolder/file.css (I don't really care for the leading /)
If there are additional query parameters, these should be included in the suffix as well.
I thought about using the raw request URL, but I'd still need a way to specify an action which is executed and what ever I thought of was too late, ASP.Net already figured out that there isn't any URL for the given action.
I would like to use controllers for this, instead of adding some "raw" code into the ASP.Net Core execution pipeline.
Update:
This exact example doesn't work for me with asp.net core dotnet core and kestrel service:
[Route("api/[controller]")]
public class SampleController : Controller
{
// GET api/values/5
[HttpGet("{id}/{urlSuffix}")]
public object Get(string id, string urlSuffix)
{
return new {id, urlSuffix};
}
}
When I call http://localhost:5000/api/sample/some-id/folder I get the correct result, but when I call http://localhost:5000/api/sample/some-id/folder/subfolder/file.extension I get a 404 error.
Referencing: Handling a Variable Number of Segments in a URL Pattern
Sometimes you have to handle URL requests that contain a variable
number of URL segments. When you define a route, you can specify that
if a URL has more segments than there are in the pattern, the extra
segments are considered to be part of the last segment. To handle
additional segments in this manner you mark the last parameter with an
asterisk (*). This is referred to as a catch-all parameter. A route
with a catch-all parameter will also match URLs that do not contain
any values for the last parameter.
Your template and placeholders will change to ...
[HttpGet("{id:guid}/{*urlSuffix}")]
Given the following URL ...
"api/sample/123e4567-e89b-12d3-a456-426655440000/folder/subfolder/file.css"
then
id = 123e4567-e89b-12d3-a456-426655440000
urlSuffix = "folder/subfolder/file.css"
Because the / is already part of the template, it will be excluded from the urlSuffix parameter.
the *urlSuffix acts as a catch all for everything after the {id}/ in the URL. If there are additional query parameters, these will also be included in the urlSuffix as well.
You were getting the not found error because your example URL could not find a matching route of api/sample/{id}.
I included the :guid route constraint based on your original example expecting a Guid for id parameter.
If the id is not going to be a Guid always you can remove the constraint and it will work for your updated example.
I have an API action defined as the following:
[Route(Name="GetMembersTest"), HttpGet, ResponseType(typeof(MemberHeadersDto))]
public IHttpActionResult GetMembers[FromUri]MemberFilterDto filter, [FromUri]PagingOptionsDto paging)
This method works as expected, routing and all, requests are flowing through just fine. However, I'd like to supply a "NextUri" for paging so that the caller can just keep following NextUri until it is null to get all the results. I have to send back a uri to the same action, 1 page ahead, if that makes sense.
So I tried using UrlHelper.Route. This route is named "GetMembers" for the purpose of this example.
NextUri = Url.Route("GetMembers", new { filter, paging });
The problem is that instead of getting something like
/v1/members?filter.q=&filter.otherproperty=&paging.count=10&paging.startRow=11
I get
/v1/members?filter=WebApi.Models.MemberFilterDto&paging=WebApi.Models.PagingOptionsDto
It looks like UrlHelper.Route doesn't support complex types in the [FromUri] parameter of a GET Request. Is there anything I can do to get this functionality? My workaround right now is to take in all the Dto properties as individual parameters then build my Dtos from them on the server. This isn't ideal because if I ever add any more options I'd have to add more parameters to the action, which also makes the route value dictionary more fragile as well because it has to match with the method signature in UrlHelper.Route(routeName,routeValues).
Unfortunately, there is no way to pass in complex object to routing. Instead, you will need to pass in the simple properties individually.
I was not able to find a way to extend Url.Route, but that would be/have been your best option.
I'm trying to add AttributeRouting to my WebAPI project.
On one controller I currently have three GET methods defined:
[GET("dictionaries")]
public IEnumerable<Dictionary> Get()
[GET("dictionaries/{id}")]
public Dictionary GetByID(int id)
[GET("dictionaries/{dictionaryID}/{page}")]
public Dictionary Browse(long dictionaryID, int page)
The first two routes are working as I expect them to, but the third always returns a 405 METHOD NOT ALLOWED.
I've tried sending the parameters in the URL, and the query-string, and it's the same response for both. When I've tried the query-string version, I've modified the route to be
[GET("dictionaries?dictionaryID={dictionaryID}&page={page}
I've also tried changing the initial word from dictionaries to dictionary to avoid any ambiguity with the other two GET routes, but still no success.
The documentation for AttributeRouting only mentions query-strings in relation to parameter constraints (which aren't available for me because of the WebHost framework) and doesn't mention how query-strings can be used in routing.
Can anyone tell me how I can achieve this third route with those two parameters, or do I have to drop AttributeRouting or try on a controller of its own?
Web API action selector implicitly thinks that the third action here is a POST, since it doesn't start with verbs like GET, POST, PUT etc. Try adding HttpGet attribute also and see if this works.
[HttpGet, GET("dictionaries/{dictionaryID}/{page}")]
public Dictionary Browse(long dictionaryID, int page)
I have a fixedURL to which I'd like to post different types of xml message, deserialized using DataContracts. Depending on the type of the deserialized message, I'd like to route to:
overloaded methods, e.g.
void Process(ContractType1 request) {}
void Process(ContractType2 request) {}
So at some point I need to deserialize this message and hopefully allow the default routing rules to match the correct method. Which extensibility point should I use for this? Or even better, can I make this work out of the box?!
If it makes any difference, I'm using MVC 3.
ASP NET MVC does not respect the overload if they are not decorated for different HTTP methods - e.g. one for POST, other for GET.
You need to use [ActionName(Name = "Process2")] to change the route name. And you will have to use different routes to access (if the HTTP methods are the same)
Have a look here.
Apart from the technical workaround, passing different contracts to the same URL is against the REST principles. Data could be in different format (XML, JSON, ...) but it must be the same. The URI defines a unique intention. Now it is possible to have a common dumpster where documents are all dumped to the same URI but then ASP NET MVC default model binder would not be able to help you and you need to create your own model binder.
Contrary to the other answer I say this is possible
Asp.net MVC is a great platform that can be easily extended. And so basically I've written a special action method selector that makes it possible to write overloads that can serve the same HTTP method but defer in parameters. By default you'd get runtime error that action method can't be resolved. But when you use this action method selector you get rid of this error.
Basically if your parameter classes have distinct parameter names, you can actually select methods by that.
Action method selector is called RequiresRouteValuesAttribute and a typical usage scenario would be with default route where id is optional in:
{controller}/{action}/{id}
This means that you either have to write
public ActionResult Index(int? id)
{
if (id.HasValue)
{
// display details view
}
else
{
// display master view
}
}
but by using my action method selector you can easily write two action methods:
public ActionResult Index()
{
// display master view
}
[RequiresRouteValues("id")]
public ActionResult Index(int id)
{
// display details view
}
The same could be applied to your action methods as long as your custom types have distinct property names or methods use different parameter names. So in your case it could be something like:
[RequiresRouteValues("first.Id")] // when you provide prefix with your form
// or
[RequiresRouteValues("Some.ContractType1.Distict.Property.Name")]
public ActionResult Process(ContractType1 first)
{
// do what's appropriate
}
[RequiresRouteValues("second.Id")] // when you provide prefix with your form
// or
[RequiresRouteValues("Some.ContractType2.Distict.Property.Name")]
public ActionResult Process(ContractType2 second)
{
// do what's appropriate
}
Read all the details about this action method selector and get the code as well.