How to prevent controllers in wrong areas from being used in MVC5 - c#

My MVC application is set up with controllers at the root/global level. These have no explicit Area. I also have a single "Admin" area. URLs that begin with /admin/ are routed to the matching controller in the Admin area. Other URLs are routed to the matching global controller. This is working fine for the most part.
The issue I'm having is that in the case when a URL matches a controller in the Admin area when one of the same name doesn't exist on the global area, the request is incorrectly routed to the controller in Admin area. I know this is happening because I put a breakpoint on the matching action in the relevant controller.
For example, I have a controller called CalendarController in the Admin area. When I visit /admin/calendar, it works because it finds the action and the corresponding view in Areas/Admin/Views/Calendar/Index.cshtml. The problem occurs when I visit /calendar. I do not have a controller named Calendar at the root level, but for some reason it routes the request to the Admin area's CalendarController, which I don't want. I want it to return a 404 because no CalendarController exists at the root level. Instead, I get an error because it's searching for the view at the root level (at /Views/Calendar/Index.cshtml) even though the matching controller was in the Admin area.
How can I prevent the Admin area from being searched for matching controllers except when the URL has /admin in it?
Here's the relevant route code, which is basically stock except for the namespaces addition. The issue still happens without the namespace. There are more routes in the actual application, but I'm getting the same behavior in a brand new MVC project.
public class AdminAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Admin";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new[] { "AreaProblem.Areas.Admin.Controllers" }
);
}
}
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
What I mean by a controller at the "root level" is Controllers/HomeController in this screenshot. I want URLs that don't start with /admin to only look at those controllers. The problem is that it's also searching in Areas/Admin/Controllers.

So the MVC routing engine will look in different namespaces to try and find a matching controller. You can solve this by specifying a namespace (like you did for the admin area). You can also specify that a route not search other namespaces using DataTokens.
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new [] { "AreaProblem.Controllers" }
).DataTokens["UseNamespaceFallback"] = false;

Related

MVC not routing to only one of my Controller's Action Result?

I have a really weird one to me that I can't figure out. I'm not an expert with routing but this should be straight forward.
I have a controller called NewsletterController and it has the typical Index ActionResult. If I run my site in debug mode and use a link to go to my Newsletter section I get a 403.14 Forbidden message. If I add the "Index" to the route then it will go to the ActionResult just fine. I have other controllers setup with a Index ActionResult the exact same way and work just fine. It's something about this specific controller that is not working and I just don't see what the problem is.
This is my controller code:
public class NewsletterController : Controller
{
public ActionResult Index()
{
var vm = new NewsletterViewModel();
...CODE REMOVED FOR SPACE
return View(vm);
}
}
This is the action link from my _Layout.cshtml:
#Html.ActionLink("Newsletters", "Index", "Newsletter", null, new { #class = "nav-link" })
My Routing class
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
If I go here I get the 403.14 Forbidden message.
http://localhost:59571/Newsletter/
If I add the Index to the route it goes to the page.
http://localhost:59571/Newsletter/index
403.14 Forbidden typically occurs when you try to browse to a directory on the site and the Web site does not have the Directory Browsing feature enabled, and the default document is not configured.
In this case you have an actual Newsletter folder which is conflicting with the default route of the NewsletterController when you try to call
http://localhost:59571/Newsletter/
It will try to return the actual content of that folder which will fail if the feature is not enabled.
Either remove or rename the folder to something that does not conflict with any of your controller names

MVC route matching routes that aren't valid

