How malleable are the conventions in ASP.NET MVC? - asp.net-mvc

Specifically, does a controller class name have to have the Controller suffix, and can you change the folder structure in your project if you want to, without breaking things?
Are there other conventions that can be overridden, and how?

Most of the conventions are malleable provided you know how the framework operates. Let's tackle two of the biggest conventions:
the "{controller}/{action}/" magic keywords for instantiating controllers from a route
the way the framework searches for Views first in the controller directory, and then in the Shared directory.
Every route that you create is associated with an instance of an MvcRouteHandler object by default. When the route is matched, that handler is invoked to deal with the incoming request. Here's what the MvcHandler's ProcessRequest looks like:
protected internal virtual void ProcessRequest(HttpContextBase httpContext)
{
this.AddVersionHeader(httpContext);
string requiredString = this.RequestContext.RouteData.GetRequiredString("controller");
IControllerFactory controllerFactory = this.ControllerBuilder.GetControllerFactory();
IController controller = controllerFactory.CreateController(this.RequestContext, requiredString);
if (controller == null)
{
throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture, MvcResources.ControllerBuilder_FactoryReturnedNull, new object[] { controllerFactory.GetType(), requiredString }));
}
try
{
controller.Execute(this.RequestContext);
}
finally
{
controllerFactory.ReleaseController(controller);
}
}
Notice the hardcoded string "controller". Well, you can replace this handler for any route you'd like if you want to code your own controller-finding logic. Simply do something like this (shameless blog plug):
routes.Add("ImagesRoute",
new Route("graphics/{filename}", new ImageRouteHandler()));
Now when the route is matched, it invokes your own logic, and you can do whatever you please. Incidentally, the reflection that is used to find the XXXXController class with the "Controller" suffix is part of the DefaultControllerFactory object, invoked in the handler above, and this factory is replaceable.
So, controller picking is one convention that's overridable. What about when it looks for Views when you do a "return View()" from any controller method? Well here's the constructor for the WebFormViewEngine, the default view engine of the framework:
public WebFormViewEngine()
{
base.MasterLocationFormats = new string[] { "~/Views/{1}/{0}.master", "~/Views/Shared/{0}.master" };
base.ViewLocationFormats = new string[] { "~/Views/{1}/{0}.aspx", "~/Views/{1}/{0}.ascx", "~/Views/Shared/{0}.aspx", "~/Views/Shared/{0}.ascx" };
base.PartialViewLocationFormats = base.ViewLocationFormats;
}
So if you didn't like the convention of looking in the controller directory, and then shared - you could easily extend WebFormViewEngine (or use an entirely different view engine) and plop it in your global.asax:
ViewEngines.Engines.Add(new MyViewEngine());
One of the amazing things about the MVC framework is how flexible it really is. You can replace almost any part of it with your own logic - and all the code is available to see what they've done.

Related

How to inject different concrete implementation into controller depending on current request action method attributes?

