Using ASP.NET MVC 5.2 with attribute routing. I have 2 actions in separate controllers:
public class AccountController
{
[Route("login")]
public ActionResult Login()
{
}
}
public class HomeController
{
[Route("{category}")]
public ActionResult Category(string category)
{
}
}
When I run it, I get an error Multiple controller types were found that match the URL. This can happen if attribute routes on multiple controllers match the requested URL.
category dynamic parameter in Category action is something that is checked against DB in the action and there are a lot of possible values so I don't think I can use route constraints to include all possible options, it's also a string, so something like category:int is not an option.
I think I also cannot use Order in routing since those actions are in different controllers.
I know I can change the route for Category action to be something like [Route("category/{category}")] but I'd prefer to not do that. What's the best way for me to go about it so both /login and /{category} work?
Say I have a controller FooController which has a bunch of action methods. Some of these methods would I like to override, but none of the methods are marked as virtual and I do not want to change the code of FooController.
So I implement a CustomFooController (not inheriting from FooController) and write new versions of the methods that I want.
Now I want to route first to CustomFooController and if the action is not available there I want to default to FooController. I have set up this route config to override the route:
routes.MapRoute(
"customFoo",
"foo/{action}",
new { controller = "CustomFoo", action = UrlParameter.Optional }
);
Here is some example definitions:
public class FooController : Controller
{
public ActionResult Bar { ... }
public ActionResult Baz { ... }
}
public class CustomFooController : Controller
{
public ActionResult Bar { ... }
}
So when accessing /Foo/Bar we should hit CustomFooController.Bar() and when accessing /Foo/Baz we should hit FooController.Baz() since Baz() is not implemented in CustomFooController.
But I get "The resource cannot be found", I understand why, but can I somehow work around it without modifying FooController?
You might want to have a look at HandleUnknownAction() (see msdn reference page).
It is invoked when a request matches a controller, but no method with the specified action name is found in that controller.
So you could override that method in your CustomFooController to redirect to the appropriate action in FooController (or even other controllers based on your custom inspection of request related data).
Redirection can be done with RedirectToAction() (see msdn reference page)
I think I found a neat solution myself. Since I don't want to modify FooController and a bunch of other controllers from another application I'm extending (and relying on updates from) I decided to use extension method MapMvcAttributeRoutes() from System.Web.Mvc.RouteCollectionAttributeRoutingExtensions in my startup RouteConfig in order to first match with route attributes on e.g. my CustomFooController which takes precedence over the conventional routing.
Just to be on the safe side: HandleUnknownAction() is implemented only in CustomFooController. You don't have to modify FooController (or other controllers).
While reading about mcv3 I came across an attribute name called [ActionName]. It actually gives a new name to the action method. I tested a scenario which made me think; how are the internals working. When I have the following two action methods in my controller class
[ActionName("Test")]
public ActionResult Index()
{
return View();
}
[ActionName("Index")]
public ActionResult Test()
{
return View();
}
I thought this will end up in some kind of infinite loop or will give some ambiguity exception. But the same works fine and the second method is called when i give this url http://mysite:1234/mycontroller
What made MVC engine to choose the second method and not the first?
Any idea why this happens?
Phil Haack has a post on this matter: How a method becomes an action
In short: the ControllerActionInvoker uses reflection to find a method matches the action name.
The ActionNameAttribute redefines the name of the method.
Also be aware that the name of your View matches the ActionName, not the MethodName: the method Index will search for a view with the name "Test"
This is the magic of the Routing engine. Somewhere within the global.asax.cs file there would be routing patterns defined, mostly which defaults to
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
This is a routing pattern defined for your application. The action name attribute maps to the 'action' parameter within the parameter collection (3rd parameter for MapRoute).
In your case if you map the action 'Index' to method 'Test'. It should call Test() method. I am not sure whether it is still calling Index() for you. In fact the routing engine does not care about the method name if it finds the ActionName attribute over your public method.
ActionNameAttribute it represents an attribute that is used for the name of an action. If it is not present the name of the method is used.
The default route in MVC {controller}/{action}/{id} is for the most part quite helpful as is being able to set a default if the incoming url doesn't include a parameter but is there also a way to specify a default action for when an action doesn't exist on a controller?
What I want to achieve is being able to have controllers with several specific actions and then its own catchall which uses the url to grab content from a basic CMS.
For example a products controller would be something like:
public class ProductsController: Controller{
public ActionResult ProductInfo(int id){...}
public ActionResult AddProduct(){...}
public ActionResult ContentFromCms(string url){...}
}
Where the default route would handle /Products/ProductInfo/54 etc but a request url of /Products/Suppliers/Acme would return ContentFromCms("Suppliers/Acme"); (sending the url as a parameter would be nicer but not needed and a parameterless method where I get it from Request would be fine).
Currently I can think of two possible ways to achieve this, either:
Create a new constraint which reflects over a controller to see if it does have an action of a given name and use this in the {controller}/{action}/{id} route thus allowing me to have a more general catchall like {controller}/{*url}.
Override HandleUnknownAction on the controller.
The first approach seems like it would be quite a roundabout way of checking this while for the second I don't know the internals of MVC and Routing well enough to know how to proceed.
Update
There's not been any replies but I thought I'd leave my solution incase anyone finds this in future or for people to suggest improvements/better ways
For the controllers I that wanted to have their own catchall I gave them an interface
interface IHasDefaultController
{
public string DefaultRouteName { get; }
System.Web.Mvc.ActionResult DefaultAction();
}
I then derived from the ControllerActionInvoker and overrode FindAction. This calls the base FindAction then, if the base returns null and the controller impliments the interface I call FindAction again with the default actionname.
protected override ActionDescriptor FindAction(ControllerContext controllerContext, ControllerDescriptor controllerDescriptor, string actionName)
{
ActionDescriptor foundAction = base.FindAction(controllerContext, controllerDescriptor, actionName);
if (foundAction == null && controllerDescriptor.ControllerType.GetInterface("Kingsweb.Controllers.IWikiController") != null)
{
foundAction = base.FindAction(controllerContext, controllerDescriptor, "WikiPage");
}
return foundAction;
}
As I also want parameters from the routing I also replace the RouteData at the start of the default Actionresult on the controller
ControllerContext.RouteData = Url.RouteCollection[DefaultRouteName].GetRouteData(HttpContext);
You approach is quite fine. As a side-note:
replace
controllerDescriptor.ControllerType.GetInterface("Kingsweb.Controllers.IWikiController") != null
with
typeof(Kingsweb.Controllers.IWikiController).IsAssignableFrom(controllerDescriptor.ControllerType)
this is more strongly-typed way then passing in the name of the interface via string: what if you change the namespace tomorrow?..
Could someone explain how routes are associated with controllers in MVC 2? Currently, I have a controller in /Controllers/HomeController.cs and a view /Home/Index.aspx.
My route registration method looks like this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("{resource}.aspx/{*pathInfo}");
routes.MapRoute(
"Default",
// Route name
"{controller}/{action}/{id}",
// URL with parameters
new { controller = "Home", action = "Index", id = "" }
// Parameter defaults
);
}
If I request the URL: http://localhost/Home/Index, then the request is correctly handled by HomeController.Index().
However, for the life of me, I can't figure out how the url /Home/Index gets pointed to HomeController. The view aspx doesn't, as far as I can tell, reference HomeController, HomeController doesn't reference the view, and the route table doesn't explicitly mention HomeController. How is this magically happening? Surely I'm missing something obvious.
then
This is the convention in ASP.NET MVC.
When using the DefaultControllerFactory this convention is buried inside the internal sealed class System.Web.Mvc.ControllerTypeCache (typical for Microsoft to write internal sealed classes). Inside you will find a method called EnsureInitialized which looks like this:
public void EnsureInitialized(IBuildManager buildManager)
{
if (this._cache == null)
{
lock (this._lockObj)
{
if (this._cache == null)
{
this._cache = GetAllControllerTypes(buildManager).GroupBy<Type, string>(delegate (Type t) {
return t.Name.Substring(0, t.Name.Length - "Controller".Length);
}, StringComparer.OrdinalIgnoreCase).ToDictionary<IGrouping<string, Type>, string, ILookup<string, Type>>(delegate (IGrouping<string, Type> g) {
return g.Key;
}, delegate (IGrouping<string, Type> g) {
return g.ToLookup<Type, string>(t => t.Namespace ?? string.Empty, StringComparer.OrdinalIgnoreCase);
}, StringComparer.OrdinalIgnoreCase);
}
}
}
}
Pay attention how the grouping is made. So basically the DefaultControllerFactory will look inside all the referenced assemblies for types implementing the Controller base class and will strip the "Controller" from the name.
If you really want to dissect in details ASP.NET MVC's pipeline I would recommend you this excellent article.
The default views engine that comes with ASP.NET MVC works on the following conventions:
You have a folder structure like this:
- Controllers\
|- HomeController.cs
- Views\
|- Home\
|-- Index.aspx
|- Shared\
When a request comes in, and matches a route defined in the RegisterRoutes method (see things like URL routing for more on that), then the matching controller is called:
routes.MapRoute(
"Default", // Route name, allows you to call this route elsewhere
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
In the default route, you are also specifying a default controller (without the "Controller" suffix) - the routing engine will automatically add Controller onto the controller name for you - and a default action.
In your controller, you call the simple method:
public ActionResult Index(){
return View();
}
The default view engine then looks for an aspx file called Index (the same as the action) in a folder called "Home" (the same as the controller) in the "Views" folder (convention).
If it doesn't find one in there, it will also look for an index page in the Shared folder.
From the ASP.NET MVC Nerd Dinner sample chapter
ASP.NET MVC applications by default use a convention-based directory naming structure when resolving view templates. This allows developers to avoid having to fully-qualify a location path when referencing views from within a Controller class. By default ASP.NET MVC will look for the view template file within the \Views\[ControllerName]\ directory underneath the application.
The \Views\Shared subdirectory provides a way to store view templates that are re-used across multiple controllers within the application. When ASP.NET MVC attempts to resolve a view template, it will first check within the \Views\[Controller] specific directory, and if it can’t find the view template there it will look within the \Views\Shared directory.
When it comes to naming individual view templates, the recommended guidance is to have the view template share the same name as the action method that caused it to render. For example, above our "Index" action method is using the "Index" view to render the view result, and the "Details" action method is using the "Details" view to render its results. This makes it easy to quickly see which template is associated with each action.
Developers do not need to explicitly specify the view template name when the view template has the same name as the action method being invoked on the controller. We can instead just pass the model object to the View() helper method (without specifying the view name), and ASP.NET MVC will automatically infer that we want to use the \Views\[ControllerName]\[ActionName] view template on disk to render it.
Edit to add:
Some example routes I've set up, that explicitly set the controller are:
routes.MapRoute(
"PhotoDetailsSlug",
"Albums/{albumId}/{photoId}/{slug}",
new {controller = "Albums", action = "PhotoDetails"},
new {albumId = #"\d{1,4}", photoId = #"\d{1,8}"}
);
Here I'm explicitly stating that I'm using the Albums controller, and the PhotoDetails action on that, and passing in the various ids, etc to the that action.
Inside the action Index there is a statement return View(). When a blank View is returned, the DefaultViewEngine searches several default folders for the name of the Controller method(specifically inside the FindView method). One of them is the Views/Home directory because Home is the name of the controller. There it finds the Index file, and uses it to display the result.