ASP.NET MVC 4 route username / action issue - asp.net-mvc

I am currently working on an asp.net mvc 4 application and I have the need for the following type of urls:
Urls that need to be routed
http://www.mysite.com/foo/user1 <------- {username}
http://www.mysite.com/foo/edit
http://www.mysite.com/foo/delete/1
http://www.mysite.com/bar/user1 <------- {username}
http://www.mysite.com/bar/edit
http://www.mysite.com/bar/delete/1
The issue I'm having is that currently {username} gets treated as an action so to work around the problem I implemented the following routes, but this would mean that every time I want to implement a new action, or have a controller that needs {username}, I would have to update my routes:
Only Foo routes shown
routes.MapRoute("FooSomeAction", "foo/someaction", new { controller = "Food", action = "SomeAction" });
routes.MapRoute("FooDelete", "foo/delete/{id}", new { controller = "Food", action = "Delete" });
routes.MapRoute(
"FooProfile",
"foo/{username}",
new { controller = "Foo", action = "Index", username = "" }
);
// Default route
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
2 Questions
1) Is there any way I can achieve the above urls without hardcoding all the routes?
2) What is the best way to handle a situation where someone uses a username that happens to be the same name as a controller or action name?
DotnetShadow

You could create a custom route constraint that would check if the username exists in the possible actions for the controller. If it finds an action match, it fails and will use your default route (Edit for example). You may want to cache the list for performance reasons, but I leave that up to you.
private static List<Type> GetSubClasses<T>()
{
return Assembly.GetCallingAssembly().GetTypes().Where(
type => type.IsSubclassOf(typeof(T))).ToList();
}
public static List<string> GetActionNames(string controllerName)
{
controllerName = controllerName + "Controller";
var controller = GetSubClasses<Controller>().FirstOrDefault(c => c.Name == controllerName);
var names = new List<string>();
if (controller != null)
{
var methods = controller.GetMethods(BindingFlags.Public | BindingFlags.Instance);
foreach (var info in methods)
{
if (info.ReturnType == typeof(ActionResult))
{
names.Add(info.Name);
}
}
}
return names;
}
public class UsernameNotAction : IRouteConstraint
{
public bool Match
(
HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection
)
{
int i = 0;
var username = values["username"];
var actionList = GetActionNames(values["controller"].ToString());
return !actionList.Any(a => a.ToUpper() == username.ToString().ToUpper());
}
}
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"FooProfile",
"{controller}/{username}",
new { controller = "Home", action = "Index2", username = "" },
new { IsParameterAction = new UsernameNotAction() }
);
// Default route
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}

This isn't really the answer you're looking for, sorry.
1) There's no way to route that way. There's nothing to differentiate those routes from one another, other than what you've done. I have to question why this is even necessary, I'm sure you have a good reason, but it makes no sense to me. You're still using the Index action, so why not just /foo/index/username. All I can come up with, is you have no control over the url for some reason.
2) If you use the default route, there's no problem. With your routing, problem. Your only real option is to make your controller and action names reserved words (prevent users from being created with those usernames in the database).
Sorry I couldn't really help you.

You can't do it like that unless you route every single route and that is not the best way to go.
What's so wrong in having the Action name in it?

Related

allow hyphens in URLs for asp.net mvc 2 controller names