I currently have a custom PrototypingControllerFactory that looks for a custom [Prototype] attribute on the action method being invoked for the current request, and depending on whether the attribute is present or not will inject a different implementation of an interface ISomeService. (In this case ISomeService more or less abstracts a messaging service, so the mock implementation allows returning "canned" results when the real implementation is not yet ready to handle a particular message).
So for example, if I have a controller class like so:
public class MyController : Controller
{
private readonly ISomeService _someService;
public MyController(ISomeService someService /*, .... other dependencies */
{ _someService = someService; //... etc }
public ActionResult Action1()
{
//...
_someService.SomeMethod();
//...
}
[Prototype]
public ActionResult Action2()
{
//...
_someService.SomeMethod();
//...
}
}
Then when Action1 is invoked from an http request, _someService should use the production implementation of ISomeService, but when Action2 is invoked, _someService should have a Mocked version of ISomeService.
From a strict design standpoint, I realize this might point to having too many actions in a particular controller (otherwise, for example, I could just mark an entire controller as having [Prototype]) , but due to project inertia, I would rather not try to force a change in how actions are placed in controllers.
Currently the autofac registration has the following:
builder.RegisterControllers(Assembly.GetExecutingAssembly());
if (ConfigurationManager.AppSettings["AllowPrototyping"] == "true")
{
builder.RegisterType<PrototypingControllerFactory>().As<IControllerFactory>().InstancePerRequest();
}
However, this means that the controller factory has to do some fancy work to figure out constructor arguments, and get instances from the DI container. Recently, I have discovered some subtle differences in behavior between the custom controller factory, and the "real" controller factory that are not desirable.
I would like to eliminate the custom controller factory, and instead have autofac fully handle resolution of the controllers.
How can I tell autofac to resolve a different implementation of an interface depending on whether the currently executing action is decorated with my custom [Prototype] attribute?
The first option depends on reliably being able to determine the controller action method directly from the route data, which is not necessarily straightforward. Part of the issue is that creation of the controller (and hence injection of the dependencies) occurs fairly early in the process, even before authorization filters run.
If some reliable implementation of a magical method say ActionDescriptor GetActionDescriptor(RoutData routeData) actually existed, then I could do something like the following:
builder.Register<ISomeService>(c =>
{
var httpRequest = c.Resolve<HttpRequestBase>();
var actionDescriptor = GetActionDescriptor(httpRequest.RequestContext.RouteData);
if (actionDescriptor.GetAttributes<PrototypeAttribute>().Any())
{
return new PrototypeSomeService();
}
return new RealSomeService();
}).InstancePerRequest();
However the closest I could come to getting an ActionDescriptor at the point where the controller is intantiated was overriding DefaultControllerFactory, which is precisely what I am trying to get away from.
From default controller factory you can use the protected DefaultControllerFactory.GetControllerType() from which you could then create a ReflectedControllerDescriptor. But then you still have to do some munging to get the correct action descriptor from controllerDescriptor.GetCanonicalActions(). (In fact, I suspect this "munging" is leading to the subtle differences in the original custom controller factory). This could get even more complicated when routes are used from other http handlers (think Elmah or MiniProfiler for example).
In the end I opted to avoid attempting to map RouteData to an ActionDescriptor by making [PrototypeAttribute] inherit from ActionFilterAttribute so that I could easily hook into OnActionExecuting, from which point I added an identifier to the current HttpContext, e.g.
public class PrototypeAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.RequestContext.HttpContext.Items.Add("Prototype", "Prototype");
base.OnActionExecuting(filterContext);
}
}
Then I modified my PrototypeSomeService so that it wrapped an implementation of ISomeService and delegated to it if the context did not contain the prototype key, e.g.
public class PrototypeSomeService : ISomeService
{
private readonly ISomeService _wrappedService;
private readonly HttpRequestBase _httpRequest;
public PrototypeSomeService(ISomeService wrappedService, HttpRequestBase httpRequest)
{
_wrappedService = wrappedService;
_httpRequest = httpRequest
}
public object SomeMethod()
{
if(_httpRequest.RequestContext.HttpContext.Items.Contains("Prototype"))
return _wrappedService.SomeMethod();
//other prototype logic...
return prototypeResult;
}
}
The final piece to tie it all together is to use autofac's decorator capabilities:
var someServiceRegistration = builder.RegisterType<SomeService>().InstancePerRequest();
if (ConfigurationManager.AppSettings["AllowPrototyping"] == "true")
{
someServiceRegistration.Named<ISomeService>("Prototype");
builder.RegisterDecorator<ISomeService>(
(c, inner) => new PrototypeSomeService(inner, c.Resolve<HttpRequestBase>()),
fromKey: "Prototype"
);
}
else
{
someServiceRegistration.As<ISomeService>();
}
One small downside is that you have to make sure you use a unique key for the httpcontext item or else strange things will happen, but that is pretty easily avoided by e.g. using a guid.
This approach allowed me to leave most of the existing code unchanged, only modifying the prototype implementation of the service and the autofac registrations.
You can read more about autofac decorators at:
http://nblumhardt.com/2011/01/decorator-support-in-autofac-2-4/
http://docs.autofac.org/en/latest/advanced/adapters-decorators.html?highlight=decorator#decorators

Tell Razor to look for partials in a particular folder, in addition to the default ones

In Razor, I have a few that's located in "~/areas/shared/views/shared/_partialName.cshtml". If I do simply #{ Html.RenderPartial("_partialName", myModel"); } in my view, it cannot find it. I instead have to fully qualify the path to that partial.
Can I tell my project to include the folder "~/areas/shared/views/shared/" in the list of folders it crawls when trying to find partials? This way I can keep the snippet of code in my view to render that partial nice and short. Thanks!
The RazorViewEngine class contains properties which have patterns for where it should look for various view files - areas, partials, everything. You can override it pretty easily and tell your application in Globals.asax to use your implementation instead:
public class MyViewEngine : RazorViewEngine
{
public MyViewEngine()
{
// Use the standard partials location in addition to this location
// {0} is the partial view name, {1} is the controller name
String[] morePartialLocations = new String[] {
"~/areas/shared/views/shared/{0}.cshtml"
};
PartialViewLocationFormats =
PartialViewLocationFormats.Concat(morePartialLocations).ToArray();
}
}
// in Globals.asax
protected void Application_Start()
{
...
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new MyViewEngine());
...
}
Note that since it sounds like you're working with Areas, there's another property that you can override called AreaViewLocationFormats which you can update in the same manner and insert "{2}" as the name of the area.

