Prevent ASP.NET Web API route engine from deducting custom method names - c#

I have the Web API controller with 2 methods - let's say the first method returns the plain project list and the second one returns all projects assigned to the specific user.
public class ProjectController: ApiController
{
public IQueryable<Project> Get() { ... }
[HttpGet]
public IQueryable<Project> ForUser(int userId) { ... }
}
The method implementation is not important in this case.
Web API route config is also adjusted to support the custom method names.
config.Routes.MapHttpRoute(
"DefaultApi",
"api/v1/{controller}/{id}",
new { id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
"DefaultApiWithAction",
"api/v1/{controller}/{action}");
It works fine, I can access both /api/v1/projects/ and /api/v1/projects/forUser/ endpoints, but seems that the route engine is too smart, so it decides that /api/v1/projects?userId=1 request may match the ForUser(..) method (due to the userId argument name, I guess) and ignores the {action} part of the route.
Is there any way to avoid this behavior and require the action part to be explicitly specified in the URL?

Couple things. First of all this route:
config.Routes.MapHttpRoute(
"DefaultApiWithAction",
"api/v1/{controller}/{action}",
new { id = RouteParameter.Optional });
Does not have "action" as an optional parameter. You have included id as optional (I assume as a typo), but as it does not exist in the route, you will not get a match with only one supplementary segment. Only URLs containing two parts, a controller and an action, will pass through this route. This url:
/api/v1/projects?userId=1
...contains a single segment and will not. This route, and any other which lacks a second component will default to this route:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/v1/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
...which only takes a controller and an optional ID. You need to either reformat the given URL to take an action parameter, or rewrite your route to make the action optional and set the default as you desire. This will all depend on your application architecture, but always err on the side of simplicity. Routes can get very complicated--simpler is generally better.
As for required/optional route components, keep in mind the following two things:
All route segments are required unless they are set as optional in the anonymous object.
Segments can also be excluded if they have a default value, set by providing one in the anonymous object in the form of placeholder = value.

I don't understand your problem completely.
Shouldn't /api/v1/projects?userId=1 indeed call the ForUser action?
Anyway, to make the action required, make your HttpRoute like this:
name: "DefaultApi",
routeTemplate: "api/v1/{controller}/{action}/{id}",
defaults: new { id = System.Web.Http.RouteParameter.Optional });
Now you can call like this:
/api/v1/projects/ForUser/2

