In WebAPI you can specify an Order in RouteAttribute to determine which order the routes are matched in. For example the below will match /other to GetOther before matching /blah to GetByName
[HttpGet, Route("{name}", Order = 1)]
public string GetByName(string name) { ... }
[HttpGet, Route("other")]
public string GetOther() { ... }
How would I do the same but with RoutePrefix (which doesn't have an Order property)? If it did it would looks something like this:
[RoutePrefix("foo", Order = 1)]
public class FooController : ApiController { ... }
[RoutePrefix("foo/bar")]
public class FooBarController : ApiController { ... }
Doing the above (without the imaginary Order property) throws the following message when calling /foo/bar:
Multiple controller types were found that match the URL
Is there existing functionality for getting around this (preferably with attributes)?
I don't believe Microsoft's attribute routing has support for ordering routes by controller.
When you specify an Order property on an action's RouteAttribute, you are specifying the order within the controller only.
AFAIK, the attribute routing algorithm will scan all of the controllers alphabetically. Then within each controller, is will use the Order property of any RouteAttributes to decide the order of action routes within that controller.
This means if you have route collisions spread across different controllers, you should either rethink the design or make sure the controllers with the more specific route patterns are named alphabetically before the controllers with the more general route patterns. Otherwise, you may run into that "ambiguous route / multiple actions with matching routes found" exception.
Update: The answer above is for Microsoft's AttributeRouting implementation, which was based on another very popular open source project that came before MVC5. In that library, you could order attribute routes by controller, though I think the property was SiteOrder or something like that.
You can add a orderby to the loop in index.cshtml:
#foreach (var group in apiGroups.OrderBy(g => g.Key.ControllerName))
Related
I have a base controller where most of my api logic sits. I want to standardise the api verbs and I want this base controller to handle most request by default unless I inherit from this controller and override the specific action.
Given a path like this: "/api/Socks/Get?apiKey=1" I am able to do something like this:
[Route("api/[controller]/[action]")]
public class RestDbApiController : Controller
{
[HttpGet]
public virtual async Task<JsonResult> Get(string apiKey = null) {
.....
public class SockController : RestDbApiController
{}
This works okay - ie. the request is routed the action on the base controller. The issue is that I have to declare the SockController, otherwise the request will not route.
I would like to be able to route all requests to "/api/xxxxx" to the base controller without having to declare any other controllers. Please let me know if there is any way to do this.
Why do I want to do this?
I'm trying to make a generic controller with external scripted definitions. Depending on the controller name it would read the script from a similarly named script file. I want to be able to just add the script file to a directory and have it work just like that without having to make any declarations in the code
This isn't really practically possible. You can technically just bind a wildcard route to the controller, but that will then swallow everything. In other words, all your API routes would hit this base controller forever, and you then have to basically set up your own routing infrastructure inside that base controller to reroute requests to the right controller. In case it's not obvious, that's an extremely bad idea.
The best thing to do is actually the thing you don't want to do: actually define the derived controller. While you may look at this like it's extraneous, it's actually not at all. It serves to make your code self-documenting. It's clear that this is a controller that deals with "socks", but it doesn't have any specific logic of it's own. That's perfectly okay.
I agree with #Chris, but if you want to have only one controller, you could omit the "controller name" part from the routing definition. For example, the following requests will be mapped to below action method
/api/Socks/Get?apiKey=1
/api/OtherSocks/Get?apiKey=1
and name parameter will be filled as "Socks", "OtherSocks" respectively:
public class RestDbApiController : Controller
{
[HttpGet]
[Route("api/{name}/[action]")]
public virtual async Task<JsonResult> Get(string name, string apiKey = null)
{
...
}
}
I'm upgrading an ASP.NET MVC 4 project to MVC 5 and want to use attribute routing instead of convention routing. So far, so good, but I have one issue with populating the Defaults RouteValueDictionary. How can this be accomplished with attribute routing?
I am using multiple routes for the same action, each passing a different enum value to determine which type the Action is. The value of the enum will not be visible in the route directly though! This is important, otherwise I could use the value of the enum parameter in the route template.
My simplified Controller Action:
public class MyController : Controller
{
public ActionResult MyAction(MyType myTypeValue)
{
// ...
}
}
public enum MyType
{
FirstOption,
SecondOption
}
My old convention routes:
routes.Add("First", new Route("a-route", new { controller = "MyController", action = "MyAction", myTypeValue = MyType.FirstOption }));
routes.Add("Second", new Route("a-total/different-route", new { controller = "MyController", action = "MyAction", myTypeValue = MyType.Second }));
With attribute routing i was expecting to use something like this:
Route["a-route", new { myTypeValue = MyType.FirstOption }]
Route["a-total/different-route", new { myTypeValue = MyType.SecondOption }]
But unfortunately, this does not exists. I've tried to make a custom RouteAttribute that accepts an object to populate the Defaults RouteValueDictionary:
public class MyRouteAttribute : RouteFactoryAttribute
{
private RouteValueDictionary _defaults;
public Route(string template, object defaults)
:base(template)
{
_defaults = new RouteValueDictionary(defaults);
}
public override RouteValueDictionary Defaults
{
get { return _defaults; }
}
}
But this is not working since the route attribute cannot handle anonymous types compile time.
Does anyone know a way to get this working one way or another?
"Just make two different actions" is not an option here.
First of all, it is unclear why you would want to change from convention-based routing to the (less flexible) attribute-based routing, especially considering some of the features you are interested in are not supported by the latter.
But if you are insistent on changing to attribute routing just because it "looks cool", then you have a couple of options.
Option 1: Make Separate Action Methods
If you use 2 different actions and return one action from the first, you generally won't have to rewrite logic. But this is the only native support in attribute routing for setting optional parameters. An example of how you can support optional parameters with Enum can be found here.
[Route("a-route")]
public ActionResult MyAction(MyType myTypeValue = MyType.FirstOption)
{
return View("Index");
}
[Route("a-total/different-route")]
public ActionResult My2ndAction(MyType myTypeValue = MyType.SecondOption)
{
return MyAction(myTypeValue);
}
Option 2: Hack the Attribute Routing Framework
Microsoft intentionally made the attribute routing framework non-extensible by using several internal/private types to load the RouteValueCollection with the attribute routes.
You could potentially hack the attribute routing framework to provide your own logic as I have done here. This requires using Reflection, but since it runs at the start of the application rather than per-request the overall performance impact will be minimal.
But depending on your requirements, you may need to copy more of the logic from the MVC attribute routing framework to populate your routes, which may not be worth the effort. In my simple case of supporting multiple cultures it was. In your case you will need to support your own attribute types with additional parameters, which will be more challenging.
But if you need more flexibility than this, I would suggest sticking with the convention-based routing.
Attributes have limitations on which datatypes are supported as opposed to code-based solutions.
Several features including populating default route values, using constraints, and building custom routes are either much more difficult or not supported when using attribute routing.
The bottom line is, attribute routing is not the holy grail of routing. It is another routing option added in MVC 5 which can be used under a limited subset of routing scenarios of which convention-based routing is capable of. It is not and should not be viewed as a routing "upgrade" just because it happens to not have been an option until MVC 5.
The RouteAttribute("abc", Order = 2) ordering only seems to be respected within a controller: when the following methods are defined in the same controller I get the expected behavior, i.e., Method1 takes priority, Method2 is never invoked.
[Route("abc", Order = 1)]
public ActionResult Method1() { ... }
[Route("abc", Order = 2)]
public ActionResult Method2() { ... }
If I define those methods in two separate controllers I get an ambiguous route exception: InvalidOperationException: Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL.
Is it possible to define the order across controllers using attribute routing?
Edit 1:
It seems that even routes that would have an implicit ordering within one controller are ambiguous when spread over multiple controllers. The following would be okay within one controller (literal before wildcard), but throws an exception if placed into different controllers.
[Route("details/abc")]
[Route("details/{product}")]
This makes me that it is by design to keep controllers focused and force similar routes to be defined in the same controller.
Edit 2:
These are the routes I actually want to use. I want to put them into different controllers, because they do different things. They differ by the prefix.
[Route("reactive/monitor")]
[Route("{tier}/monitor")]
You have no constraints upon the route parameters, for your second route. If you were to define the route as [Route("{tier:int}/monitor")] you might not have the ambiguity. Alternatively, you can add a regex to the routes, to make them exclusive, something like {tier:regex(^(?!reactive))?} would let you resolve this.
I have several ASP.NET MVC controllers. Many of these take one or more required values (e. g. ids). Because these values are required, I'd like to make them part of the url path rather than query string arguments. For example:
// route should be MyController/Action1/[someKindOfId1]
public ActionResult Action1(int someKindOfId1) { ... }
// less commonly:
// route should be MyController/Action1/[someKindOfId2]/[someKindOfId3]
public ActionResult Action2(int someKindOfId2, int someOtherKindOfId3) { ... }
I'm looking for a way to Map these routes without manually listing out each one. For example, I currently do:
routes.MapRoute(
"Action1Route",
"MyController/Action1/{someKindOfId1}",
new { controller = "MyController", action = "Action1" }
);
Some ways I've considered:
* Use the default {controller}/{action}/{id} route, and just either rename my parameters to id or (not sure if this works) use the [Bind] attribute to allow bind them to the id route value while still having descriptive names. This still restricts me to a common controller/action base URL (not bad, but not the most flexible either as it ties URLs to the current code organization).
* Create an attribute which I could put on action methods to configure their routes. I could then reflect over all controllers and configure routes on application start.
Is there a best-practice/built-in approach for doing this?
Sadly, no. The method you describe is the only way with MVC Routing. If you're not going to use the default (or at least your own version of the default), you must add a separate route for each unique scheme.
However, I would encourage you to check out AttributeRouting, which for me at least, is far superior to managing routes in the traditional way. With AttributeRouting, you specify the URL for each controller action using, appropriately enough, an attribute. For example:
[GET("MyController/Action1/{someKindOfId1}")]
public ActionResult Action1(int someKindOfId1) { ... }
[GET("MyController/Action1/{someKindOfId2}/{someKindOfId3}")]
public ActionResult Action2(int someKindOfId2, int someOtherKindOfId3) { ... }
Only, you're not bound to using the controller/action route scheme either, so you can do something like:
[GET("foo/{someKindOfId1}")]
public ActionResult Action1(int someKindOfId1) { ... }
[GET("foo/{someKindOfId2}/{someKindOfId3}")]
public ActionResult Action2(int someKindOfId2, int someOtherKindOfId3) { ... }
And to even better, you can add a RoutePrefix attribute to your controller itself to specify a path partial that should apply to all actions in that controller:
[RoutePrefix("foo")]
public class MyController : Controller
{
[GET("{someKindOfId1}")]
public ActionResult Action1(int someKindOfId1) { ... }
[GET("{someKindOfId2}/{someKindOfId3}")]
public ActionResult Action2(int someKindOfId2, int someOtherKindOfId3) { ... }
}
There's support for handling areas, subdomains, etc. as well and you can even type-qualify parameters (e.g. {someKindOfId1:int} to make it only match if the URL part is an integer type). Give the documentation a read.
UPDATE
It's worth mentioning that ASP.NET 5 now has attribute routing built in. (It's actually using very similar code to AttributeRouting, submitted by the author of that package.) It's not really a good enough reason on its own to upgrade all your projects (since you can just add in the AttributeRouting package to get basically the same functionality), but if you're starting off with a new project, it's definitely nice to have.
Suppose that I have a nested one to many-type hierarchy database as follows:
One Region has many Countries; each Country has many Cities; a City must belong to one and only one country.
Abstracting this information into a RDBMS is a trivial exercise, but (to my mind) the most sensible REST endpoint to return a list of countries for a given region id would be something like the following:
HTTP GET http://localhost/Region/3/Countries
By default, the .NET Web API's routing would be, at best, http://localhost/Countries/Region/3 or http://localhost/Region/Countries/3.
Is there a sensible naming-convention I should follow, or is the routing customisable enough to allow URIs to take any shape I like?
The routing should be customizable enough to get the URLs you're looking for. Assuming you want URLs in the form 'http://localhost/Region/3/Countries', you could register this custom route:
config.Routes.MapHttpRoute("MyRoute", "Region/{regionId}/Countries", new { controller = "Region", action = "GetCountries" });
This would dispatch requests to the 'GetCountries' action on the 'RegionController' class. You can have a regionId parameter on the action that gets model bound automatically for you from the URI.
You may want to look online for the attribute routing package for WebAPI since it may be more appropriate in your case.
Routings should be quite flexible - the question would be how you'd like to serve the data. Do you have one controller in mind or multiple?
If you had a RegionController I don't see why you couldn't configure a route:
routes.MapHttpRoute(
name: "CountryList",
routeTemplate: "{controller}/{regionId}/countries"
);
And a corresponding method:
public CountryCollection Get(int regionId)
Or am I missing something in your question? Where does your default routing come from?
Have a look at their documentation:
http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-and-action-selection
http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-in-aspnet-web-api