How to retrieve a route template variable given the HttpRequestMessage? - c#

I have two API controllers located in two different folders which has GetEvents() action:
V1
EventsController
V2
EventsController
I'd like to be able to access the controllers using:
api/v1/events and api/v2/events
my route template looks like this
api/v{version}/{controller}
private static string GetRequestVersion(HttpRequestMessage request)
{
// how to find the {version} value from url?
}
I've written a code using REGEX which works fine:
private static string ExtractVersionFromUrl(HttpRequestMessage request)
{
Match match = Regex.Match(request.RequestUri.PathAndQuery, #"/v(?<version>\d+)/", RegexOptions.IgnoreCase);
return match.Success ? match.Groups["version"].Value : null;
}
What's the standard approach to retreive such data from uri?

Try,
Request.GetRouteData().Values["version"];
GetRouteData is an extension method and is available in the namespace System.Net.Http.

I'm hazarding a guess regarding the return value. Another way to do this is simply define it as a parameter in your method in the controller like so:
public IEnumerable<Event> GetEvents(string version, HttpRequestMessage request) {
...
}
I just figured this out myself. Web API is just beautiful!

Related

The request matched multiple endpoints on .NET Core

I use OpenAPI (Swagger) in a .NET Core project and when using multiple methods that have similar get requests, I encounter "Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints." error during runtime. I look at several pages on the web and SO and tried to apply the workarounds like The request matched multiple endpoints but why?, but it does not fix the problem. Here are the API methods and route definitions that I use.
[Route("get", Name="get")]
public IEnumerable<DemoDto> Get()
{
//
}
[Route("get/{id}", Name="getById")]
public DemoDto GetById(int id)
{
//
}
[Route("get/{query}", Name="getWithPagination")]
public IEnumerable<DemoDto> GetWithPagination(DemoQuery query)
{
//
}
I use Name property in order to fix the problem but not solved. Any idea to make changes on the routes to differentiate Get() and GetWithPagination()?
You have two endpoints with equals routes:
get/{id} and get/{query}.
If you write in browser line: get/123, the system can't understand what route to use, because they have the same pattern.
You need to distinguish them and I suggest you use restful style for routes, like:
item/{id},
items?{your query}
[Route("get/{query}", Name="getWithPagination")]
This doesn't make sense. DemoQuery is an object, it can't be represented by a single part of a url. You can tell the ModelBinder to build your object from multiple query parameters, though.
The routing engine is getting this route confused with the [Route("get/{id}", Name="getById")] route. They both appear to match get/blah.
In addition to fixing your DemoQuery route, try adding a route constraint on the id route -
[Route("get/{id:int}", Name="getById")]
to better help the engine.
To get DemoQuery to work, assume it looks something like:
public class DemoQuery
{
public string Name { get; set; }
public int Value { get; set; }
}
Then change your action to
[Route("getPaged/{query}", Name="getWithPagination")]
public IEnumerable<DemoDto> GetWithPagination([FromQuery] DemoQuery query)
and call then endpoint like /getPaged?name=test&value=123. And the ModelBinder should build your object for you.
ASP.NET Web API 2 supports a new type of routing. Offical Doc
Route constraints let you restrict your parameters type and matched with these types (int, string, even date etc). The general syntax is "{parameter:constraint}"
[Route("users/{id:int}")]
public User GetUserById(int id) { ... }
[Route("users/{name}")]
public User GetUserByName(string name) { ... }
I tested at API;
//match : api/users/1
[HttpGet("{id:int}")]
public IActionResult GetUserById(int id){ ... }
//match : api/users/gokhan
[HttpGet("{name}")]
public IActionResult GetUserByName(string name){ ... }

webapi2 Binding not working

I'm new to webapi 2 and trying to get my api to bind to this call below.
Can anyone see what I'm doing wrong?
Call:
https://blabla.azurewebsites.net/myjohndeere/authenticated/e33dd8f74c97474d86c75b00dd28ddf8?oauth_token=1539dccf-d935-4d9e-83be-e00f76cabbb9&oauth_verifier=B22dWL
[RoutePrefix("myjohndeere")]
public class ApiMyJohnDeereController : ApplicationController
{
[HttpGet, Route("authenticated/{callbackId}")]
[SwaggerResponse(HttpStatusCode.OK, Type = typeof(ApiResponseModel))]
[SwaggerResponse(HttpStatusCode.InternalServerError, "An unknown error occurred")]
[SwaggerResponse(HttpStatusCode.BadRequest, "Missing FieldMappings")]
public IHttpActionResult Authenticated(string callbackId,[FromUri]string oauth_token, [FromUri]string oauth_verifier)
{
...
}
First of, you missed the 'api' in the route.
Try it like this
https://blabla.azurewebsites.net/api/myjohndeere/authenticated/e33dd8f74c97474d86c75b00dd28ddf8?oauth_token=1539dccf-d935-4d9e-83be-e00f76cabbb9&oauth_verifier=B22dWL
Then, remove the FromUri attribute from your controller method. The attribute is not needed when just reading querystring of value types.
Try it like this
public IHttpActionResult Authenticated(string callbackId, string oauth_token, string oauth_verifier)
The issue was the Azure API gateway was still set to use PUT rather than GET.

"GetBy" Methods in Web API

We are using .NET Core to build a Web API. We need to support "GetBy" functionality, e.g. GetByName, GetByType, etc. but the issue we are running into is how to depict this through routes in a Restful way as well as the method overloading not properly working with how we think the routes should be. We are using MongoDB so our IDs are strings.
I'm assuming our routes should be something like this:
/api/templates?id=1
/api/templates?name=ScienceProject
/api/templates?type=Project
and the issue is that all our methods in our controller have a single string parameter and isn't properly mapped. Should the routes me different or is there a way to properly map these routes to the proper method?
If the parameters are mutually exclusive, i.e. you only search by name or type but not by name and type, then you can have the parameter be a part of the path instead of the query-params.
Example
[Route("templates")]
public class TemplatesController : Controller
{
[HttpGet("byname/{name}")]
public IActionResult GetByName(string name)
{
return Ok("ByName");
}
[HttpGet("bytype/{type}")]
public IActionResult GetByType(string type)
{
return Ok("ByType");
}
}
This example would lead to routes like:
/api/templates/byname/ScienceProject
/api/templates/bytype/Project
If there parameters are not mutually eclusive then you should do it like suggested in the answer by Fabian H.
You can make a TemplatesController with a single get method, that can take all the arguments.
[Route("api/templates")]
public class TemplatesController : Controller
{
[HttpGet]
public IActionResult Get(int? id = null, string name = null, string type = null)
{
// now handle you db stuff, you can check if your id, name, type is null and handle the query accordingly
return Ok(queryResult);
}
}

asp.net webapi2 catch all route

I'd like to have a catch all route that matches a general route prefix ("api/myaccount/1") execute if there are no more specific routes defined on other controllers (i.e "api/myaccount/1/feature") however I get the following exception when I do this:
Multiple controller types were found that match the URL. This can
happen if attribute routes on multiple controllers match the requested
URL.
As mentioned here:
Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL it seems this may not be possible.
Wanting a default route to execute when no better one is found sounds pretty common so what am I missing? Do I need to hook lower in the pipeline or something...
FYI: I have the catch all working fine ("api/myaccount/1/{*uri}") it's just the being able to override it that's the problem.
Turns out this is pretty easy, I just needed to create a custom Controller Selector and override the GetControllerName function. That particular override is required because the method you would expect to override:
HttpControllerDescriptor SelectController(HttpRequestMessage request)
does not just return the descriptor (or null if It can't find a match) as you may expect. The method actually handles the request for you and returns a 404 :/ However once you're aware of that it is trivial to work around and I was able to get the behavior I wanted using the code below:
using System.Web.Http;
using System.Web.Http.Dispatcher;
public class CustomControllerSelector : DefaultHttpControllerSelector
{
public override string GetControllerName(HttpRequestMessage request)
{
var name = base.GetControllerName(request);
if(string.IsNullOrEmpty(name))
{
return "MyFeature"; //important not to include "Controller" suffix
}
return name;
}
}
And add it to your configuration:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
...
config.Services.Replace(typeof(IHttpControllerSelector),
new CustomControllerSelector(config));
...
}
}

Accessing the query string in MVC 6 Web Api?

I am trying to add a Get() function in a MVC 6 (Asp .Net 5) Web Api to pass a configuration option as a query string. Here are the two functions that I already have:
[HttpGet]
public IEnumerable<Project> GetAll()
{
//This is called by http://localhost:53700/api/Project
}
[HttpGet("{id}")]
public Project Get(int id)
{
//This is called by http://localhost:53700/api/Project/4
}
[HttpGet()]
public dynamic Get([FromQuery] string withUser)
{
//This doesn't work with http://localhost:53700/api/Project?withUser=true
//Call routes to first function 'public IEnumerable<Project> GetAll()
}
I've tried several different ways to configure the routing, but MVC 6 is light on documentation. What I really need is a way to pass some configuration options to the list of Projects for sorting, custom filtering etc.
You can't have two [HttpGet]s with the same template in a single controller. I'm using asp.net5-beta7 and in my case it even throws the following exception:
Microsoft.AspNet.Mvc.AmbiguousActionException
Multiple actions matched. The following actions matched route data and had all constraints satisfied:
The reason for this is that [From*] attributes are meant for binding, not routing.
The following code should work for you:
[HttpGet]
public dynamic Get([FromQuery] string withUser)
{
if (string.IsNullOrEmpty(withUser))
{
return new string[] { "project1", "project2" };
}
else
{
return "hello " + withUser;
}
}
Also consider using Microsoft.AspNet.Routing.IRouteBuilder.MapRoute() instead of attribute routing. It may give you more freedom defining the routes.
Accessing the query string is very doable by using either the RESTful routing conventions (enforced by ASP.NET 5 / MVC 6 by default) or by defining custom routes, as explained in this answer.
Here's quick example using custom, attribute-based routes:
[HttpGet("GetLatestItems/{num}")]
public IEnumerable<string> GetLatestItems(int num)
{
return new string[] { "test", "test2" };
}
For more info about custom routing, read the following article on my blog.

Categories

Resources