I've finally come up with the solution that satisfies my requirements. I've combined this answer and ideas suggested by levib and user1797792 into the following config:
config.Routes.MapHttpRoute(
"DefaultApiWithActionAndOptionalId",
"api/v1/{controller}/{action}/{id}",
new {id = RouteParameter.Optional});
config.Routes.MapHttpRoute(
"DefaultApiGet",
"api/v1/{controller}",
new { action = "Get" },
new { httpMethod = new HttpMethodConstraint(HttpMethod.Get) });
Note that the config order matters a lot here.
First of all, the /api/v1/projects request with any query string (even with arguments whose names match the other action's parameters) is dispatched to the Get() method via the second route. This is important because in the real project I've got a custom action filter attached to this action that filters the returned IQueryable based on the provided request arguments.
api/v1/projects/forUser/1-like requests are dispatched to ForUser(int id) method by the first route. Renaming userId parameter into id allowed to construct cleaner URLs.
Obviously, this approach has some limitations, but it is all I need in my specific case.

Related

Route variables received as action variables?

I've got the following route configuration:
routes.MapRoute(
name: "Default",
url: "{project}/{version}/{controller}/{action}",
defaults: new { controller = "Portal", action = "Index" }
);
The idea is to have two custom parameters in the URL, before anything else so an action filter I created can do a few useful things. That works well so far, but here's the problem.
Here's an action method with a parameter called version
public ActionResult SomeMethod(string version)
{
//Some logic has been performed using the custom route variable values before getting in here....
}
If I do the following call: http:/server/RouteValue1/RouteValue2/MyController/SomeMethod?version=1
The value of the version parameter of SomeMethod is RouteValue2.
Is there any way to tell the framework not to do that? Keep route variables seperate from action variables ?
No, you can't use the same parameter name in both the route config and as a query string. You'll need to rename one, i.e.:
http:/server/RouteValue1/RouteValue2/MyController/SomeMethod?ver=1

WebAPI ActionName routing half working

I have been building a WebAPI, trying to route to the right methods with ActionName. It works with one of my methods I try to call, but the other one gets a 404 error.
My WebAPI Config file:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Configure Web API to use only bearer token authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
My WebAPI Controller methods are formatted as such:
This first one is the working one:
[ActionName("postdb")]
public IEnumerable<string[]> postDB(string id)
{ ...
This second one does not:
[ActionName("getquery")]
public IEnumerable<string[]> getQuery(string tables)
{ ...
I'm calling both of them the same way from angular (Temp is a string that is being passed as the argument):
$http.post('api/Test/postdb/' + temp).then(function (response) { ...
and
$http.get('api/Test/getquery/' + temp).then(function (response) { ...
I have tried changing names of both actions, the first one works no matter the name, the second one doesn't work no matter the name. I have also tried reordering them, changing between GET and POST, and changing arguments.
Any suggestions?
Not sure why you are using ActionName to setup routing?
You should probably be looking at Route attribute. eg.
[HttpPost]
[Route("postdb")]
// Action doesn't have to be called 'postdb'
public IEnumerable<string[]> postDB(string id)
ActionName is usually used for a different purpose (Purpose of ActionName)
Nevertheless, I think something odd is going on in your example - I'd think setting ActionName shouldn't have affected routing there. To debug I'd suggest to set up Failed Request Tracing to see at which point the request fails to reach the action.
These are the basic rules for Action selection in WebAPI (http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-and-action-selection)
You can specify the HTTP method with an attribute: AcceptVerbs, HttpDelete, HttpGet, HttpHead, HttpOptions, HttpPatch, HttpPost, or HttpPut.
Otherwise, if the name of the controller method starts with "Get", "Post", "Put", "Delete", "Head", "Options", or "Patch", then by convention the action supports that HTTP method.
If none of the above, the method supports POST.
So, in your example postdb method may map to the POST method. But may be because it's in lower case ASP.NET didn't like that and applied Rule 3 - try with ActionName("PostDB") and [ActionName("GetQuery")] if you really want to use ActionName (for whatever reason) instead of Route.
The name of the parameter tables in the second action
[ActionName("getquery")]
public IEnumerable<string[]> getQuery(string tables)
{ ...
does not match the name of the parameter id in the route:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }

Get Url from ApiController and Action names, in a project containing Controllers and ApiControllers

An existing project has controllers that inherit from either:
Controller: RouteTable.Routes.MapRoute with "{controller}/{action}/{id}".
ApiController: GlobalConfiguration.Configure and in the callback MapRoute with "api/{controller}/{id}".
Everything works fine, but I need to generate URLs for action methods in both of these types of controllers. Given:
a name or type of a controller that inherits from either of these, and
an action method name
Then from the web site side, how can I generate proper URLs for the web API side?
I'm using reflection to get action and controller names right now, and then through using UrlHelper.Action(actionName, controllerName, routeValueDictionary) am getting the correct URL for web site routes.
However, this method is (of course) generating URLs like this for the WebAPI side: /ApiControllerName/Get?parameter1=value when it needs to be /api/ApiControllerName?parameter1=value and the separate knowledge that it's a GET request.
Purpose: this is for a smoke test page for the web site that uses attributes and reflection to decide what to smoke test. It would be nice to be able to use the same attribute throughout the project, and furthermore it would be very nice to be able to use the correct UrlHelper that is aware of the routing tables and can produce the right prefix such as /api/, instead of the code assuming, perhaps wrongly, that the API routes were registered with api and not, say, webapi.
Update
After continued research I have found the Url.HttpRouteUrl method which can generate WebAPI URLs, but this requires knowing a route name, not an action method name.
I've done even more research on this and haven't gotten any closer to a solution. It looks like if you know the route name of the appropriate route, you can cook up a Url easily. There are also some likely hints here and here. But if there are multiple routes for WebApi, how do you know which one matches the controller and action you want? It would be dumb to reimplement what MVC itself already does in selecting a controller and action. I guess I could construct a URL from the given parameters using every WebApi route, then run the URL through its paces (using some of the above links) and see if it is going to match the desired controller... yuck.
There's got to be an easier way.
For now I'm going to have to move on, but here's to hoping someone can help me out.
Here a few ways of do it:
Using RouteUrl() method:
var url1 = Url.RouteUrl(new { id = 1, controller = "...", httproute = true });
The trick is of course httproute = true. Setting this property, you inform that you only want http routes (Web Api routes).
Using HttpRouteUrl() method:
var url2 = Url.HttpRouteUrl(null, new { id = 2, controller = ".." });
And another way to do it using directly the routes and httproute value:
var values = new RouteValueDictionary(new
{
id = 3,
controller = "...",
httproute = true
});
var url3 = RouteTable.Routes.GetVirtualPath(Request.RequestContext, values).VirtualPath;
The 3 ways are basically the same. Since you don't specify the route, the system will try to find the first match according to the route values. For example, lets say you have an ApiController called RestfulController and web api routes are configured like so:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Using the first method, when you do var url = Url.RouteUrl(new { id = 123, controller = "restful", httproute = true }); in a MVC Controller, the value in url is /api/restful/123.
But if you add a new route ConstraintApi:
config.Routes.MapHttpRoute(
name: "ConstraintApi",
routeTemplate: "api2/{controller}/{id}",
defaults: new { id = RouteParameter.Optional },
constraints: new { controller = "restful" }
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
The url returned by RouteUrl is /api2/restful/123.
You should be aware the order when you are declaring your routes. If the route DefaultApi is added before the route ConstraintApi, the url generated is /api/restful/123.
There is the Hyprlinkr library which allows you to create URIs from the Controller and Action using the route configuration.
This seems more like a hack, but worth a try if you only plan to use it for testing. As you mentioned, you can generate a URL with a route name. So you could grab all route names and generate all possible URLs. Then you need to check if the URL resolves to the action that you need or not. For that you can use something like the following: https://stackoverflow.com/a/19382567/1410281. The basic idea is that if you've got a URL, then do a fake redirect there, and get the routing data from the framework.

ASP.NET Web Api 2 - Attribute Based Routing - How do I force a parameter to query string only?

I am working on a Web Api 2 project and I am using attribute based routing. Here is a sample route:
[Route("{id:int}", Name = "GetEmployeeById")]
[HttpGet]
public IHttpActionResult GetEmployee(int id)
{
...
}
This works with the following URLs:
://host/employee/12345
://host/employee?id=12345
What I would prefer is that the first form (the parameter in the URI), would not be allowed, and only the second form (query string) would work.
What I've Tried
Mostly, I've tried searching the web for a way to make this work, and I'm not finding much.
This page talks about route constraints but this syntax doesn't seem to work (anymore?).
This page doesn't actually prevent the URI form from working.
There is an attribute called "[FromUri]" that you can use to decorate a method parameter, and the model binder will try to look for that parameter from the Querystring, it may not help you with this scenario but it is good to know about it, so in case you want to pass a search options for example to a Get method.
http://msdn.microsoft.com/en-us/library/system.web.http.fromuriattribute(v=vs.118).aspx
http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api
Hope that helps.
Couple of ways to achieve this. Here are some options
Rename parameter to something else than id (eg. employeeId).
Change the default routing configuration in WebApiConfig:
//Default configuration, you can see here the "id" parameter which enables action/id matching
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
//It should look like this
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}"
);
Also you can do it with custom attributes.
Actually, I was wrong about my original code. The query string parameter did not work with the route I specified. Instead, I could do this:
[Route("", Name = "GetEmployeeById")]
[HttpGet]
public IHttpActionResult GetEmployee(int id)
{
...
}
And this will do what I want. It must be getting the name id from the function's parameter list.
Unfortunately, this means I can't put a constraint on it anymore, but I guess I can just validate within the function.

Disambiguating RESTful services

I have used WCF extensively in previous projects. Lately, I have been exploring the use of ASP.NET Web API in creating RESTful services. After studying the DO's and DONT's of RESTful services and even trying it practically, I have a rather straightforward question.
Suppose I have a UsersController (inheriting ApiController) where I NEED to have 3 GET-type action methods:
GetUsers()
GetUserById(string id)
GetUserByName(string name)
Suppose I also have the following route in WebApiConfig
config.Routes.MapHttpRoute(
name: "Users",
routeTemplate: "api/users/{id}",
defaults: new { controller = "users", id = RouteParameter.Optional }
);
http://localhost:<port>/api/users would obviously invoke GetUsers()
The problem comes when I need to invoke either of the two action methods that take a single parameter.
I would like
http://localhost:<port>/api/users/5c6fe209-821e-475f-920d-1af0f3f52a82 to invoke GetUserById(string id)
and
http://localhost:<port>/api/users/jdoe to invoke GetUserByName(string name)
What I expect will happen instead is that I'll either get an error or only the first action method will be invoked for either case.
Since introducing the action on the route to disambiguate is considered as a deviation from pure RESTful services, how do I make the different URLs invoke the respective action method? I have scoured the web and most examples of RESTful services (by purists) stop at a first action method to retrieve everything and a second to retrieve a single item.
This can be achieved using more specific constrained routes. The example below is similar to what you are trying to achieve:
config.Routes.MapHttpRoute(
name: "ById",
routeTemplate: "api/{controller}/{id}",
defaults: new { action = "GetById" },
constraints: new { id = #"\d+" }
);
config.Routes.MapHttpRoute(
name: "ByName",
routeTemplate: "api/{controller}/{name}",
defaults: new { action = "GetByName", },
constraints: new { name = #"\w+" }
);
config.Routes.MapHttpRoute(
name: "All",
routeTemplate: "api/{controller}",
defaults: new { action = "GetAll", }
);
The constraints object you pass to MapHttpRoute specifies constraints in the form of regex for the different URL parameters. In this example, for the ById route we only match if the id parameter is a number. We only match ByName if the name parameter is a string of alpha characters. We match All on no parameters. Note the "+" on each of the regex specifies no empty values. Routes are matched in the order they are defined, so you should put the least specific rules below more specific rules.
In your case, you will need to find or write a regular expression that matches the GUID format you are using and constrain your ById route to match this expression. You'll then want to define your ByName route below accepting any string. Because it's below, it will only get called if the input string is not a GUID.
I should also add that if you have not worked with MVC routes before, they are very specific. You'll notice that the name of my parameter is {id} for ById and {name} for ByName. It is important these parameters match the exact name of your input parameter on your controller method since it's this that is used by the router build the method call. Even if you have only a single parameter on an action, if the name is not mapped correctly you'll get an error.

Categories