Enumerating ASP.NET MVC RouteTable route URLs - asp.net-mvc

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;

Related

ASP.NET MVC Custom Route class - Can't get route data

I'm trying to implement my own route class, inheriting from the default Route.
This is what my custom route class looks like:
public class FriendlyRoute : Route
{
public FriendlyRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
: base(url, defaults, routeHandler)
{
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var routeData = base.GetRouteData(httpContext);
var controllerName = routeData.Values["controller"].ToString();
var actionName = routeData.Values["action"].ToString();
routeData.Values["controller"] = fix(controllerName);
routeData.Values["action"] = fix(actionName);
return routeData;
}
private string fix(string name)
{
//Remove dashes: "my-controller" => "mycontroller"
}
}
What I'm doing is accepting urls with dashes and routing the to the correct action ("my-controller/my-action" to "MyController/MyAction"), but I have some more plans for this custom route to.
To put my custom route class in action, I use the following route config:
routes.Add("Default",
new FriendlyRoute("{controller}/{action}/{id}",
new RouteValueDictionary(new { controller = "Public", action = "Start", id = UrlParameter.Optional }),
new MvcRouteHandler()));
This works fine! But I'm not happy with the url structure. I want to have some urls with no controller names only action names (e.g. "/about", "/contact") and some with controller names (e.g. "/mypage", "/mypage/invoices"). I start by using the default route class (not my own custom) and fix this problem:
routes.Add("MyPages",
new Route("MyPage/{action}",
new RouteValueDictionary(new { controller = "MyPage", action = "Summary"}),
new MvcRouteHandler()));
routes.Add("Public",
new Route("{action}/{id}",
new RouteValueDictionary(new { controller = "Public", action = "Start", id = UrlParameter.Optional }),
new MvcRouteHandler()));
This also works fine, but now there's no support for urls with dashes. So I just swap in my custom route class into the route config:
routes.Add("MyPages",
new FriendlyRoute("MyPage/{action}",
new RouteValueDictionary(new { controller = "MyPage", action = "Summary" }),
new MvcRouteHandler()));
routes.Add("Public",
new FriendlyRoute("{action}/{id}",
new RouteValueDictionary(new { controller = "Public", action = "Start", id = UrlParameter.Optional }),
new MvcRouteHandler()));
Now when I run the application I try to go to the default page ("/") it crashes because the call to base.GetRouteData(httpContext) in my FriendlyRoute.GetRouteData() returns null.
I'm all new to creating a custom route class, so any hints on what I' doing wrong would be appreciated.
After adding the additional two routes, when the root url is hit ("/") it will be processed against route definitions sequentially top to bottom until a match is made. So now for the first route "MyPage/{action}" it will call the public abstract RouteData GetRouteData(HttpContextBase httpContext); method, which in turn matches the url with the route definition and check for constraints. It returns a RouteValueDictionary object in case of match and constraint check, null otherwise. So for the first route definition it is bound to return null as the url does not match. You should add a null check as foollowing
if (routeData != null)
{
var controllerName = routeData.Values["controller"].ToString();
var actionName = routeData.Values["action"].ToString();
routeData.Values["controller"] = Fix(controllerName);
routeData.Values["action"] = Fix(actionName);
}
relevant resource: Route.cs
hope this helps.

ASP.NET MVC 4 route username / action issue

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?

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.

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!

In ASP.NET MVC, how can I rewrite from /somepage to /pages/showpage/somepage?

How can I rewrite a url like: /SomePage to /Pages/ShowPage/SomePage?
I tried:
routes.MapRoute("myroute", "{id}", new { controller = "Pages", action = "ShowPage" });
But It's not working. What am I doing wrong?
If you are trying to say "navigating to /SomePage shall call PagesController.ShowPage("SomePage")", then you probably want this:
// Find a method with signature PagesController.ShowPage( string param )
// and call it as PagesController.ShowPage("SomePage")
route.MapRoute(
"MyRoute",
"SomePage",
new { controller = "Pages", action = "ShowPage", param = "SomePage" } );
This will only redirect the exact URL /SomePage. If you are trying to say "navigating to /{something} shall run the PagesController.ShowPage( something ) method", then that is a more difficult problem.
If this second case is indeed what you want, then you'll have to define it after most of your other routes. The routing entry you would want would be:
// This will call the method PagesController.ShowPage( string param )
route.MapRoute(
"MyRoute",
"{param}",
new { controller = "Pages", action = "ShowPage" } );
I think this should be:
routes.MapRoute("myroute", "{controller}/{action}/{id}", new { controller = "Pages", action = "ShowPage", id = "SomePage" });
It was wrong because I think in your application, there is this default map route :
routes.MapRoute(
"root", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
it will looks for the controller with the name equal to the id that you passed in, if you remove this default map route, your map route would work.
You should try this route debugger tool, it helps out a lot:
Debugger Tool
You need to ensure that your route maps to your objects. In your case, you need to have a controller called PagesController with a method called ShowPage, with a single parameter called pagename (if you use a route like the following).
routes.MapRoute("route", "{controller}/{action}/{pagename}", new { controller = "Pages", action = "ShowPage", pagename = "" } );
Also, do not forget that you can use regex when specifying the route - this may help you ensure the correct route is used by the routing engine.
routes.Add(new Route("{controller}/{action}/{params}",
new RouteValueDictionary { { "controller", "user" }, { "action", "login" }, { "params", "" } },
new RouteValueDictionary { { "controller", #"^(?!Resources)\w*$" }, { "action", "[a-zA-Z]+" } },
new MvcRouteHandler()));
You can write your own static routing. Above the default route add your own.
routes.MapRoute("MyRoute", // Route name
"Pages/ShowPage/SomePage/{id}", // URL with parameters
new { controller = "Pages", action = "ShowPage", id = "" } // Parameter defaults
);
Now, if SomePage is a variable, you'll want something like this:
routes.MapRoute("MyRoute", // Route name
"Pages/ShowPage/{somePage}/{id}", // URL with parameters
new { controller = "Pages", action = "ShowPage", id = "", somePage = "" } // Parameter defaults
);
You can leave out the {id} if you want, just leave it out of your action parameters.
routes.MapRoute("MyRoute", // Route name
"Pages/ShowPage/{somePage}", // URL with parameters
new { controller = "Pages", action = "ShowPage", somePage = "" } // Parameter defaults
);

Resources