ASP.NET routing - avoiding clashes between controller/action and vanity/slug urls - asp.net-mvc

I'm looking for a good solution to having a URL scheme that works for both standard ASP.NET MVC controller/action urls eg:
/Home/About --> Controller "Home", Action "About"
and vanity/slug urls eg:
/fred/post --> Controller "Posts", Action "View", User "fred", Post "post"
Importantly, I want the outbound url generation to work so that
Html.ActionLink("View", "Posts", new { User="fred", Post="post" }, null }
gives /fred/post - not /Posts/View/fred/post
It seems, I can get it to work for either inbound or outbound routing but not both. Or I can get it sort of working but it's messy and prone to breaking. What approaches, tips and tricks are there to getting something like this working cleanly?

I finally came up with the solution of using a routing constraint that can check if a parameter matches (or doesn't match) the name of a controller:
public class ControllerConstraint : IRouteConstraint
{
static List<string> ControllerNames = (from t in System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
where typeof(IController).IsAssignableFrom(t) && t.Name.EndsWith("Controller")
select t.Name.Substring(0, t.Name.Length - 10).ToLower()).ToList();
bool m_bIsController;
public ControllerConstraint(bool IsController)
{
m_bIsController = IsController;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (m_bIsController)
return ControllerNames.Contains(values[parameterName].ToString().ToLower());
else
return !ControllerNames.Contains(values[parameterName].ToString().ToLower());
}
}
Use like this:
// eg: /myusername
routes.MapRoute(
"MemberUrl",
"{member_urlid}",
new { controller = "Members", action = "View" },
new { action="View", member_urlid = new ControllerConstraint(false) }
);
// eg: /myusername/mypagename
routes.MapRoute(
"ItemUrl",
"{member_urlid}/{item_urlid}",
new { controller = "Items", action = "View" },
new { action="View", member_urlid = new ControllerConstraint(false) }
);
// Normal controller/action routes follow
The constraint new ControllerConstraint(false) means don't match this routing rule if the parameter matches the name of a controller. Pass true to make the constraint check that the parameter does match a controller name.

Related

How to customize route to map url dynamically to a composed named controller in ASP.NET MVC3

I need to map URLs like this:
/stock/risk -->StockRiskController.Index()
/stock/risk/attr -->StockRiskController.Attr()
/srock/risk/chart -->StockRiskController.Chart()
...
/bond/performance -->BondPerformanceController.Index()
/bond/performance/attr -->BondPerformanceController.Attr()
/bond/performance/chart -->BondPerformanceController.Chart()
...
The first part is dynamic but enumerable, the second part has only two options(risk|performance).
For now I know only two ways:
customized a ControllerFactory(seems overkilled or complicated)
hard code all the combinations because they are enumerable(ugly).
Can I use routes.MapRoute to achieve this? Or any other handy way?
There is a nice solution based on IRouteConstraint. First of all we have to create new route mapping:
routes.MapRoute(
name: "PrefixedMap",
url: "{prefix}/{body}/{action}/{id}",
defaults: new { prefix = string.Empty, body = string.Empty
, action = "Index", id = string.Empty },
constraints: new { lang = new MyRouteConstraint() }
);
Next step is to create our Constraint. Before I will introduce some way how to check relevance as mentioned above - two list with possible values, but logic could be adjusted
public class MyRouteConstraint : IRouteConstraint
{
public readonly IList<string> ControllerPrefixes = new List<string> { "stock", "bond" };
public readonly IList<string> ControllerBodies = new List<string> { "risk", "performance" };
...
And now the Match method, which will adjust the routing as we need
public bool Match(System.Web.HttpContextBase httpContext
, Route route, string parameterName, RouteValueDictionary values
, RouteDirection routeDirection)
{
// for now skip the Url generation
if (routeDirection.Equals(RouteDirection.UrlGeneration))
{
return false;
}
// try to find out our parameters
string prefix = values["prefix"].ToString();
string body = values["body"].ToString();
var arePartsKnown =
ControllerPrefixes.Contains(prefix, StringComparer.InvariantCultureIgnoreCase) &&
ControllerBodies.Contains(body, StringComparer.InvariantCultureIgnoreCase);
// not our case
if (!arePartsKnown)
{
return false;
}
// change controller value
values["controller"] = prefix + body;
values.Remove("prefix");
values.Remove("body");
return true;
}
You can play with this method more, but the concept should be clear now.
NOTE: I like your approach. Sometimes it is simply much more important to extend/adjust routing then go to code and "fix names". Similar solution was working here: Dynamically modify RouteValueDictionary

How to route PUT and DELETE requests for the same url to different controller methods

I was searching for an answer to this question, and found this question, which is indeed very similar. However the solutions(s) posted there don't seem to be working for me... I wonder if it has to do with the question's age.
Given the following URL:
/my/items/6
I want HTTP PUT requests for this URL to be handled by one action method, and HTTP DELETE requests to be handled by another action method. Below are the routes I defined (note these are based in an area, so context is an AreaRegistrationContext instance, if that matters):
context.MapRoute(null,
"my/items/{id}",
new { area = "AreaName", controller = "ControllerName", action = "Replace" },
new
{
httpMethod = new HttpMethodConstraint("POST", "PUT"),
}
);
context.MapRoute(null,
"my/items/{id}",
new { area = "AreaName", controller = "ControllerName", action = "Destroy" },
new
{
httpMethod = new HttpMethodConstraint("POST", "DELETE"),
}
);
URL generation works fine with both of these routes, however there are problems when routing incoming requests. Only the first-declared route correctly maps to its respective action.
I dug into the HttpMethodConstraint source code and discovered that it does not care about the "X-HTTP-Method-Override" parameter, only HttpContext.Request.HttpMethod.
I was able to solve this problem with the following custom route constraint class:
public class HttpMethodOverrideConstraint : HttpMethodConstraint
{
public HttpMethodOverrideConstraint(params string[] allowedMethods)
: base(allowedMethods) { }
protected override bool Match(HttpContextBase httpContext, Route route,
string parameterName, RouteValueDictionary values,
RouteDirection routeDirection)
{
var methodOverride = httpContext.Request
.Unvalidated().Form["X-HTTP-Method-Override"];
if (methodOverride == null)
return base.Match(httpContext, route, parameterName,
values, routeDirection);
return
AllowedMethods.Any(m =>
string.Equals(m, httpContext.Request.HttpMethod,
StringComparison.OrdinalIgnoreCase))
&&
AllowedMethods.Any(m =>
string.Equals(m, methodOverride,
StringComparison.OrdinalIgnoreCase))
;
}
}
...and these route definitions:
context.MapRoute(null,
"my/items/{id}",
new { area = "AreaName", controller = "ControllerName", action = "Replace" },
new
{
httpMethod = new HttpMethodOverrideConstraint("POST", "PUT"),
}
);
context.MapRoute(null,
"my/items/{id}",
new { area = "AreaName", controller = "ControllerName", action = "Destroy" },
new
{
httpMethod = new HttpMethodOverrideConstraint("POST", "DELETE"),
}
);
My question: is it really necessary to have a custom route constraint to accomplish this? Or is there any way to make it work out-of-the-box with standard MVC & routing classes?
Action filters are your friend...
HttpDeleteAttribute, HttpPutAttribute, HttpPostAttribute, HttpGetAttribute

Mapping route from root url to dynamic product title / id

I'm developing an MVC 3 app
I want to have a url something like:
/some-product-name-goes-here-4726482648
(with 4726482648 being the product id)
I do have other pages such as:
/category-name
(allows browsing by category)
/about
etc...
How could I go about setting up routing to allow this?
You would certainly be using a RouteConstraint here
in your routes you could have something like this:
routes.MapRoute("CustomProductUrl",
"{Product}",
new { controller = "Product", action = "Index" },
new { Product= new ProductConstraint() }
);
then in your ProductConstraint class you would have something as follows:
public class ProductConstraint : IRouteConstraint
{
public ProductConstraint ()
{
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
//do your logic in here to split the name and the id and amke sure it is valid data
//if valid return true;
//else false;
}
}
lastly in the Method signature of the Index action (or action in the route) take the string of the - and then split it down and process as appropriate
let me know if there are any issues
paul
If you can make them slightly different this would be very simple. For example, if you can use the following URL structure:
/some-product-name-goes-here/4726482648 (notice the slash)
/category-name
/about
The following routes would do the trick:
routes.MapRoute("about", "about", ... )
routes.MapRoute("product", "{product}/{id}", ... )
routes.MapRoute("category", "{category}", ... )

MVC routing question

I want to setup routing as follows:
/Profile/Edit -> routes to Edit action
/Profile/Add -> routes to Add action
/Profile/username -> routes to Index action with parameter username, because action username doesn't exist.
So I want the second parameter to be parsed as the controller action, except when no controller with that name exists; then it should route to the default index page and use the url part as id.
Possible?
You can use regex in your route constraints like so
routes.MapRoute(
"UserProfileRoute",
"Profile/{username}",
new { controller = "Profile", action = "Index" },
new { username = "(?i)(?!edit$|add$)(.*)" });
this will match urls like /profile/addendum /profile/someusername and will ignore /profile/edit and /profile/add
Matt's solution gets you 90% of the way. However, instead of using a route constraint to exclude action names, use a route constraint to include only valid usernames, like so:
public class MustMatchUserName : IRouteConstraint
{
private Users _db = new UserEntities();
public MustMatchUserName()
{ }
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
return _db.Users.FirstOrDefault(x => x.UserName.ToLower() == values[parameterName].ToString().ToLower()) != null;
}
}
Then, as Matt points out, in the user creation process, you must enforce a rule that your ActionNames are not valid for user names.
counsellorben
Here is one way to accomplish this:
Make these your routes in Global.asax.cs:
routes.MapRoute("UserProfileRoute", "Profile/{username}",
new { controller = "Profile", action = "Index" });
routes.MapRoute("DefaultProfileRoute", "Profile/{action}",
new { controller = "Profile", action = "SomeDefaultAction" });
This will match /Profile/someUsername as expected. But it will fail for all other actions. All action names are assumed to be usernames now. A quick fix to this is to add an IRouteConstraint to the first route:
routes.MapRoute("UserProfileRoute", "Profile/{username}",
new { controller = "Profile", action = "Index" },
new { username = new NotAnActionRouteConstraint() });
routes.MapRoute("DefaultProfileRoute", "Profile/{action}",
new { controller = "Profile", action = "SomeDefaultAction" });
public class NotAnActionRouteConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
string value = values[parameterName].ToString();
// it is likely parameterName is not cased correctly,
// something that would need to be
// addressed in a real implementation
return typeof(ProfileController).GetMethod(parameterName,
BindingFlags.Public | BindingFlags.Instance) == null;
}
}
However, this is a bit ugly. Hopefully someone knows of a better solution.
You also have problems when one of your users picks a name that is the same as an action :)
Anything is possible. However, why not just make /profile your root?
If that isn't possible, you may need to hardcode your action's routes.