Problem
For the default route MVC is returning a "Multiple types were found that match the controller named 'admin'." error instead of a 404 not found. There is no admin controller in that namespace.
We're using MVC 5.2.2.
Background
We're using MVC areas. Two areas contain an "admin" controller. When you use the full path as defined in their respective routes, both areas' admin controllers are accessible and work correctly.
The problem arises when you try to access "admin" from the default route. Admin does not exist in that context, so we'd expect a 404 not found, however instead we receive:
Multiple types were found that match the controller named 'admin'. This can happen
if the route that services this request ('{controller}/{action}/{id}') does not
specify namespaces to search for a controller that matches the request. If this
is the case, register this route by calling an overload of the 'MapRoute' method
that takes a 'namespaces' parameter.
The request for 'admin' has found the following matching controllers:
DMP.Stock.Web.Controllers.AdminController
DMP.Inventory.Web.Controllers.AdminController
Here is our default route and two area routes:
public static void RegisterRoutes(RouteCollection routes)
{
// Default Route
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "DMP.Core.Web.Controllers" }
);
}
public override void RegisterArea(AreaRegistrationContext context)
{
// Area 1: Stock
context.MapRoute(
name: "Stock_default",
url: "Stock/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "DMP.Stock.Web.Controllers" }
);
}
public override void RegisterArea(AreaRegistrationContext context)
{
// Area 2: Inventory
context.MapRoute(
"Inventory_default",
"Inventory/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "DMP.Inventory.Web.Controllers" }
);
}
/Stock/Admin/Index works correctly.
/Inventory/Admin/Index works correctly.
/Admin/ does not work correctly (expect: 404 not found, receive "multiple controllers" error).
The errors suggests we add namespaces to our routes, but as you can see above both the default and the two areas already have a namespace definition. The default route points at a namespace without any "admin" controllers in it.
I think MVC is trying to be "helpful" by searching for possible controllers that could match the requested URL. Is there any way I can turn that off?
I've been able to resolve this issue myself. Here is the solution I found:
// Default Route
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "DMP.Core.Web.Controllers" }
).DataTokens["UseNamespaceFallback"] = false;
Note the addition of .DataTokens["UseNamespaceFallback"] = false; this is what resolves the issue. There isn't very much (any?) documentation on this functionality, however I found it while reading the MVC source code, specifically within DefaultControllerfactory (the source of this issue).
After you know to google for "UseNamespaceFallback" you can find a few blog posts and questions where people have had a similar issue and resolved it the same way. However I can find no MSDN documentation on this DataToken.

MVC Routing picking up Area controllers at root

I'm having difficulties with my controllers that are in an Area answering requests on routes that aren't for the area. So I have a setup like this (Extra stuff cut):
/Areas/Security/Controllers/MembersController.cs
/Areas/Security/SecurityAreaRegistration.cs
/Controllers/HomeController.cs
I have my area for security defined:
namespace MyApp.Web.Areas.Security
{
public class SecurityAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Security";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Security_default",
"Security/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
}
And my global routing rules:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("{*robotstxt}", new { robotstxt = #"(.*/)?robots.txt(/.*)?" });
routes.IgnoreRoute("{*favicon}", new { favicon = #"(.*/)?favicon.ico(/.*)?" });
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "MyApp.Web.Controllers" }
);
In my global asax I'm doing quite a few things but the relevant part is that I call AreaRegistration.RegisterAllAreas(); then I call the routing function that does the above.
But my problem is that requests for "/Members/" are hitting my Members controller using my "Default" route... even though the controller's not in the namespace I specified. Then when it tries to run it can't find it's Views cause they're defined in the Area and it's trying to find them in the overall Views folders. I tried making the route namespace "Weird.Namespace.With.No.Content" and it STILL hits the Members controller - I can't find any way to make it not use that controller. How do I make it not answer requests that aren't in it's area?
Ended up finding a solution by changing the Route to:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "MyApp.Web.Controllers" }
).DataTokens["UseNamespaceFallback"] = false;
For some reason with that unset it seemed to always find my Controllers no matter where they were and totally disregard my namespaces - even from other referenced assemblies. Looking through the ILSpy of DefaultControllerFactory it looks like GetControllerType eventually falls back to searching absolutely every controller if it doesn't find the controller in the namespaces you asked for...
This flag seems to be set automatically on the Routes I made in a specific area, but not on the ones I made globally. When I set it on the global ones they began behaving how I had originally expected. I have no idea why you'd ever want to turn this on...
You should register the namespace on your area as well.
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Security_default",
"Security/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new []{ "MyApp.Web.Areas.Security.Controllers"},
);
}
And then make sure all your controllers are in their appropriate namespaces.
Are you calling register all areas after registering your main route?
This is the default snippet from the MVC template (note how area regisration goes first)
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);

How to set up routing so that Index does show?

