I haven't used the ASP.NET WebAPI much although I have used routes in straight MVC. Obviously I'm doing something wrong, perhaps someone can help?
I have one controller called UsersController and two methods on it, Register and Details, both take two string parameters and return a string, both marked as HttpGet.
Originally I started with two route mappings in WebApiConfig.cs:
config.Routes.MapHttpRoute(
name: "TestApi",
routeTemplate: "api/{controller}/{action}/{userId}/{key}"
);
config.Routes.MapHttpRoute(
name: "Test2Api",
routeTemplate: "api/{controller}/{action}/{userId}/{class}"
);
With this set up only the first route is found for a URL such as:
http://<domain>/api/users/register/a123/b456/
If I call:
http://<domain>/api/users/details/a123/b456/
I get a 404. If I swap the two routes around then I can call the Details method but not the Register method, again getting a 404. The workaround I have in place is to be more specific with the routes:
config.Routes.MapHttpRoute(
name: "TestApi",
routeTemplate: "api/{controller}/register/{userId}/{key}"
);
config.Routes.MapHttpRoute(
name: "Test2Api",
routeTemplate: "api/{controller}/details/{userId}/{class}/"
);
The UsersController looks like:
namespace HelloWebAPI.Controllers
{
using System;
using System.Web.Http;
using HelloWebAPI.Models;
public class UsersController : ApiController
{
[HttpGet]
public string Register(string userId, string key)
{
return userId + "-" + key;
}
[HttpGet]
public string Enrolments(string userId, string #class)
{
return userId + "-" + #class
}
}
}
So what I don't understand is why the {action} component of the route registered second is not being associated with the correct method.
Thanks
Joe
The routing in ASP.NET Web API works in three steps:
Matching the URI to a route template.
Selecting a controller.
Selecting an action.
The framework selects the first route in the route table that matches the URI, there is no "second guessing" in case when 2nd or 3rd step fails - just 404. In your case both URI are always matching the first route so the second is never used.
For further analysis let's assume that the first route is:
api/{controller}/{action}/{userId}/{key}
And you call it with following URI:
http://<domain>/api/users/enrolments/a123/b456/
In order to choose action framework is checking three things:
The HTTP method of the request.
The {action} placeholder in the route template, if present.
The parameters of the actions on the controller.
In this case the {action} part will be resolved correctly to enrolments, but framework will be looking for Enrolments method with userId and key parameters. Your method has a class parameter which is no match. This will result in 404.
To avoid the issue you have to make more specific routes (like you did) or unify parameters names.
You only need to define the one route:
config.Routes.MapHttpRoute(
name: "TestApi",
routeTemplate: "api/{controller}/{action}/{userId}/{key}"
);
Then change your controller methods to the following:
[HttpGet]
public string Register(string userId, string key)
{
return userId + "-" + key;
}
[HttpGet]
public string Details(string userId, string key)
{
return userId + "-" + key
}
Related
I have 2 get methods in my API controller, one which accepts no arguments and one which accepts an integer argument.
The path for the API page is /api/contact. When I navigate here, the page displays as expected.
However, when I change the path to /api/contact/4 to try to invoke the get method which accepts an integer argument, the code instead just invokes the same get method without any arguments. I know this by putting in breakpoints and debugging. What is going wrong?
public PhoneInfo[] Get()
{
return contactRepository.GetAllContacts();
}
public PhoneInfo[] Get(int phn)
{
return contactRepository.GetMessages(phn.ToString());
}
WebApi works based on reflection this means that your curly braces {vars} in your global.asax/routing configuration must match with the same name in your methods.
By default your global.asax will look like this:
RouteTable.Routes.MapHttpRoute(name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = System.Web.Http.RouteParameter.Optional });
Therefore to match the above where id exists as a var the url is api/contact/{id} the method needs to be declare like this:
public string Get(int id)
return "test";
}
While in your example your are changing the default name of the parameter from id to phn causing WebApi not being able to find your method.
Another option can be to use the RouteAttribute:
[Route("api/contact/{phn}"), HttpGet]
public string Get(int phn)
return "another value";
}
I am trying to achieve api versioning using a CustomHttpControlSelector and AttributeRouting on asp.net webapi.
What i am trying to do is distinguish controller's versions by it's namespaces.
if a request is made to /api/v2/foo/bar
i want it to match
namespace Web.Controllers.Api.v2
{
[RoutePrefix("foo")]
public class LongerThanFooController : ApiController
{
[HttpGet]
[Route("bar")]
public string BarFunction()
{
return "foobar";
}
}
}
but as i see when i don't use full url on RoutePrefix (/api/v2/foo) attribute routing doesn't kick in and i get null when i call
request.GetRouteData().GetSubRoutes();
on my CustomHttpControlSelector. i don't want to Repeat /api/v2 on every controller.
if i decide to remove attributeRouting and use manual routes like
config.Routes.MapHttpRoute(
name: "DefaultVersionedApi",
routeTemplate: "api/v{version}/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional, version = Config.LatestVersion }
);
i lose all flexibility of naming my controllers and functions.
is there a way to get out of this limbo?
note: for the CustomHttpControlSelector i modified code on http://aspnet.codeplex.com/SourceControl/changeset/view/dd207952fa86#Samples/WebApi/NamespaceControllerSelector/NamespaceHttpControllerSelector.cs
I realize this is bit of an old question now, but it can answered using the ASP.NET API Versioning package for ASP.NET Web API. In the latest 3.0 version, you can achieve your scenario by updating your configuration with:
var constraintResolver = new DefaultInlineConstraintResolver()
{
ConstraintMap =
{
["apiVersion"] = typeof( ApiVersionRouteConstraint )
}
};
configuration.AddApiVersioning(
options =>
{
options.Conventions.Add( new VersionByNamespaceConvention() );
options.AssumeDefaultVersionWhenUnspecified = true;
options.ApiVersionSelector = new CurrentImplementationApiVersionSelector( options );
} );
configuration.MapHttpAttributeRoutes( constraintResolver );
You should also remove your convention-based routes. Those are unnecessary if you are using attribute routing.
The setup for your controller simply changes to:
namespace Web.Controllers.Api.v2
{
[RoutePrefix("api")]
public class LongerThanFooController : ApiController
{
[HttpGet]
[Route("foo/bar")]
[Route("v{version:apiVersion}/foo/bar")]
public string BarFunction()
{
return "foobar";
}
}
}
The reason you need two route definitions is that you cannot have default values in the middle of the route template. Default values can only be used at the end. This also means that you need to allow no API version to be specified and indicate the way to determine which API version should be selected is to use the current implementation (e.g. latest). I'm personally not a fan of this approach because I think things should be predictable to clients, but this will achieve your desired result.
I'm designing a webservice which has nothing to do with REST. It backs up a single-page application and currently must implement three simple methods:
public class ImportController : ApiController
{
[HttpPost]
public string[] Parse(string source) { ... }
[HttpPost]
public ConvertResponse Convert(ConvertRequest request) { ... }
[HttpGet]
public object GetHeaders() { ... }
}
It worked pretty well when I was using Controller, except for one thing: I needed to convert all returned JSON data to camelCase. I found a pretty reasonable solution on the web which used CamelCasePropertyNamesContractResolver, but it was only applicable to WebApi controllers since MVC controllers that return JsonResult always use JavascriptSerializer and ignore this configuration.
When I switched the base class to ApiController, the routing broke: GetHeaders works, but other methods return a 404 error!
My route configuration is as follows:
routes.MapHttpRoute(
name: "ImportParse",
routeTemplate: "import/{action}",
defaults: new { controller = "Import" }
);
The successful request (AngularJS):
var baseUrl = 'http://localhost:3821/';
$http.get(baseUrl + 'import/getHeaders').success( ... );
The unsuccessful request:
$http.post(baseUrl + 'import/parse', { source: 'test' }).success( ... );
Error:
message: "No HTTP resource was found that matches the request URI 'http://localhost:3821/import/parse'."
messageDetail: "No action was found on the controller 'Import' that matches the request."
How do I define the correct routing rules for those methods?
Most probably Web Api is looking for a Parse action that supports HttpPost and has object parameter. Because you post object, but not a string, that is why you get 404.
To solve this problem try to send :
$http.post(baseUrl + 'import/parse', 'test').success( ... );
I am building a Http service using asp.net web api, I have two get methods in the controller with the same parameters, I can't figure out how to define the route the matches both methods, I can call only one of them and for the other I get an error that no action method was found on the controller.
Here are the routes defined
RouteTable.Routes.MapHttpRoute(
name: "default",
routeTemplate: "{controller}/{lang}",
defaults: new { lang = System.Web.Http.RouteParameter.Optional });
RouteTable.Routes.MapHttpRoute(
name: "details",
routeTemplate: "{controller}/{lang}/{action}/{id}");
and the methods in the controller:
public IQueryable<RecipeDTO> Get(string lang)
{
}
[HttpGet]
public RecipeDTO Details(string lang, int id)
{
}
[HttpGet]
public IQueryable<RecipeDTO> Random(string lang, int count)
{
}
You see the methods Details and Random have the same parameters, I can make the following calls:
controller-name/en (which matches the first get method)
controller-name/en/details/1 (which matches the details method)
but when I try:
controller-name/en/random/5
I get the error no action method was found on the controller, how can I fix that.
Thanks in advance
Web API is strict about matching route variables with parameter names. You can try changing your action to the following(Here we are aliasing the count parameter):
[HttpGet]
public IQueryable<RecipeDTO> Random(string lang, [FromUri(Name = "id")]int count)
I would like to create a simple route which will allow me to have ONLY one item listed after the base URL (other than when it's a controller), and for that item to be passed into a controller as a parameter. for example:
www.mydomain.com/user1
www.mydomain.com/user2
www.mydomain.com/user3
www.mydomain.com/user3
where user1, user2 etc are usernames and are being passed dynamically, ie i don't want to have to create a controller for each username.
naturally i would want to make sure if something like this is possible that it wont cause conflicts with other genuine controller names, thus i guess the other controllers would have to have rules created specifically for them and listed above the wildcard route
i'm not sure how to do this as i guess the first item after the slash is usually a controller.
any ideas how to tackle this?
the examples i provided may seem ambiguous, when i put www.mydomain.com/user1 etc it represents that it could be anything (ie a username),for example, www.mydomain.com/jsmith, www.mydomain.com/djohnson, www.mydomain.com/sblake, www.mydomain.com/fheeley
the idea is that a users profile can be looked up simply by entering the domain name then a fwd slash and the username.
ASP.Net MVC routes are process from the top down, and as soon as a match is found it won't look any further for a match. So put your most specific routes first, and your wildcard route last. If none of the specific routes match, control will be passed to the wildcard route.
Use a route definition such as
routes.MapRoute("{username}", new { controller = "User", action = "View"});
in your global.asax.cs file but put it last in your list of route definitions.
MVC processes your route mappings from top to bottom, so put your most general route mappings at the top, and your more specific ones at the bottom.
After you do this you will need the corresponding controller/action so create a new Controller named "UsersController" with an action named "View", like as follows:
using System.Web.Mvc;
namespace Routes.Controllers
{
public class UsersController : Controller
{
//
// GET: /{username}
public ActionResult List(string username)
{
// Get User from database/repository and pass to view
var user = ...;
return View(user);
}
}
}
You have to do following thing.
Create one controller or Action for Handle above scenario. For example controller="Home" and Action="GetUser"
public ActionResult GetUser(string username){
// do your work
}
In Global.asax ( Top route)
// Look at point 3 for UserNameConstraint
route.MapRoute("UserRoute" , "{username}" , new { controller="Home" , action="GetUser" } , new { username = new UserNameConstraint() });
// Put your default route after above here
Now for Create Route constraint.
Hope this help you.
public class UserNameConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
List<string> userName = GetUserName from DB
userName.Contain(values[parameterName].ToString());
}
}
Create a route like as follows,
routes.MapRoute(
"DefaultWithUser", // Route name
"{user}/{controller}/{action}/{id}", // URL with parameters
new { user=UrlParameter.Optional, controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);