How Do I Support Empty RouteAttribute in Web API? - c#

I have this class:
[RoutePrefix("api/v2/Foo")]
public class SomeController : BaseController
{
[Route("")]
[HttpGet]
public async Task<HttpResponseMessage> Get(SomeEnum? param)
{
//...
}
}
I want to call it via:
api/v2/Foo?param=Bar
but it doesn't work.
If I change the routing attribute thusly to include something in the RouteAttribute:
[Route("SomeRoute")]
[HttpGet]
public async Task<HttpResponseMessage> Get(SomeEnum? param)
{
//...
}
...then I can call
api/v2/Foo/SomeRoute?param=Bar
, but that isn't what I want.
How do I get the first circumstance to work?
EDIT:
Domas Masiulis steered me toward the answer: the above scenario DOES work, it's just that a default global routing screwed things up. I solved the issue by adding a separate default routing that matched our convention...
public static void RegisterRoutes(RouteCollection routes)
{
//ADDING THIS FIXED MY ISSUE
routes.MapHttpRoute(
name: "API Default",
routeTemplate: "api/v2/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
//SOURCE OF THE ORIGINAL PROBLEM
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Administration", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}

Any special conditions? Maybe something hidden in BaseController? Any custom configurations in RouteConfig?
Your given example works for me, made a quick test:
Used code:
[RoutePrefix("api/v2/Foo")]
public class SomeController : ApiController
{
[Route("")]
[HttpGet]
public Task<int> Get(int param)
{
return Task.FromResult(2);
}
}
Calling
http://localhost:1910/api/v2/Foo?param=1 works as expected - returns 2.

1) You have to specify [FromUri] attribute for query parameter
2) If you use nullable parameter like SomeEnum? setup a default value for it
[Route("")]
[HttpGet]
public async Task<HttpResponseMessage> Get([FromUri] SomeEnum? param = null)
{
//...
}

Related

Web API Routing Not Recognising Parameters