So I know google can penalize a site if you have the same content on multiple urls... unfortunately, in MVC this is too common i can have example.com/, example.com/Home/ and example.com/Home/Index and all three urls would take me to the same page... so how do I make sure that whenever Index is in the url, that it redirects to the same without the Index and of course the same thing with the Home
Perhaps this little library may be useful for you.
This library is not very convinient in your case, but it should work.
var route = routes.MapRoute(name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });
routes.Redirect(r => r.MapRoute("home_index", "/home/index")).To(route);
routes.Redirect(r => r.MapRoute("home", "/home")).To(route);
The way I handle this is for default pages like Index is to simply create an explicit route for only one of them. I.e. "example.com/People" would be the route for People/Index, and there would be no valid page at the url "/example.com/People/Index".
The Home example is unique in that it has potentially three different URLs. Again in this case I'd simply create a route for "example.com" for that Index action, and not support the other two urls. In other words, you would never link to the other forms of the URL, so their absence should never cause a problem.
We use a Nuget package called AttributeRouting to support this. When you specifiy a GET route for a page, it overrides the defaults for MVC.
Using AttributeRouting usually you'd map the index to [GET("")] but for the special case of Home where you also want to also support the root URL that omits the controller name , I think you'd also add an additional attribute with IsAbsoluteUrl:
public class HomeController : BaseController
{
[GET("")]
[GET("", IsAbsoluteUrl = true)]
public ActionResult Index()
{...
So I found a way to do it without any external Library...
In my RouteConfig I had to add these two routes at the top, just below the IgnoreRoute
routes.MapRoute(
"Root",
"Home/",
new { controller = "Redirect", action = "Home" }
);
routes.MapRoute(
"Index",
"{action}/Index",
new { controller = "Redirect", action = "Home" }
);
Then I had to create a new Controller called Redirect and I created a method for each of my other Controllers like this:
public class RedirectController : Controller
{
public ActionResult Home()
{
return RedirectPermanent("~/");
}
public ActionResult News()
{
return RedirectPermanent("~/News/");
}
public ActionResult ContactUs()
{
return RedirectPermanent("~/ContactUs/");
}
// A method for each of my Controllers
}
That's it, now my site looks legit. No more Home, no more Index in my URLs, this of course has the limitation of not being able to accept parameters to any of the Index methods of your Controllers though if it was really necessary, you should be able to tweak this to achieve what you want.
Just an FYI, if you want to pass an argument to your Index Action, then you can add a third route like this:
routes.MapRoute(
name: "ContactUs",
url: "ContactUs/{id}/{action}",
defaults: new { controller = "ContactUs", action = "Index", id = UrlParameter.Optional }
);
This will create a URL like this: /ContactUs/14

Using RouteLink with Areas

In my MVC application I have a default route defined:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new []{ "Demo.Controllers" }
);
I created a new Area called Admin and it added a route to in the AdminAreaRegistration class:
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", controller = "Home", id = UrlParameter.Optional }
);
}
In my main _Layout file I tried to do the following:
#Html.RouteLink("Admin", "Admin_default")
It only works in certain cases (such as if I'm already in an Admin page). If I am in the /Home/About section of my site, then the URL gets generated like so:
/Admin/Home/About
If I am in my Index action of the Home controller (in the main area, not admin) then the URL gets generated like so:
/Admin
Why doesn't RouteLink work like I think it should using Areas in MVC?
#Html.RouteLink("Admin", "Admin_default") this route uses the route with the name of Admin_default so it will always use that route to generate the url.
#Html.RouteLink("Admin", "Admin_default", new { controller = "Home", action = "Index" })
When you don't specify stuff like route values most of MVC uses the values that currently exist. In this case since the action and controller values are null it looks at the RouteValues and checks to see if they're there and if they're found they use the values found there instead.
This is sometimes helpful. For instance if you want {id} to be populated with the same value that was used on the page.
Url: /home/edit/1701
#Html.RouteLink("Refresh", "Default") would result in the same exact url - you can specify overrides if you want.
I would also suggest not naming your routes. You can force yourself to do this by passing null to the route name arg when creating it.
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(null,
"Admin/{controller}/{action}/{id}",
new { action = "Index", controller = "Home", id = UrlParameter.Optional }
);
}
You can then use RouteLink like so:
#Html.RouteLink("Admin",
new { controller = "Home", action = "Index", area = "Admin" })
Reply to comment
Wish I could say I came up with the idea. Learned this from Steve Sanderson's MVC book. There are a ton of method overloads that take a route name as a parameter (RouteLink, but also RedirectToRoute and others). Not using route names forces you to use other overloads of those methods, which IMO are more intuitive and easier to "get right".
Naming your routes is not recommended since it creates dependencies in your view towards the existing routes. I suggest using a null value

Categories

Resources