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());
}
Related
I'd like to achieve the following (with ASP.NET MVC 3):
A controller called "apps" with the following actions:
/apps/my
/apps/agency
/apps/new
Within the last action I really want some sub-actions, e.g.:
/apps/new/product
/apps/new/tariff
I could write the New() action to take some kind of parameter to say which view I should render (i.e. product or tariff) but that feels a bit dirty.
What I really want is separate action methods for product and tariff.
What's the best way to go about this?
I think I could use Areas but this seems overkill for what I want - is the solution just to write a custom route?
Many thanks!
Sam
You could use Areas but for this small amount I agree that it is probably overkill. I'd say making the New action take in a parameter is fine for what you need. It may get more complicated if you want to pass more information in but its still do able. If you want to keep the code clean(er) you can have the action do all the complicated bits in separate private methods.
However, doing it with a custom route way (and with a separate controller as well) ...
Global.asax.cs
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute("newControllerRoute",
"apps/new/{action}",
new {controller = "NewApps"});
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
The add a controller (NewAppsController).
public class NewAppsController : Controller
{
public ActionResult Product()
{
/* used as example */
return Content("NewApps controller - Product");
}
public ActionResult Tariff()
{
/* used as example */
return Content("NewApps controller - Tariff");
}
}
Hope this helps.
If you have two distinct things that you want to be able to add then that is two distinct actions on your controller. It sounds like you are just wanting to control your URL schema so I think some sort custom routing is the answer.
Why not have separate controllers for tariffs and products? That will lead to a natural URL schema.
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.
The concept of routes is nothing new, and it works great for the concept of {area}/{controller}/{action}/{parameter}, but few sites are standalone UI interaction.
Websites often need parts of themselves that aren't really dedicated to taking data, but presenting it. For instance one of the sites I am working on has a large part of itself dedicated to user interaction (which the MVC system solves expertly. A Membership area, a place to manage information, a way to purchase items, etc.) - but it also needs a part that functions more like an old-fashioned website, where you're simply looking at pages like a folder structure.
one solution I have found is to try a custom view engine. This worked, but I quick found myself lost in a convoluted routing scheme. Another I guess I could go with is to just have an IgnoreRoute and put files in the ignored folder like normal html/aspx, but I'd really rather have the option of using Controllers so that there is a chance I can have data returned from a database, etc in the future.
So let me show you my current scenario...
Areas
Membership
Rules
Controllers
HomeController
FileView(string folder, string file)
Views
Home
General
Customize
Content
yyy.cshtml
xxx.cshtml
#Html.Partial("Content/yyy.cshtml")
xxx.cshtml
xxx.cshtml
etc. The Rules area is basically setup to function like a normal /folder/file/ structure. So here is my Controller for it..
public class HomeController : Controller
{
//
// GET: /Information/Home/
public ActionResult Index()
{
return View();
}
// **************************************
// URL: /Rules/{controller}/{folder}/{file}
// **************************************
public ViewResult FileView(string folder, string filename)
{
return View(String.Format("{0}/{1}", folder, filename));
}
}
Now, if I have a category, I simply have a lightweight controller that inherits from that Area's HomeController, like this...
public class GeneralController : Rules.Controllers.HomeController
{
// **************************************
// URL: /Rules/General/Customize/{id}
// **************************************
public ViewResult Customize(string id)
{
return FileView("Customize", id);
}
}
So then, for each folder in the 'sub' controller, I have a single Route that takes in the name of the file.
This works, but I feel it's excessively clunky. Can anyone suggest a better alternative? There are just too many pages, and too much nesting, to have a full ActionResult for each one. I also want to maintain clean urls.
Perhaps you can use a catch-all route for the Membership area, route it to a controller (MembershipController?) and have that controller just render the view that is catched by the route, like this:
public class MembershipController : Controller
{
public ActionResult Index(string pageTitle)
{
return View(pageTitle);
}
}
And the route:
routes.MapRoute(
"Membership",
"Membership/{*pageTitle}",
new {controller = "Membership", action = "Index", pageTitle = "NotFound"});
Of course, in the controller you should check whether the view exists or not, but this one should get you moving. Although I don't see why you want to have MVC in front of this when you just want to display (static?) content.
I've seen a great answer to a similar question which explains, by inheriting all controllers from a new base class decorated with your own ActionFilter attribute, how you could apply some logic to all requests to your site.
I'd like to find a way to do that based on the area of a site my user is visiting.
For example, I will have a Product controller with a View action but I want to allow that to be used for the two following urls:
/Product/View/321 - display product id 321 to 'normal' users
/Admin/Product/View/321 - use the same View controller but spit out extra functionality for my admin users.
I could pass "admin" in as a parameter named "user" into my view action on my product controller to show extra information for administrators, a method for doing that is shown here. But what I'd then need to do is confirm my user was allowed to view that url. I don't want to decorate my Product controller with an ActionAttribute that checks for authentication because when unauthenticated users (and logged in administrators) view it at /Product/View/321, I want them all to see the standard view.
So what I'd like to do, is described below in pseudo-code:
When a url in the format "{userlevel}/{controller}/{action}/{id}" is called, I'd like like to call another controller that does the authentication check and then 'chain' to the original {controller} and pass through the {action}, {id} and {userlevel} properties.
How would I do that?
(I know that the over-head for doing a check on every call to the controller is probably minimal. I want to do it this way because I might later need to do some more expensive things in addition to user authentication checks and I'd prefer to only ever run that code for the low-traffic admin areas of my site. There seems no point to do these for every public user of the site)
At first I thought this might be as simple as adding a new route like this:
routes.MapRoute(
"Admin",
"Admin/{*pathInfo}",
new { controller="Admin", action="Index", pathInfo="" }
);
and then have a controller something like this:
public class AdminController : Controller
{
public ActionResult Index(string pathInfo)
{
//Do admin checks, etc here....
return Redirect("/" + pathInfo);
}
}
However, unfortunately all the options you have available in order to do the redirect (i.e. Redirect, RedirectToAction & RedirectToRoute) all do a 302 style redirect. Basically this means that your /Admin/Product/Whatever will execute & then bounce back to the browser telling it to redirect to /Product/Whatever in a totally new request, which means you've lost your context. I don't know of a clean way of keeping the redirect server side (i.e. like a Server.Transfer of old), apparently neither does the SO community...
(obviously, this is a non-solution, since it doesn't solve your problem, but I thought I'd put it here anyway, in case you could use the ideas in some other way)
So, what's an actual solution to the problem then? Another idea is to use an ActionFilter (yes I know you said you didn't want to do so, but I think the following will serve your purposes). Add a new route like this:
routes.MapRoute(
"Admin",
"Admin/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "", userLevel = "Admin" }
);
and then add an ActionFilter like this (that you could apply to all requests via a base controller object as you mentioned):
public class ExtendedAdminViewAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
object userLevel = filterContext.RouteData.Values["userLevel"];
if (userLevel != null && userLevel.ToString() == "Admin")
{
//Do your security auth checks to ensure they really are an admin
//Then do your extra admin logic...
}
}
}
So although it is using an ActionFilter that will apply to all requests, the only extra work done in most normal cases (i.e. a request for /Product/Whatever), is a single check of that bit of route data (userLevel). In other words, you should really see a performance hit for normal users since you're only doing the full auth check and extra admin work if they requested via /Admin/Product/Whatever.
1) Can't you just check for the role within the view?
<% if (HttpContext.Current.User.IsInRole ("Administrator")) { %>
// insert some admin specific stuff here
<%= model.ExtraStuff %>
% } %>
You can perform the same check in the controller if you need to set admin specific view model properties. In your controller you can do your extra processing only when the user is already authenticated:
public ActionResult Details (int productId)
{
ProductViewModel model = new ProductViewModel ();
if (User.Identity.IsAuthenticated && User.IsInRole ("Administrator"))
{
// do extra admin processing
model.ExtraStuff = "stuff";
}
// now fill in the non-admin specific details
model.ProductName = "gizmo";
return View (model);
}
The only thing missing here is a redirect to your login page when an admin tries to access the view without being authenticated.
2) Alternatively if you want to reuse your default product view with some extra bits you could try the following:
public class AdminController
{
[Authorize(Roles = Roles.Admin)]
public ActionResult Details(int productId)
{
ProductController productController = new ProductController(/*dependencies*/);
ProductViewModel model = new ProductViewModel();
// set admin specific bits in the model here
model.ExtraStuff = "stuff";
model.IsAdmin = true;
return productController.Details(productId, model);
}
}
public class ProductController
{
public ActionResult Details(int productId, ProductViewModel model)
{
if (model == null)
{
model = new ProductViewModel();
}
// set product bits in the model
return Details(model);
}
}
NOTE: I would prefer solution 1) over 2) due to the fact that you need to create a new instance of ProductController and that brings up it's own set of issues especially when using IoC.
You can solve this fairly easily by creating a base controller class which checks the user level in OnActionExecuting and, if authorized, sets a Role property to the same value and adds a "Role" entry to ViewData for use in the view. You can use this as a base class for all of your controllers and they will all have access to the Role property and all your views will have a "Role" entry added to ViewData:
public abstract class BaseController : Controller
{
public string Role { get; protected set; }
protected override void OnActionExecuting( ActionExecutingContext filterContext )
{
base.OnActionExecuting( filterContext );
Role = string.Empty;
string role = string.Empty;
object value;
if ( filterContext.RouteData.Values.TryGetValue( "role", out value ) )
role = value as string ?? string.Empty;
if ( filterContext.HttpContext.User.IsInRole( role ) )
Role = role.ToLowerInvariant();
ViewData[ "role" ] = Role;
}
}
Change the default route in Global.asax.cs:
routes.MapRoute(
"Default",
"{role}/{controller}/{action}/{id}",
new { role = "", controller = "Home", action = "Index", id = "" }
);
Now, in your controller actions, check the Role property for e.g. "admin" and, if so, add any necessary view data for the admin functions.
Render your admin UI using partials and in your view, check the role and call RenderPartial:
<% if ( Equals( ViewData[ "role" ], "admin" ) )
Html.RenderPartial( "_AdminFunctions" ); %>
<p>
This is the standard, non Admin interface...
</p>
This is an "outside the box" answer:
What about leveraging the policy injection block in entLib? With that you could create a policy that would run a "pre-method" on your action. Your pre-method could perhaps handle your problem.
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.