I have the following WebAPI controller:
namespace MyApp.WebApi.Controllers
{
[RoutePrefix("api/listing")]
public class ListingController : ApiController
{
[Route("{firstparam:int?}/{nextparam:int?}")]
public IEnumerable<ListItem> Get(int firstparam = 100, int nextparam = 12)
{
// firstparam is always 100, and nextparam is always 12
However, I've tried specifying the URL:
http://localhost:56004/#/listing?firstparam=2
If I specify the URL like this:
http://localhost:56004/#/listing/2
Then it breaks the routing.
Clearly I'm missing something regarding routing; please could someone point me in the right direction?
You are using multiple optional parameter which don't work well for routeTemplates. Normally the last parameter tends to be the optional parameter.
Documentation: Attribute Routing in ASP.NET Web API 2: Optional URI Parameters and Default Values
FIrst make sure the attribute routing is enabled
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Attribute routing.
config.MapHttpAttributeRoutes();
// Convention-based routing.
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}
To get what you are after
[RoutePrefix("api/listing")]
public class ListingController : ApiController {
//GET api/listing
//GET api/listing?firstparam=x
//GET api/listing?nextparam=y
//GET api/listing?firstparam=x&nextparam=y
[HttpGet]
[Route("")]
public IEnumerable<ListItem> Get(int firstparam = 100, int nextparam = 12) { ... }
}
The problem with having multiple inline parameters that are optional is that the router wont know which to use which is why they tend to be at the end of the url.
However to get them inline like how you mentioned in your example you are going to need multiple routes.
[RoutePrefix("api/listing")]
public class ListingController : ApiController {
//GET api/listing
[HttpGet]
[Route("")]
public IEnumerable<ListItem> Get() { return Get(100, 12); }
//GET api/listing/2
//GET api/listing/2/5
[HttpGet]
[Route("{firstparam:int}/{nextparam:int?}")]
public IEnumerable<ListItem> Get(int firstparam, int nextparam = 12) { ... }
}
You could try using [FromUri] attribute within the params of the Get() method to extract any query params being passed into the "/listing" uri along with a class that consists of int properties firstparam and secondparam.
namespace MyApp.WebApi.Controllers
{
[RoutePrefix("api/listing")]
public class ListingController : ApiController
{
[Route("")]
[HttpGet]
public IEnumerable<ListItem> Get([FromUri] ClassRepresentingParams params)
{
Hopefully that helps.

MVC Default route with nullable id not working as expected

A simple routing scenario is not working for me.
my route registration looks like this
context.MapRoute(
"Users_default",
"Users/{controller}/{action}/{id}",
new { action = "Index", id= UrlParameter.Optional });
and i am expecting it to honor the requests for
users/profile/
users/profile/1
users/profile/2
with the following controller
public class ProfileController : Controller
{
public ActionResult Index(int? id)
{
var user = id == null ? (UserModel)HttpContext.Session["CurrentUser"] : userManager.GetUserById((int)id);
return View(user);
}
}
it works for users/profile but not for users/profile/1
i've tried few different things but i know the answer must be simple, its just my lack of knowledge, what am i missing here.
i dont want index to appear. i want to use the same method for both users/profile/1 and users/profile/
Then don't put action into your URL.
context.MapRoute(
"Users_default",
"Users/{controller}/{id}",
new { action = "Index", id= UrlParameter.Optional });
The route you have defined will not allow index to be optional because it is followed by another parameter (in this case "id"). Only the last parameter can be optional on all but the default route.
This is because your route interprets as:
{controller: "profile", action: "1"}.
You need to point you details action url explicit, something like this:
users/profile/index/1
You can use Attribute routing
The code would look like
public class ProfileController : Controller
{
[Route("users/profile/{id}")]
public ActionResult Index(int? id)
{
var user = id == null ? (UserModel)HttpContext.Session["CurrentUser"] : userManager.GetUserById((int)id);
return View();
}
}
And you have to modify your RouteConfig
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// This will enable attribute routing in your project
routes.MapMvcAttributeRoutes();
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
So now you can use users/profile for your default behaviour and users/profile/ for a specific profile.

Why do certain parameter types cause ambiguous routing in ASP.NET Web API?

Suppose I have a very simple routing table, like this:
routes.MapHttpRoute("root", "",
new { controller = "Home", action = "Index" });
And within my HomeController, I have two methods called Index:
[HttpGet]
public IHttpActionResult Index()
{
return Content(HttpStatusCode.OK, new { service = "hard-coded string" });
}
[HttpGet]
public IHttpActionResult Index(string a)
{
return Content(HttpStatusCode.OK, new { a });
}
If I run this application, routing occurs as I would expect: if I omit the a parameter from the query string, my request gets routed to the first method, and if I include it, the request is routed to the second method.
However, if I change the type of a to a more complex type, such as string[], then when I make a request on the default route, I get the following error (regardless of whether I specified the query parameter):
{
Message: "An error has occurred."
ExceptionMessage: "Multiple actions were found that match the request: Index on type SurveysApi.v1.Web.Controllers.HomeController Index on type SurveysApi.v1.Web.Controllers.HomeController"
ExceptionType: "System.InvalidOperationException"
StackTrace: " at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem.SelectAction(HttpControllerContext controllerContext) at System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken) at System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken) at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__0.MoveNext()"
}
The error remains if even if I specify FromUri or ModelBinder attributes on the parameter.
Why does this error occur for complex types, and is there any way to avoid it, short of specifying a simple type in the argument list and performing the necessary conversions in the controller method?
Method overloading doesn't seem to play well with Web API. What I suggest you use instead is attribute routing.
Enable it in WebApiConfig.cs
config.MapHttpAttributeRoutes();
An example Controller would be:
public class HomeController : ApiController
{
[Route("home/id/{Id}")]
[HttpGet]
public string Get(int Id)
{
return "id";
}
[Route("home/string/{str}")]
[HttpGet]
public string Get(string str)
{
return "string";
}
}
Method names are the same, but routes are the key factor. Another way you could do this is using the ActionName attribute:
An example Controller would be:
public class HomeController : ApiController
{
[ActionName("GetById")]
public string Get(int id)
{
return "id";
}
[ActionName("GetByGUID")]
public string Get(string id)
{
return "guid";
}
}
Routes would be something like:
//Matches /api/Home/7
config.Routes.MapHttpRoute(
name: "DefaultDigitApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { action = "GetById" },
constraints: new { id = #"^\d+$" } // id must be digits
);
//Matches /api/Home/CD73FAD2-E226-4715-B6FA-14EDF0764162
config.Routes.MapHttpRoute(
name: "DefaultGuidApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { action = "GetByGUID" },
constraints: new { id = #"\b[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}\b" } // id must be guid
);
Hope this helps at least a bit.

Why do WebApi routing config defaults not include the action param?

I am a bit new to WebApi so maybe someone can explain this to me, the default route added for WebApis is:
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
So given an ApiController that looks like this:
public class LookupController : ApiController
{
[HttpGet]
public IHttpActionResult GetCountries()
{
// do stuff
return Ok();
}
[HttpGet]
public IHttpActionResult GetStates()
{
// do stuff
return Ok();
}
}
How would it know which action to call? It wouldn't right?
Shouldn't the default Route be more like:
config.Routes.MapHttpRoute(
name: "ApiWithAction",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Or should you only have one GET/UPDATE/DELETE etc per ApiController? Which really wouldn't cater for some scenarios...
Web API figures out which method to call based on routing data. You don't need to specify the action because Web API will use the verb(GET, POST,DELETE,etc) of the request.
If you would like to have multiple GET actions in a controller, you can specify a route for each action.
[Route("api/lookup/countries"]
[HttpGet]
public IHttpActionResult GetCountries()
{
// do stuff
return Ok();
}
[Route("api/lookup/states"]
[HttpGet]
public IHttpActionResult GetStates()
{
// do stuff
return Ok();
}
More information is available here http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api
Strictly speaking your controller should never look like that. Out of the box the idea is that a controller handles requests for a single entity (e.g. Customer) and the operations, which map to HTTP verbs, operate against that entity. So under normal circumstances your controller would look something like the following (created using scaffolding against a simple model class, method bodies omitted for brevity):
public class CustomerController : ApiController
{
public IQueryable<Customer> GetCustomers()
{
}
[ResponseType(typeof(Customer))]
public IHttpActionResult GetCustomer(int id)
{
}
[ResponseType(typeof(void))]
public IHttpActionResult PutCustomer(int id, Customer customer)
{
}
[ResponseType(typeof(Customer))]
public IHttpActionResult PostCustomer(Customer customer)
{
}
[ResponseType(typeof(Customer))]
public IHttpActionResult DeleteCustomer(int id)
{
}
}
If you have more business-oriented operations to perform (e.g. BillCustomer), rather than just the basic CRUD operations, I would suggest creating a separate route for those. We did that in one of our applications and it created a nice logical separation. For example:
config.Routes.MapHttpRoute(
name: "RestApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Routes.MapHttpRoute(
name: "RpcApi",
routeTemplate: "rpc/{controller}/{action}"
);

Custom method names in ASP.NET Web API

I'm converting from the WCF Web API to the new ASP.NET MVC 4 Web API. I have a UsersController, and I want to have a method named Authenticate. I see examples of how to do GetAll, GetOne, Post, and Delete, however what if I want to add extra methods into these services? For instance, my UsersService should have a method called Authenticate where they pass in a username and password, however it doesn't work.
public class UsersController : BaseApiController
{
public string GetAll()
{
return "getall!";
}
public string Get(int id)
{
return "get 1! " + id;
}
public User GetAuthenticate(string userName, string password, string applicationName)
{
LogWriter.Write(String.Format("Received authenticate request for username {0} and password {1} and application {2}",
userName, password, applicationName));
//check if valid leapfrog login.
var decodedUsername = userName.Replace("%40", "#");
var encodedPassword = password.Length > 0 ? Utility.HashString(password) : String.Empty;
var leapFrogUsers = LeapFrogUserData.FindAll(decodedUsername, encodedPassword);
if (leapFrogUsers.Count > 0)
{
return new User
{
Id = (uint)leapFrogUsers[0].Id,
Guid = leapFrogUsers[0].Guid
};
}
else
throw new HttpResponseException("Invalid login credentials");
}
}
I can browse to myapi/api/users/ and it will call GetAll and I can browse to myapi/api/users/1 and it will call Get, however if I call myapi/api/users/authenticate?username={0}&password={1} then it will call Get (NOT Authenticate) and error:
The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.String Get(Int32)' in 'Navtrak.Services.WCF.NavtrakAPI.Controllers.UsersController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
How can I call custom method names such as Authenticate?
By default the route configuration follows RESTFul conventions meaning that it will accept only the Get, Post, Put and Delete action names (look at the route in global.asax => by default it doesn't allow you to specify any action name => it uses the HTTP verb to dispatch). So when you send a GET request to /api/users/authenticate you are basically calling the Get(int id) action and passing id=authenticate which obviously crashes because your Get action expects an integer.
If you want to have different action names than the standard ones you could modify your route definition in global.asax:
Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { action = "get", id = RouteParameter.Optional }
);
Now you can navigate to /api/users/getauthenticate to authenticate the user.
This is the best method I have come up with so far to incorporate extra GET methods while supporting the normal REST methods as well. Add the following routes to your WebApiConfig:
routes.MapHttpRoute("DefaultApiWithId", "Api/{controller}/{id}", new { id = RouteParameter.Optional }, new { id = #"\d+" });
routes.MapHttpRoute("DefaultApiWithAction", "Api/{controller}/{action}");
routes.MapHttpRoute("DefaultApiGet", "Api/{controller}", new { action = "Get" }, new { httpMethod = new HttpMethodConstraint(HttpMethod.Get) });
routes.MapHttpRoute("DefaultApiPost", "Api/{controller}", new {action = "Post"}, new {httpMethod = new HttpMethodConstraint(HttpMethod.Post)});
I verified this solution with the test class below. I was able to successfully hit each method in my controller below:
public class TestController : ApiController
{
public string Get()
{
return string.Empty;
}
public string Get(int id)
{
return string.Empty;
}
public string GetAll()
{
return string.Empty;
}
public void Post([FromBody]string value)
{
}
public void Put(int id, [FromBody]string value)
{
}
public void Delete(int id)
{
}
}
I verified that it supports the following requests:
GET /Test
GET /Test/1
GET /Test/GetAll
POST /Test
PUT /Test/1
DELETE /Test/1
Note That if your extra GET actions do not begin with 'Get' you may want to add an HttpGet attribute to the method.
I am days into the MVC4 world.
For what its worth, I have a SitesAPIController, and I needed a custom method, that could be called like:
http://localhost:9000/api/SitesAPI/Disposition/0
With different values for the last parameter to get record with different dispositions.
What Finally worked for me was:
The method in the SitesAPIController:
// GET api/SitesAPI/Disposition/1
[ActionName("Disposition")]
[HttpGet]
public Site Disposition(int disposition)
{
Site site = db.Sites.Where(s => s.Disposition == disposition).First();
return site;
}
And this in the WebApiConfig.cs
// this was already there
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
// this i added
config.Routes.MapHttpRoute(
name: "Action",
routeTemplate: "api/{controller}/{action}/{disposition}"
);
For as long as I was naming the {disposition} as {id} i was encountering:
{
"Message": "No HTTP resource was found that matches the request URI 'http://localhost:9000/api/SitesAPI/Disposition/0'.",
"MessageDetail": "No action was found on the controller 'SitesAPI' that matches the request."
}
When I renamed it to {disposition} it started working. So apparently the parameter name is matched with the value in the placeholder.
Feel free to edit this answer to make it more accurate/explanatory.
Web Api by default expects URL in the form of api/{controller}/{id}, to override this default routing. you can set routing with any of below two ways.
First option:
Add below route registration in WebApiConfig.cs
config.Routes.MapHttpRoute(
name: "CustomApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Decorate your action method with HttpGet and parameters as below
[HttpGet]
public HttpResponseMessage ReadMyData(string param1,
string param2, string param3)
{
// your code here
}
for calling above method url will be like below
http://localhost:[yourport]/api/MyData/ReadMyData?param1=value1&param2=value2&param3=value3
Second option
Add route prefix to Controller class and Decorate your action method with HttpGet as below.
In this case no need change any WebApiConfig.cs. It can have default routing.
[RoutePrefix("api/{controller}/{action}")]
public class MyDataController : ApiController
{
[HttpGet]
public HttpResponseMessage ReadMyData(string param1,
string param2, string param3)
{
// your code here
}
}
for calling above method url will be like below
http://localhost:[yourport]/api/MyData/ReadMyData?param1=value1&param2=value2&param3=value3
In case you're using ASP.NET 5 with ASP.NET MVC 6, most of these answers simply won't work because you'll normally let MVC create the appropriate route collection for you (using the default RESTful conventions), meaning that you won't find any Routes.MapRoute() call to edit at will.
The ConfigureServices() method invoked by the Startup.cs file will register MVC with the Dependency Injection framework built into ASP.NET 5: that way, when you call ApplicationBuilder.UseMvc() later in that class, the MVC framework will automatically add these default routes to your app. We can take a look of what happens behind the hood by looking at the UseMvc() method implementation within the framework source code:
public static IApplicationBuilder UseMvc(
[NotNull] this IApplicationBuilder app,
[NotNull] Action<IRouteBuilder> configureRoutes)
{
// Verify if AddMvc was done before calling UseMvc
// We use the MvcMarkerService to make sure if all the services were added.
MvcServicesHelper.ThrowIfMvcNotRegistered(app.ApplicationServices);
var routes = new RouteBuilder
{
DefaultHandler = new MvcRouteHandler(),
ServiceProvider = app.ApplicationServices
};
configureRoutes(routes);
// Adding the attribute route comes after running the user-code because
// we want to respect any changes to the DefaultHandler.
routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(
routes.DefaultHandler,
app.ApplicationServices));
return app.UseRouter(routes.Build());
}
The good thing about this is that the framework now handles all the hard work, iterating through all the Controller's Actions and setting up their default routes, thus saving you some redundant work.
The bad thing is, there's little or no documentation about how you could add your own routes. Luckily enough, you can easily do that by using either a Convention-Based and/or an Attribute-Based approach (aka Attribute Routing).
Convention-Based
In your Startup.cs class, replace this:
app.UseMvc();
with this:
app.UseMvc(routes =>
{
// Route Sample A
routes.MapRoute(
name: "RouteSampleA",
template: "MyOwnGet",
defaults: new { controller = "Items", action = "Get" }
);
// Route Sample B
routes.MapRoute(
name: "RouteSampleB",
template: "MyOwnPost",
defaults: new { controller = "Items", action = "Post" }
);
});
Attribute-Based
A great thing about MVC6 is that you can also define routes on a per-controller basis by decorating either the Controller class and/or the Action methods with the appropriate RouteAttribute and/or HttpGet / HttpPost template parameters, such as the following:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
namespace MyNamespace.Controllers
{
[Route("api/[controller]")]
public class ItemsController : Controller
{
// GET: api/items
[HttpGet()]
public IEnumerable<string> Get()
{
return GetLatestItems();
}
// GET: api/items/5
[HttpGet("{num}")]
public IEnumerable<string> Get(int num)
{
return GetLatestItems(5);
}
// GET: api/items/GetLatestItems
[HttpGet("GetLatestItems")]
public IEnumerable<string> GetLatestItems()
{
return GetLatestItems(5);
}
// GET api/items/GetLatestItems/5
[HttpGet("GetLatestItems/{num}")]
public IEnumerable<string> GetLatestItems(int num)
{
return new string[] { "test", "test2" };
}
// POST: /api/items/PostSomething
[HttpPost("PostSomething")]
public IActionResult Post([FromBody]string someData)
{
return Content("OK, got it!");
}
}
}
This controller will handle the following requests:
[GET] api/items
[GET] api/items/5
[GET] api/items/GetLatestItems
[GET] api/items/GetLatestItems/5
[POST] api/items/PostSomething
Also notice that if you use the two approaches togheter, Attribute-based routes (when defined) would override Convention-based ones, and both of them would override the default routes defined by UseMvc().
For more info, you can also read the following post on my blog.
See this article for a longer discussion of named actions. It also shows that you can use the [HttpGet] attribute instead of prefixing the action name with "get".
http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api
Web APi 2 and later versions support a new type of routing, called attribute routing. As the name implies, attribute routing uses attributes to define routes. Attribute routing gives you more control over the URIs in your web API. For example, you can easily create URIs that describe hierarchies of resources.
For example:
[Route("customers/{customerId}/orders")]
public IEnumerable<Order> GetOrdersByCustomer(int customerId) { ... }
Will perfect and you don't need any extra code for example in WebApiConfig.cs.
Just you have to be sure web api routing is enabled or not in WebApiConfig.cs , if not you can activate like below:
// Web API routes
config.MapHttpAttributeRoutes();
You don't have to do something more or change something in WebApiConfig.cs. For more details you can have a look this article.
Just modify your WebAPIConfig.cs as bellow
Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{id}",
defaults: new { action = "get", id = RouteParameter.Optional });
Then implement your API as bellow
// GET: api/Controller_Name/Show/1
[ActionName("Show")]
[HttpGet]
public EventPlanner Id(int id){}

Categories

Resources