Multilingual URLs with ASP.NET MVC

I’m working out the concepts for a new project where I need to support for multilingual URL’s. Ideally all URL’s need to be in the native language of the user. So we don’t want to use domain.com/en/contact and domain.com/es/contact but we like domain.com/contact and domain.com/contactar (contactar is Spanish for contact). Internally both should be routed to the same ContactController class.
This could be handled by adding multiple static routes to Global.asax.cs for each language but we’d like to make this very dynamic and would like the user of the system to be able to change the translation of the URL’s through the content management system. So we need some kind of dynamic mapping from URL’s to controllers and actions.
By looking at the source code of MVC3 I figured out that the ProcessRequestInit method of MvcHandler is responsible for determining which controller to create. It simply looks in the RouteData to get the name of the controller. One way to override the default MVC routing would be to create a simple default route that uses a custom RouteHandler. This RouteHandler forces MVC to use my own custom subclassed version of MvcHandler that overrides the ProcessRequestInit method. This overridden method insert my own dynamically found controller and action into the RouteData before calling back to the original ProcessRequestInit.
I’ve tried this:
Global.asax.cs
routes.Add(
new Route("{*url}", new MultilingualRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Default", action = "Default" })
}
);
MultilingualRouteHandler.cs
public class MultilingualRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new MultilingualMVCHandler(requestContext);
}
}
MultilingualMvcHandler.cs
public class MultilingualMVCHandler : MvcHandler
{
public MultilingualMVCHandler(RequestContext context) : base(context)
{
}
protected override void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory)
{
if (RequestContext.RouteData.Values.ContainsKey("controller"))
{
RequestContext.RouteData.Values.Remove("controller");
}
if (RequestContext.RouteData.Values.ContainsKey("action"))
{
RequestContext.RouteData.Values.Remove("action");
}
RequestContext.RouteData.Values.Add("controller", "Product");
RequestContext.RouteData.Values.Add("action", "Index");
base.ProcessRequestInit(httpContext, out controller, out factory);
}
}
In this handler I hardcoded the controller and action for testing purposes to some fixed values but it’s not difficult to make this dynamic. It works but the only problem is that I had to modify the source code of ASP.NET MVC3 to get it working. The problem is that the ProcessRequestInit method of MvcHandler is private and thus cannot be overridden. I’ve modified the source code and changed it to protected virtual which allows me to override it.
This is all great but possibly not the best solution. It’s cumbersome that I would always need to distribute my own version of System.Web.Mvc.dll. It would be much better that it would work with the RTM version.
Am I missing any other possibilities of hooking into ASP.NET MVC that would allow me to dynamically determine the controller and action to launch, depending on the URL? One other way I thought of is to build the RouteCollection dynamically on *Application_Start* but I think that will make it more difficult to change it on the fly.
I would appreciate any tips of hooks that I’ve not yet found.
This is fairly old now, nut just in case anyone else is looking for something similar...
Unless I'm completely misunderstanding what you want to do, it's pretty simple really.
Step 1: Add a new route to global.ascx.cs containing a reference to your personal routing engine
routes.Add(new MyProject.Routing.ContentRoutingEngine());
Make sure that it is in the right place in the list of routes so that other routing engines can catch stuff before it if required, or continue the route search if your engine doesn't handle a particular route. I put it after the ignores, but before the MVC default routes.
Step 2: Create the Content Routing Engine, making sure that it inherites from System.Web.Routing.RouteBase abstract class, and overrides the GetRouteData and GetVirtualPath methods as required e.g.
public class ContentRoutingEngine : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var routeHandler = new MvcRouteHandler();
var currentRoute = new Route("{controller}/{action}", routeHandler);
var routeData = new RouteData(currentRoute, routeHandler);
// set your values dynamically here
routeData.Values["controller"] = "Home" ;
// or
routeData.Values.Add("action", "Index");
// return the route, or null to have it passed to the next routing engine in the list
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//implement this to return url's for routes, or null to just pass it on
return null;
}
}
and that should do it. You can change routes as dynamically as you wish within your engine, and no changes to MVC source required. Let the standard MVC RouteHandler actually invoke the controller.
Postscript: Obviously the code above is not production standard - it's written to make it as obvious as possible what's going on.
If you are allowing modification of urls through your CMS, then you will have to keep all old versions of the urls so that you can 301 redirect to the new ones.
The best bet for this will be to put the url tokens eg "contactar" in the db along with its corresponding controller.
query that, and create your routes out of that.
create a route that will handle the 301s
I think that most elegant solution would be using some action filter combined with custom ActionInvoker. That way, you could invoke an action that has specific filters applied. Something like ActionName attribute, only capable to accept multiple values (names).
Edit: Take a look at ActionMethodSelectorAttribute, meybe you don't need a custom ActionInvoker after all.

