MVC 4 catch all route never reached - asp.net-mvc

When attempting to create a catch all route in MVC 4 (something I've found several examples of, and based my code on) it returns a 404 error. I'm running this on IIS 7.5. This seems like a straight forward solution, so what am I missing?
One note, if I move the "CatchAll" route above the "Default" route it works. But of course then none of the other controllers are ever reached.
Here is the code:
Route.Config:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
"CatchAll",
"{*dynamicRoute}",
new { controller = "CatchAll", action = "ChoosePage" }
);
Controller:
public class CatchAllController : Controller
{
public ActionResult ChoosePage(string dynamicRoute)
{
ViewBag.Path = dynamicRoute;
return View();
}
}

Since the ultimate goal of creating the catchall route was to be able to handle dynamic urls and I was unable to find a direct answer to the original issue above, I approached my research from a different perspective. In doing so I came across this blog post: Custom 404 when no route matches
This solution allows handling of multiple sections within a given url
(i.e. www.mysite.com/this/is/a/dynamic/route)
Here is the final custom controller code:
public override IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
{
if (requestContext == null)
{
throw new ArgumentNullException("requestContext");
}
if (String.IsNullOrEmpty(controllerName))
{
throw new ArgumentException("MissingControllerName");
}
var controllerType = GetControllerType(requestContext, controllerName);
// This is where a 404 is normally returned
// Replaced with route to catchall controller
if (controllerType == null)
{
// Build the dynamic route variable with all segments
var dynamicRoute = string.Join("/", requestContext.RouteData.Values.Values);
// Route to the Catchall controller
controllerName = "CatchAll";
controllerType = GetControllerType(requestContext, controllerName);
requestContext.RouteData.Values["Controller"] = controllerName;
requestContext.RouteData.Values["action"] = "ChoosePage";
requestContext.RouteData.Values["dynamicRoute"] = dynamicRoute;
}
IController controller = GetControllerInstance(requestContext, controllerType);
return controller;
}

It's probably because whatever route your're testing this with is matching your 1st - Default route. The way the routing in MVC works, any address you pass in will try to match routes in your routes collection in order of appearance. Once it find the 1st matching route it aborts further execution. In this case your Default route is 1st one in the list so if it is matched your second route will never be examined.
Basically write something like http://www.mysite.com/Home/Testing/Item/Page in your address bar and this should fail to match to your Default route and then try to match the CatchAll route.

Try defining the optional string dynamicRoute parameter on your route:
routes.MapRoute(
"CatchAll",
"{*dynamicRoute}",
new { controller = "CatchAll", action = "ChoosePage", dynamicRoute = UrlParameter.Optional } );

Related

ASP.NET MVC4 Searching for controller in wrong area

