MVC route matching routes that aren't valid - c#

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.

Related

Why does an ASP.NET MVC route have a name?

In an ASP.NET MVC 5 web application, there is a RouteConfig class that registers many routes. In all of the examples I have seen so far, only the "Default" route has a non-empty name. The URL pattern and default route values seem to be sufficient to correctly associate any URL to the controller and action to execute. So, is there any benefit to naming a route? For debugging or logging? Just for self-documenting the code?
For example:
public class RouteConfig
{
public static void RegisterRoutes( RouteCollection routes )
{
routes.IgnoreRoute( "{resource}.axd/{*pathInfo}" );
// Most pages.
routes.MapRoute( name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "home", action = "index", id = UrlParameter.Optional }
// A special page.
// Should the route have a name?
routes.MapRoute( name: "",
url: "special",
defaults: new { controller = "special", action = "index", HttpVerbs = HttpVerbs.Get } );
}
}
The main reason to name a route is so that if you need a link to a route from somewhere in the app, you can reference the name. That way if the actual URL/path changes you don't break everywhere in the code that references that route.

MVC5 routing namespace prioritize

In MVC5 routing I need one situation. I have 2 namespaces with same controller name. I want to dynamically chose between controllers without conflict
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "Namespace1","Namespace2" });
In here if there are 2 controllers with same name and actions under both namespaces it gives conflict error. What I want to do is prioritize namespaces.
If both namespaces has Home.Index action I want Namespace1 take the lead instead of error.
If namespace1 does not have the action I want system check for Namespace2.
I tried few ways but I don't want to use attribute routing better architecture.
We have a BaseECommerce asp.net mvc project and UI projects that are taking inherit of BaseEcommerce project. Purpose is if there is same Controller and Action in UI project use that. Else use base project.
Thanks for help.
You prioritize routes by registering them in the correct order - the first match wins.
You cannot prioritize namespaces. The namespaces field is for specifying all of the namespaces for a specific route (which is usually a single namespace). These are the set of namespaces where the specific route will scan for controllers.
So, if you want to avoid a conflict between similar named controllers, you need to have a route for each namespace. But, since the first matching route always wins, for it to work each controller action will need a unique URL.
routes.MapRoute(
"Default", // Route name
"Namespace2/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "Namespace2.Controllers" });
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "Namespace1.Controllers" });
There are other ways of making routes match requests than above - for ideas, see this answer.
Note that you could use a custom route constraint to check whether some module is loaded into your application and if so, disable the route.
public class ModuleNotInstalledConstraint : IRouteConstraint
{
public bool Match(
HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection)
{
if (module is installed)
{
return false;
}
return true;
}
}
Usage
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { _ = new ModuleNotInstalledConstraint() },
new[] { "Namespace1.Controllers" });
This way, the route is ignored if the module is available, so you can use a route with the exact same signature and it will be "overridden" when the module is available. But you will still need to specify namespaces to ensure the scan doesn't conflict with controllers that have the same name.

How to prevent controllers in wrong areas from being used in MVC5

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;

MVC controllers with same name with areas but finds "Multiple types"

I have myself a little problem with areas and controllers.
I have a controller called "HomeController" this is the default controller, I then added a new area called "Supplier", I then copied over my HomeController and removed all the methods I don't want to edit and just edited my Index method.
Now when I build it works fine but when I go to my home controller as a supplier it comes up with this error
Multiple types were found that match the controller named 'Home'. 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 'Home' has found the following matching controllers:
TestProject.Controllers.Home
TestProject.Areas.Supplier.Controllers.Home
I have updated my areas like so
This is my default area
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 },
namespaces: new[] { "TestProject.Controllers" }
);
}
}
And here is my area route file
public class SupplierAreaRegistration: AreaRegistration
{
public override string AreaName
{
get
{
return "Supplier";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"SupplierHomeIndex",
"Home/Index/{id}",
defaults: new { area = "Supplier", controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new[] { "TestProject.Areas.Supplier.Controllers" },
constraints: new { permalink = new AreaConstraint(AreaName) }
);
context.MapRoute(
"SupplierDefault",
"{controller}/{action}/{id}",
defaults: new { area = "Supplier", action = "index", id = UrlParameter.Optional },
namespaces: new[] { "TestProject.Controllers"},
constraints: new { permalink = new AreaConstraint(AreaName) }
);
}
}
Can anyone sign some light on this? I have looked at many topics and answers for this via Google and Stackoverflow however nothing seems to work for me.
You've customized the area's routes and removed the Supplier URL prefix. When the routing framework spins up it merely collects all controllers from your application, regardless of where they are, and then looks for a match based on the URL. In your case, you now have two controllers that are both bound to the URL /Home/*. Typically, the area's URL would be prefixed with the area's name to avoid the confusion, i.e. /Supplier/Home.

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);

Categories

Resources