[ActionName("about-us")]
public ActionResult EditDetails(int id)
{
// your code
}
The above works for actions but I would like to be able to do the same (or similar) for controllers, ie have a hyphen in the URL name too. Is there any easy way to achieve this (I tried the ActionName attribute but no luck)
Easiest way would be adding a custom route:
routes.MapRoute("RouteName", "controler-name/{action}/{id}", new { controller = "ControllerName", action = "Index", id = "" });
I haven't seen a controller name attribute like that before although it may be possible.
You can use custom route handler to give you needed functionality:
public class HyphenatedRouteHandler : MvcRouteHandler
{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
requestContext.RouteData.Values["controller"] = requestContext.RouteData.Values["controller"].ToString().Replace("-", "_");
requestContext.RouteData.Values["action"] = requestContext.RouteData.Values["action"].ToString().Replace("-", "_");
return base.GetHttpHandler(requestContext);
}
}
And the route should be registered using that handler:
var route = routes.MapRoute(
"Some Action",
"{controller}/{action}/{id}"
);
route.RouteHandler = new HyphenatedRouteHandler();
There is a similar quastion asked here: ASP.net MVC support for URL's with hyphens
Hyphenated route in the route table should be before the default route.
routes.MapRoute(
"InformationAbout",
"information-about/{action}/{id}",
new { controller = "InformationAbout", action = "Index", id = "" }
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
Maybe this works:
public class CustomControllerFactory : DefaultControllerFactory {
protected override Type GetControllerType(RequestContext requestContext, string controllerName) {
return base.GetControllerType(requestContext, controllerName.Replace("-", ""));
}
}
May be here is the correct answer to the question. All other are workarounds which work for a single url but this one is a generic approach
http://blog.didsburydesign.com/2010/02/how-to-allow-hyphens-in-urls-using-asp-net-mvc-2/

Dynamic ASP.Net Routing problem

I am new to ASP.Net MVC. May be this question looks simple, but i couldn't fix it. Here the scenario. I have an application listing data based on city. So the url will be looking like this
www.xxxxxx.in/chennai
www.xxxxxx.in/mumbai
www.xxxxxx.in/delhi
In normal routing the first part (chennai/mumbai) is controller in the above url, But here i dont want this to be a controller. instead i want to map the single controller (LocationController) to these URl's. Because later time i can add any number of city.
I am struck here, can someone help me out.
Try this:
routes.MapRoute(
"CityRoute", // Route name
"{city}", // URL with parameters
new { controller = "Location", action = "Index", city = "" } // Parameter defaults
);
I am not sure there won't be easier option than this, but you can try this - using route constraint. Basically, you need to know the list of cities you have and then constrain the route to match only entries in that list.
The route constraint can be implemented as follows
public class CityConstraint : IRouteConstraint
{
public static IList<string> CityNames = (Container.ResolveShared<ICityService>()).GetCities();
bool _IsCity;
public CityConstraint(bool IsCity)
{
_IsCity = IsCity;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
if (_IsCity)
return CityNames.Contains(values[parameterName].ToString().ToLower());
else
return !CityNames.Contains(values[parameterName].ToString().ToLower());
}
}
And then put the route as follows:
routes.MapRoute("Location", "{cityName}", new { controller = "LocationController", action = "Index" }, new { cityName = new CityConstraint(true) });
Also make sure the above route is listed before the default route
routes.MapRoute("Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional
);
Also note that, no controller name can be a city name.
Try this and see.
If all your routing is related to these cities than remove default route and replace it with this route definition:
routes.MapRoute(
"Default",
"{city}",
new { controller = "Location", action = "Index", city = "Mumbai" }
);
Then create a LocationController class:
public class LocationController : Controller
{
public ActionResult Index(string city)
{
// do whatever needed; "city" param has the city specified in URL route
}
}
If you still need your default route (controller/action/id) for other pages not just cities then it's probably better to put a constraint on your default route and define them like this:
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = "Home|...|..." } // put all controllers here except "Location"
);
routes.MapRoute(
"Location",
"{city}",
new { controller = "Location", action = "Index", city = "Mumbai" }
);
This will make other controllers still working and location will work just as well. The problem is of course if there's a city name that's the same as a name of one of your regular controllers. :) But you can control/avoid that as well.
You can do that by adding a route that hardcodes the controller name:
routes.MapRoute(
"location", // Route name
"{cityName}", // URL with parameters
new { controller = "location", action = "index" } // Parameter defaults
);
routes.MapRoute(
"Location", // Route name
"{controller}/{action}/{cityName}", // URL with parameters
new { controller = "Location", action = "index"} // Parameter defaults
)
This will route all requests of the form "/mumbai" to LocationController action method Index with parameter cityName set to "mumbai". It will also be able to route full controller/action spec using the second route.

Enumerating ASP.NET MVC RouteTable route URLs