Having issues with MVC Routing

I'm trying to implement routing such as the following:
posts/535434/This-is-a-post-title
posts/tagged/tags+here
// Matches {controller}/{action}/{id} - Default
// Displays all posts with the specified tags
// uses PostsController : ActionTagged(string tags)
posts?pageSize=50&pageIndex=4
// Matches {controller}/{action}/{id} - Default
// Displays all posts
// uses PostsController : Index(int? pageSize, int? pageIndex)
Here's the problem I want to do this:
posts/39423/this-is-a-post-title-here
// Typically this is implemented using an action like 'Details'
// and would normally look like : posts/details/5
I can't seem to get the routing working right. I tried something like this:
{controller}/{id}/{description}
and set the default action to be "Display" which works, but then won't allow me to navigate to other named actions like "Tagged".
What am I missing?
Thanks!
Two things:
First, you should always order your routes in decreasing specificity (e.g. most specific case first, least specific case last) so that routes will "fall through", if one doesn't match it will try the next.
So we want to define {controller}/{postid}/... (must be a postid) before we define {controller}/{action}/... (could be anything else)
Next, we want to be able to specify that if the provided value for postid does not look like a Post ID, the route should fail and fall through to the next one. We can do this by creating an IRouteConstraint class:
public class PostIDConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection)
{
//if input looks like a post id, return true.
//otherwise, false
}
}
We can add it to the route definition like so:
routes.MapRoute(
"Default",
"{controller}/{postid}/{description}",
new { controller = "Posts", action = "Display", id = 0 },
new { postid = new PostIDConstraint() }
);
I'm not 100% I understand your question, but it sounds like you can just define a couple different routes.
routes.MapRoute("PostId", "posts/{id}/{title}",
new { Controller = "Posts", Action = "DisplayPost", id = 0, title = "" },
new { id = #"\d+" });
routes.MapRoute("TaggedPosts", "posts/tagged/{tags}",
new { Controller = "Posts", Action = "DisplayTagged", tags = "" });
routes.MapRoute("Default", "posts",
new { Controller = "Posts", Action = "Index" });
You can use regular expressions to validate parameters like I used for id in the first route, or if you want some better validation do something like Rex M posted. The querystring parameters pageSize and pageIndex don't need to be included in your route; they will just be passed in to your Index method as long as the parameter names match.
The part of the url that's the "description" actually isn't used.
For example, this post is 519222 and I can still get to it using the url: Having issues with MVC Routing

Resources