asp.net mvc - dynamic controller based on authenticated user

If I want the default url of my web app to display completely different UIs depending on the user, what is the best way to accomplish this? I don't really want to use the same controller for every type of user. To put it another way, if a user is logged in and goes to http://mysweetapp.com and is an admin user, they should get what they would see the same thing as if they had gone to http://mysweetapp.com/admin. If the user is logged in as a normal user, they should see the same thing as if they had gone to http://mysweetapp.com/normaluser
Should I just make a "redirect" controller as my default and have it send the client to the appropriate controller?
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Redirect", action = "Index", id = "0" });
I've also tried creating my own ControllerFactory, but I don't think I was clear on the concept and couldn't get it to work.
Thanks
The cleanest way in my opinion would be to create a custom route handler to be used by your default route. Then you can separate out which controller to be used if the controller name is your default controller name, in the example below, it is: Home. Then check if the user is an administrator or not and process the request with the controller you would like to use.
Here is the code:
public class CustomHttpHandler : IHttpHandler
{
public RequestContext RequestContext { get; private set; }
public CustomHttpHandler(RequestContext requestContext)
{
try
{
string controllerName = RequestContext.RouteData.GetRequiredString("controller");
if (controllerName.Equals("home", StringComparison.CurrentCultureIgnoreCase))
{
bool isAdmin = RequestContext.HttpContext.User.IsInRole("Admin");
controllerName = isAdmin ? "admin" : "normaluser";
}
IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
IController controller = factory.CreateController(RequestContext, controllerName);
if (controller != null)
{
controller.Execute(RequestContext);
}
}
finally
{
factory.ReleaseController(controller);
}
}
}
public class CustomRouteHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new CustomHttpHandler(requestContext);
}
}
// Now use the CustomRouteHandler when you map your default route.
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
).RouteHandler = new CustomRouteHandler();
Hope this helps.
For simplicity, in your HomeController Index method (or whatever default controller you are using) you could put some code like this and then the links from the AdminIndex view or the Index view can send the users to appropriate areas when they start navigating round your site - that way you have one shared controller and the other controllers can be specific to the user type.
return user.IsAdministrator ? View("AdminIndex") : View("Index");
the user.IsAdministrator call is pseudocode of course - replace this with whatever method you are using to work out if the user is an admin user
If you don't want to use the same controller set up individual controllers and views for each item first - mysweetapp.com/admin and mysweetapp.com/normaluser.
You can then redirect specific users to this page through a default controller based on their logged in role.
if (User.IsInRole("Admin")
{
return RedirectToAction("Index", "admin");
}
else if (User.IsInRole("Standard")
{
return RedirectToAction("Index", "normaluser");
}
What you might want to consider is areas. This would allow you to have separate controllers for each area. Then permit access to those areas based on roles or whatever you wish.
This will give you routes like '/admin/controller/action', '/users/controller/action', etc. The 'pattern' separates all your controllers by namespace, and handles the routing quite well. Separate master pages easily, etc.
It won't give you the (potentially confusing, IMO) '/' and '/admin/' looking the same to an admin user, but it will let you separate the content and controllers.
What you are describing would lead to potentially tons of methods for each controller, something that is generally frowned upon by the MVC/REST crowd. It's not horrible, but its not considered best practice either.
You can read about areas at this blog here. Google 'asp.net mvc areas' for more.
--------edit-----------
To expand a bit:
Without custom routes or some other shenanigans, actions are mapped to controllers by the url. So if you want to keep all admin actions and views different, but on the root url, along with normal user actions, this would lead to one big controller that has to handle all these actions, or some strange "if this role, this view; if that role, that view" sort of nonsense that would have to happen in each action. Kind of a mess to debug potentially.
Similarly, the default view engine finds the views based on the url as well.
This would mean that all of your views are going to sit in one big ugly directory full of all sorts of weird similarly named but differently behaving views.
In short, this would become a potentially horrific maintenance nightmare, depending on complexity of the application.
Could you create a class that extends from DefaultControllerFactory and overrides CreateController?
public class RedirectControllerFactory : DefaultControllerFactory
{
public override IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName)
{
if (controllerName.Equals("Redirect"))
{
controllerName = requestContext.HttpContext.User.IsInRole("Admin") ? "Admin" : "NormalUser";
}
return base.CreateController(requestContext, controllerName);
}
}
Then in your Application_Start():
protected void Application_Start()
{
// ...
ControllerBuilder.Current.SetControllerFactory(new RedirectControllerFactory());
}

Categories of controllers in MVC Routing? (Duplicate Controller names in separate Namespaces)

I'm looking for some examples or samples of routing for the following sort of scenario:
The general example of doing things is: {controller}/{action}/{id}
So in the scenario of doing a product search for a store you'd have:
public class ProductsController: Controller
{
public ActionResult Search(string id) // id being the search string
{ ... }
}
Say you had a few stores to do this and you wanted that consistently, is there any way to then have: {category}/{controller}/{action}/{id}
So that you could have a particular search for a particular store, but use a different search method for a different store?
(If you required the store name to be a higher priority than the function itself in the url)
Or would it come down to:
public class ProductsController: Controller
{
public ActionResult Search(int category, string id) // id being the search string
{
if(category == 1) return Category1Search();
if(category == 2) return Category2Search();
...
}
}
It may not be a great example, but basically the idea is to use the same controller name and therefore have a simple URL across a few different scenarios, or are you kind of stuck with requiring unique controller names, and no way to put them in slightly different namespaces/directories?
Edit to add:
The other reason I want this is because I might want a url that has the categories, and that certain controllers will only work under certain categories.
IE:
/this/search/items/search+term <-- works
/that/search/items/search+term <-- won't work - because the search controller isn't allowed.
I actually found it not even by searching, but by scanning through the ASP .NET forums in this question.
Using this you can have the controllers of the same name under any part of the namespace, so long as you qualify which routes belong to which namespaces (you can have multiple namespaces per routes if you need be!)
But from here, you can put in a directory under your controller, so if your controller was "MyWebShop.Controllers", you'd put a directory of "Shop1" and the namespace would be "MyWebShop.Controllers.Shop1"
Then this works:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
var shop1namespace = new RouteValueDictionary();
shop1namespace.Add("namespaces", new HashSet<string>(new string[]
{
"MyWebShop.Controllers.Shop1"
}));
routes.Add("Shop1", new Route("Shop1/{controller}/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new
{
action = "Index",
id = (string)null
}),
DataTokens = shop1namespace
});
var shop2namespace = new RouteValueDictionary();
shop2namespace.Add("namespaces", new HashSet<string>(new string[]
{
"MyWebShop.Controllers.Shop2"
}));
routes.Add("Shop2", new Route("Shop2/{controller}/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new
{
action = "Index",
id = (string)null
}),
DataTokens = shop2namespace
});
var defaultnamespace = new RouteValueDictionary();
defaultnamespace.Add("namespaces", new HashSet<string>(new string[]
{
"MyWebShop.Controllers"
}));
routes.Add("Default", new Route("{controller}/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
DataTokens = defaultnamespace
});
}
The only other thing is that it will reference a view still in the base directory, so if you put the view into directories to match, you will have to put the view name in when you return it inside the controller.
The best way to do this without any compromises would be to implement your own ControllerFactory by inheriting off of IControllerFactory. The CreateController method that you will implement handles creating the controller instance to handle the request by the RouteHandler and the ControllerActionInvoker. The convention is to use the name of the controller, when creating it, therefore you will need to override this functionality. This will be where you put your custom logic for creating the controller based on the route since you will have multiple controllers with the same name, but in different folders. Then you will need to register your custom controller factory in the application startup, just like your routes.
Another area you will need to take into consideration is finding your views when creating the controller. If you plan on using the same view for all of them, then you shouldn't have to do anything different than the convention being used. If you plan on organizing your views also, then you will need to create your own ViewLocator also and assign it to the controller when creating it in your controller factory.
To get an idea of code, there are a few questions I have answered on SO that relate to this question, but this one is different to some degree, because the controller names will be the same. I included links for reference.
Views in separate assemblies in ASP.NET MVC
asp.net mvc - subfolders
Another route, but may require some compromises will be to use the new AcceptVerbs attribute. Check this question out for more details. I haven't played with this new functionality yet, but it could be another route.

Resources