I'm trying to figure out how to enumerate the URLs of Routes in the RouteTable.
In my scenario, I have the following routes defined:
routes.MapRoute
("PadCreateNote", "create", new { controller = "Pad", action = "CreateNote" });
routes.MapRoute
("PadDeleteNote", "delete", new { controller = "Pad", action = "DeleteNote" });
routes.MapRoute
("PadUserIndex", "{username}", new { controller = "Pad", action = "Index" });
In other words, if my site is mysite.com, mysite.com/create invokes PadController.CreateNote(), and mysite.com/foobaris invokes PadController.Index().
I also have a class that strongly types usernames:
public class Username
{
public readonly string value;
public Username(string name)
{
if (String.IsNullOrWhiteSpace(name))
{
throw new ArgumentException
("Is null or contains only whitespace.", "name");
}
//... make sure 'name' isn't a route URL off root like 'create', 'delete'
this.value = name.Trim();
}
public override string ToString()
{
return this.value;
}
}
In the constructor for Username, I would like to check to make sure that name isn't a defined route. For example, if this is called:
var username = new Username("create");
Then an exception should be thrown. What do I need to replace //... make sure 'name' isn't a route URL off root with?
This doesn't fully answer what you are wanting to do by preventing users from registering protected words, but there is a way you can constrain your routes. We had /username url's in our site and we used a constraint like so.
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" }, // Parameter defaults
new
{
controller = new FromValuesListConstraint(true, "Account", "Home", "SignIn"
//...etc
)
}
);
routes.MapRoute(
"UserNameRouting",
"{id}",
new { controller = "Profile", action = "Index", id = "" });
You may just have to keep a list of reserved words, or, if you really want it automatic, you could possibly use reflection to get a list of the controllers in the namespace.
You can access the route collection with this. The issue with this approach is that it requires you to explicitly register all routes you want to be "protected". I still hold to my statement you'd be better off having a list of reserved keywords stored elsewhere.
System.Web.Routing.RouteCollection routeCollection = System.Web.Routing.RouteTable.Routes;
var routes = from r in routeCollection
let t = (System.Web.Routing.Route)r
where t.Url.Equals(name, StringComparison.OrdinalIgnoreCase)
select t;
bool isProtected = routes.Count() > 0;

Routing in ASP.NET MVC 2.0

