WebAPI ActionName routing half working - c#

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 }

Related

Can't get Route to work in ASP.Net Web API 2 when inheriting Controller

I'm trying to get this to work in my ASP.Net Web API 2 application. You will notice that this Controller inherits Controller. This is because I need to return a View instead of JSON.
[RoutePrefix("api/Manage")]
public class ManageController : Controller
{
[Route("TestOne")]
public async Task<ActionResult> MyTestOne(string value1, string value2)
{
return View("");
{
}
Here is the error I'm getting.
<error>
<MessageDetail> No type was found that matches the controller named 'Manage'.</MessageDetail>
</Error>
I need to call the Manage Controller like so.
https://api.domain.com/api/Manage/TestOne?value1=foo&value2=bar
My RouteConfig is configured like so.
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
NOTE: [RoutePrefix("api/Account")] works in my AccountController. This is an API Controller and inherits ApiBase.
Any help is much appreciated! Thanks!
It happens because you have 2 route configuration, one for MVC controllers and one for Web API. And in your case Web API route configuration goes first. Global.asax.cs looks like this
//some configs
WebApiConfig.Register(GlobalConfiguration.Configuration);
//some configs
RouteConfig.RegisterRoutes(RouteTable.Routes);
And you must be having something like this in Web API route config
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
When you request /api/Manage/TestOne the Web API routing applies first. No attribute based route fits but the request perfectly matches to DefaultApi route. Manage matches to {controller} and TestOne goes to {id}. So the framework starts searching for api controller with name Manage like this
public class ManageController : ApiController
But there is not such controller and indeed you have an error
{
"Message": "No HTTP resource was found that matches the request URI 'http://host/api/Manage/TestOne/?value1=foo&value2=bar'.",
"MessageDetail": "No type was found that matches the controller named 'Manage'."
}
So I can suggest you few possible solutions.
Change route configuration order
//some configs
RouteConfig.RegisterRoutes(RouteTable.Routes);
//some configs
WebApiConfig.Register(GlobalConfiguration.Configuration);
And then your example will work as expected but it may create unexpected errors because I don't know all possibles routes in your application.
Remove DefaultApi route
If you completely rely on attribute based routing for Web API you can just remove this configuration with no negative effect for you application
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Or just change prefix
If you change prefix from api to anything else it will work as well because it won't match DefaultApi route anymore
[RoutePrefix("view/Manage")]

How to pass parameters to web api2

Controller signature:
public class UserAlertsController : ApiController
public IHttpActionResult Post(Guid id, List<long> users)
Route:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Postman request:
http://localhost:50828/api/UserAlerts/4af3fee7-84ae-48fa-9215-45a00c35dbf
Content-Type application/json
[1,2]
With the above setup I receieve the message:
"Message": "The request is invalid.",
"MessageDetail": "The parameters dictionary contains a null entry
for parameter 'id' of non-nullable type 'System.Guid'"
I also use the standard global.asax
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
}
Can any one tell me where I'm going wrong
If I change my action signature to:
public IHttpActionResult Post(string id, long[] users)
Then it is bound correctly, but the error is thrown if id is changed to a Guid.
EDIT
You made a mistake to pass 4af3fee7-84ae-48fa-9215-45a00c35dbf that is not a GUID and you should pass 4af3fee7-84ae-48fa-9215-45a00c35dbf6 Pay attention to 6 at the end.
Based on your original Question
The problem is with your route, you should use: "api/{controller}/{id}" as route url template.
You are passing guid as a route parameter but your route didn't match the url you are using.
Open WebApi.Config and change your default route to this route:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Also make sure you have registered your WebApiConfig in global.asax in Application_Start:
GlobalConfiguration.Configure(WebApiConfig.Register);
Side Note:
By default, Web API uses the following rules to bind parameters:
If the parameter is a “simple” type, Web API tries to get the value from the URI. Simple types include the .NET primitive types (int, bool, double, and so forth), plus TimeSpan, DateTime, Guid, decimal, and string, plus any type with a type converter that can convert from a string.
For complex types, Web API tries to read the value from the message body, using a media-type formatter.
The GUID value you're passing is in the header. You can specify you want to receive the value from the uri via the FromUri attribute:
public IHttpActionResult Post([FromUri]Guid id, List<long> users)
Another problem is that you have "api" part in your HTTP URL:
http://localhost:50828/api/UserAlerts/4af3fee7-84ae-48fa-9215-45a00c35dbf
While your controller is missing it in the declaration. Either remove the "api" part from your url, or add it to your route.
Either this:
http://localhost:50828/UserAlerts/4af3fee7-84ae-48fa-9215-45a00c35dbf
Or modify your url under MapRoute:
url: "api/{controller}/{action}/{id}",

Even though I have 2 dedicate HttpGet Actions defined in my controller, only one gets called even when the URL specifies 2 different Actions

I am trying to develop REST APIs on my server for a JS plugin as described in http://docs.annotatorjs.org/en/v1.2.x/storage.html. Two of the APIs I am required to develop are as follows:
Index: Index functionality with path as /annotations and method as get. I have implemented this in a controller annotations(placed in a directory called api) as follows:
[HttpGet]
public IList annotations(long userID = 2)
Search: Search functionality with path as /search and method as get again. My implementation in the same controller is as:
[HttpGet]
public AnnotationSearchResults search(int count, string uri)
The problem I am facing is, in case of both the following URLS: http://localhost:5555/api/Annotation/search?limit=20&uri=www.abc.com and http://localhost:5555/api/Annotation/annotations the method annotation gets called, though I am expecting the search method to be called. I am very to web development and trying hard to get this running since last 2 days. Please excuse me if this is a very basic and obvious question.
In your App_Start folder, there is a WebApiConfig.cs file. The route template here does not include an action by default. You will need to add the action route to get your desired output.
The default route for Web API is
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
Add an {action} to the routeTemplate as shown below and the correct action urls will be hit.
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}

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.

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

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.

Categories