I'm using default MVC routing setup:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
I have area defined as:
public class AdministrationAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Administration";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Administration_default",
"Administration/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
And I have a controller in that area:
namespace XXX.Areas.Administration.Controllers
{
public class CountryController : Controller
{
public ActionResult Index()
{
///
}
}
}
When I type
/Administration/Country
it works good as it is desired.
When I type
/Country
action still gets invoked, though view is not found so I get an error.
Why is MVC accepting
/Country
as valid route? I don't have another CountryController in non-area zone.
Add NameSpace in the Global.asax file for the default Route.
var route = routes.MapRoute(
"Default", // Route name
"{controller}/{action}", // URL with parameters
new { controller = "Home", action = "Index" }, // Parameter defaults,
new[] { "YourNameSpace.Controllers" }
);
Add NameSpace in the AreaRegistration class present in your Area
public class MyArea : AreaRegistration
{
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"test",
"Test/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new[] { "MyProjectNameSpace.Controllers" }
);
}
}
Explanation
I have a following area in my application. So the below highlighted section is out concerned Controller. ok.
Figure -1
I typed Url : http://localhost:2474/ActionFilterAttribute/index
Before moving toward the destination. I will show you some how I initialized my test. I added a Reference of RoureDebugger. You can get the Dll from this location. Then I added a line of code in my Global.asax file under Application_Start Handler.
RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);
So, finally using the above mentioned Url, I started debugging the application. Finally I saw the below picture.
Figure -2
Question
action still gets invoked, though view is not found so I get an error.
Answer
So if you pay attention to the above highlighted Route, well, this is the Default Route. This pattern is matched with Url as mentioned above. But View will not be found in this case and that's the reason your Controller Action Method is Invoked.
Before moving to the next part that why did I get 404. I will show you some test I did in my sample application.
I created a class which derives from ActionFilterAttribute like below. This contains only one Override that's called OnResultExecuting. This Handler executes before executing the View corresponding to particular Action
The purpose of Creating this class is just to verify what is happening with RouteData and DataTokens.
public class MyActionFilter : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
var viewResult = filterContext.Result as ViewResult;
if (viewResult != null)
{
var razorEngine = viewResult
.ViewEngineCollection
.OfType<RazorViewEngine>()
.Single();
var viewName = !String.IsNullOrEmpty(viewResult.ViewName) ?
viewResult.ViewName :
filterContext.RouteData.Values["action"].ToString();
var razorview = razorengine
.FindView
(
filtercontext.Controller.ControllerContext,
viewname,
viewResult.MasterName,
false
).View as RazorView;
}
base.OnResultExecuting(filterContext);
}
}
Ok. Let's come back to the original question. Why did I get 404?
Your Controller will be picked up by the default base route {controller}/{action} before it checks the Area Route and therefore will look for the View in the Root/Views instead of in the Area/views.
To examine this, I set the debugger in the Action Method of Controller inside the Area and found that that there is no DataToken Information when the Requested url is without Area Name. let's see more details about DataToken in Debug Mode
Figure -3
If you pay attention to the ControllerContext, I enumerated the DataTokens, which is showing no key/Value. It's because no view is found pertaining to that controller under the Root Directory
How can you say that the currently located Directory is Root Directory? Proof is below
Figure -4
There is no Namespace or any Area mentioned in the RouteData values. right?
Now let's move to the RouteData that matched the pattern which is containing the Area Name. So, this time my Url is : http://localhost:2474/mypractise/ActionFilterAttribute/index and below is the RouteData matched by URLRoutingModule
Figure -5
Please pay attention to the highlighted section, this time the Route matched belongs to AreaName pattern and matched value is false for the Default Route which belongs to some RouteData at Root Directory. Right?
My final details for the DataTokens in case of above mentioned requested Url. You can see the Namespace details and Area details this time.
Figure -6
Conclusion :
When the Controller is inside the Area and your DataTokens are not showing the information for Area , NameSpace and UseNameSpaceFallback informations. That means you will get 404. As mentioned in Figure-4, your requested Url was correct, so you got the DataTokens and As mentioned in Figure 3, DataTokens were not shown because the requested Url does not contains the Area Name and despite of the fact that you have got the RouteData as mentioned in Figure 2 because it's a default Url Pattern. Finally try to execute the third line of code in OnResultExecuting. It will show null because View is not found.
Hope this explanation will help you.
Check it. Modify the default route in your Global.ascx.cs file like so.
var route = routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "APPLICATION_NAMESPACE.Controllers.*" }
);
route.DataTokens["UseNamespaceFallback"] = false;
EDIT:
My apologies. It seemed like you didn't want it to do this as well as know why.
You are running into the fact that the default routing will look for anything that is a controller. Even if it's in an Area. You can overcome this default behavior by simply adding the namespaces parameter to the route and specify what the default routing should be looking for with controllers.
The solution that I provided above is merely a fix if you wanted to not serve the view of an area outside the area itself.
There is a great article on why this is occurring here.

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?

ASP.NET MVC3 Routing various subfolders to the same controller

I'm trying to set up my MVC project to have URLs so that I can go to:
/Groups/
/Groups/Register
/Groups/Whatever
But in my controller, I can also flag some actions as admin only, so that they are accessed at:
/Admin/Groups/Delete/{id}
I would like to keep one GroupController, and have actions so that:
public class GroupController : Controller
{
public ActionResult Index(){
return View();
}
[AdminAction]
public ActionResult Delete(int id){
...
return View();
}
}
Allows:
/Groups is a valid URL.
/Admin/Groups is a valid URL (but would call some other action besides Index - maybe)
/Admin/Groups/Delete/{id} is a valid URL (post only, whatever)
/Groups/Delete is an INVALID url.
I realize this is probably a pretty broad question, but I'm new to MVC and I'm not really sure where to start looking, so if you could just point me in the right direction that would be hugely appreciated.
As we discussed in the comments below, while it is possible to use my original answer below to achieve the routing solution you requested, a better solution is to use Areas, establish an Admin area, and create controllers in your Admin area to handle the administrative tasks for different objects, such as Group, User, etc. This allows you to set up restricted administrative functions more easily, and is both a better design and a better security model.
ORIGINAL ANSWER
What you want can be accomplished by using the following routes:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Admin", // Route name
"admin/{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
However, as Akos said in the comments, it is a much better design to separate the administrative functions into a different controller. While this is possible, I would recommend against using this design.
UPDATE
It is possible to use a RouteConstraint on your Default route to make it fail if Admin actions are requested. The Default route would look like this:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional, // Parameter defaults
new { action = IsNotAdminAction() } // route constraint
);
The RouteConstraint would look like this:
public class IsNotAdminAction : IRouteConstraint
{
private string adminActions = "create~delete~edit";
public IsNotAdminAction()
{ }
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// return false if there is a match
return !adminActions.Contains(values[parameterName].ToString().ToLowerInvariant());
}
}

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