I'm looking to make a really simple route in my ASP.NET MVC 2.0 website. I've been googling for help but all the examples I can find are for really complex routing.
Basically I want all the pages in my Home Controller to resolve after the domain as opposed to /Home/
For example I want http://www.MyWebsite.com/Home/LandingPage/
To become http://www.MyWebsite.com/LandingPage/
But only for the Home controller, I want the rest of my controllers to function as normal.
I thought about creating a controller for each and just using an index, but we need lots of landing pages for our marketing like this and it would quickly make the site loaded with controllers for a single page each, which is less than ideal.
One way to do this would be to have a separate route for each landing page. Another way would be to have a single route with a constraint that matches each landing page (and nothing else).
routes.MapRoute(
"LandingPage1"
"landingpage1/{id}",
new { controller = "home", action = "landingpage", id = UrlParameter.Optional } );
routes.MapRoute(
"LandingPage2"
"landingpage2/{id}",
new { controller = "home", action = "landingpage2", id = UrlParameter.Optional } );
Note that you could probably do this with a bit of reflection as well (untested).
foreach (var method on typeof(HomeController).GetMethods())
{
if (method.ReturnType.IsInstanceOf(typeof(ActionResult)))
{
routes.MapRoute(
method.Name,
method.Name + "/{id}",
new { controller = "home", action = method.Name, id = UrlParameter.Optional } );
}
}
The RouteConstraint solution would be similar except that you'd have a single route with a custom constraint that evaluated whether the appropriate route value matched one of the methods on the HomeController and, if so, replaced the controller and action with "home" and the matched value.
routes.MapRoute(
"LandingPage",
"{action}/{id}",
new { controller = "home", action = "index", id = UrlParameter.Optional },
new LandingPageRouteConstraint()
);
public LandingPageRouteContstraint : IRouteConstraint
{
public bool Match
(
HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection
)
{
// simplistic, you'd also likely need to check that it has the correct return
// type, ...
return typeof(HomeController).GetMethod( values.Values["action"] ) != null;
}
}
Note that the route per page mechanism, even if you use reflection, is done only once. From then on you do a simple look up each time. The RouteConstraint mechanism will use reflection each time to see if the route matches (unless it caches the results, which I don't think it does).
I think you are missing the default route.
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
So, when you type www.mywebsite.com, the controller, action, and id parameters would have the following values:
controller : Home
action: Index
id : ""

ASP.NET MVC Route with dash

I've got ASP.NET MVC routing question.
I prepared following routing table to map such url
mywebsite/mycontroller/myaction/14-longandprettyseoname
to parameters:
14 => id (integer)
longandprettyseoname -> seo_name (string)
routes.MapRoute(
"myname",
"mycontroller/myaction/{id}-{seo_name}",
new { controller = "mycontroller", action = "myaction", id = 0, seo_name = (string)null });
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" });
It works for URL above but it has problems for following type of urls
mywebsite/mycontroller/myaction/14-long-and-pretty-seo-name
Is that possible to make it working?
EDIT:
"mycontroller/myaction/{seo_name}-{id}"
seems to be working
The most obvious way to do this is to use constraints.
Since that your id is an integer, you can add a constraint which will look for an integer value:
new { id = #"\d+" }
and here is the whole route:
routes.MapRoute("myname","mycontroller/myaction/{id}-{seo_name}",
new { controller = "mycontroller", action = "myaction" },
new { id = #"\d+"});
My solution is define route as:
routes.MapRoute("myname","mycontroller/myaction/{id}",
new { controller = "mycontroller", action = "myaction"});
and parse id and seoname manualy using Regex in HTTP handler:
var routeData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(context));
var match = System.Text.RegularExpressions.Regex.Match((string)routeData.Values["id"], #"^(?<id>\d+)-(?<seoname>[\S\s]*)$");
if (!match.Success)
{
context.Response.StatusCode = 400;
context.Response.StatusDescription = "Bad Request";
return;
}
int id = Int32.Parse(match.Groups["id"].Value);
string seoname = match.Groups["seoname"].Value;
I don't think the route will be distinguishable as it will not be able to figure which "-" to split at to specify the {id} and the {seo-name}.
How about using underscores for your SEO name? Or you could just use the SEO name as the actual {id}. If the SEO name is something that is going to be unique, this is a very viable option you can use as a pseudo primary key to that entry in your db (assuming it's pulling something from a DB)
Also, utilize Phil Haack's route debugger to see what works and doesn't work.
Define a specific route such as:
routes.MapRoute(
"TandC", // Route controllerName
"CommonPath/{controller}/Terms-and-Conditions", // URL with parameters
new { controller = "Home", action = "Terms_and_Conditions" } // Parameter defaults
);
But this route has to be registered BEFORE your default route.
What you could do is create a custom controller factory. That way you can have custom code to decide which controller needs to be called when.
public class CustomControllerFactory : IControllerFactory
{
#region IControllerFactory Members
public IController CreateController(RequestContext requestContext, string controllerName)
{
if (string.IsNullOrEmpty(controllerName))
throw new ArgumentNullException("controllerName");
//string language = requestContext.HttpContext.Request.Headers["Accept-Language"];
//can be used to translate controller name and get correct controller even when url is in foreign language
//format controller name
controllerName = String.Format("MyNamespace.Controllers.{0}Controller",controllerName.Replace("-","_"));
IController controller = Activator.CreateInstance(Type.GetType(controllerName)) as IController;
controller.ActionInvoker = new CustomInvoker(); //only when using custominvoker for actionname rewriting
return controller;
}
public void ReleaseController(IController controller)
{
if (controller is IDisposable)
(controller as IDisposable).Dispose();
else
controller = null;
}
#endregion
}
To use this custom controllerfactory, you should add this in your global.asax
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ControllerBuilder.Current.SetControllerFactory(typeof(CustomControllerFactory));
}
Note that this only works for the controller, not for the actions... To hook up custom rewriting on actions before they get executed, use this code:
public class CustomInvoker : ControllerActionInvoker
{
#region IActionInvoker Members
public override bool InvokeAction(ControllerContext controllerContext, string actionName)
{
return base.InvokeAction(controllerContext, actionName.Replace("-", "_"));
}
#endregion
}
I got most of this code from this blog and adjusted it to my needs. In my case, I want dashes to separate words in my controller name but you can't create an action with a dash in the name.
Hope this helps!

Resources