Populate the Defaults RouteValueDictionary with attribute routing - c#

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.

Related

Asp net Web Api: Overload Api Controller Parameter with List of Object?

I have an Api controller with the following method:
[Route("{app}")]
public IHttpActionResult Put(string app, Property setting){ //do stuff }
I want to over load it with:
[Route("{app}")]
public IHttpActionResult Put(string app, Property[] settings){
foreach (Property property in settings)
{
Put(app, property);
}
return Ok("Settings Updated");
}
This overload causes a routing conflict. I don't want to change the route. How can I achieve what I want?
You're using attribute routing here with two put requests. Firstly I'd suggest to use a [FromBody] atrribute before the Property[] settings parameter here.
Based on the way you used Route Attributes here points to the same action where you have multiple assigned already. So I can suggest two things here.
http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-and-action-selection.
Just use the second action that you mentioned that takes Property[] settings and just check whether the array is empty or not and you really don't have to worry about one setting change or more as essentially it's the same underneath. :)
And as you're using Asp.net Web Api 2 this could contain important information.

ASP.NET MVC 5: Define route order across controllers (RouteAttribute)

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.

RoutePrefix Order alternative for WebAPI 2

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))

What's the best way to generically map routes with required parameters?

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.

asp.net mvc master details and routing

I'm trying to model class and sections of a class and further instances of sections.
so route should be
Class/ (Create, Details, Index, Edit) for classes
Then I've a section controller
ClassSection
so I would do
Class/1/ClassSection/ (Create, ... ) since ClassSection without classid is useless
and then further
Class/1/ClassSection/1/Instance
to go to SectionInstance controller
how can I map my routes to conform to this notation
I've tried doing this for class sections
routes.MapRoute(
"ClassSections",
"Class/{classid}/ClassSection/{action}/{id}",
new { controller = "ClassSection" },
new { classid = #"d+" }
);
but I can't generate a proper link from Html.ActionLink in Index action of Class
You should consider using Areas. It sounds like you're moving in this direction already, but ASP.NET MVC 2.0 has specific support for this.
http://haacked.com/archive/2009/07/31/single-project-areas.aspx
Given the hard constrains "Class" and "ClassSection" in your Url template, I doubt that you need new { classid = #"d+" } to make it a unique match. Try removing that part of your MapRoute and see if that fixes the problem.

Categories

Resources