I'm trying to set up my MVC project to have URLs so that I can go to:
/Groups/
/Groups/Register
/Groups/Whatever
But in my controller, I can also flag some actions as admin only, so that they are accessed at:
/Admin/Groups/Delete/{id}
I would like to keep one GroupController, and have actions so that:
public class GroupController : Controller
{
public ActionResult Index(){
return View();
}
[AdminAction]
public ActionResult Delete(int id){
...
return View();
}
}
Allows:
/Groups is a valid URL.
/Admin/Groups is a valid URL (but would call some other action besides Index - maybe)
/Admin/Groups/Delete/{id} is a valid URL (post only, whatever)
/Groups/Delete is an INVALID url.
I realize this is probably a pretty broad question, but I'm new to MVC and I'm not really sure where to start looking, so if you could just point me in the right direction that would be hugely appreciated.
As we discussed in the comments below, while it is possible to use my original answer below to achieve the routing solution you requested, a better solution is to use Areas, establish an Admin area, and create controllers in your Admin area to handle the administrative tasks for different objects, such as Group, User, etc. This allows you to set up restricted administrative functions more easily, and is both a better design and a better security model.
ORIGINAL ANSWER
What you want can be accomplished by using the following routes:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Admin", // Route name
"admin/{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
However, as Akos said in the comments, it is a much better design to separate the administrative functions into a different controller. While this is possible, I would recommend against using this design.
UPDATE
It is possible to use a RouteConstraint on your Default route to make it fail if Admin actions are requested. The Default route would look like this:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional, // Parameter defaults
new { action = IsNotAdminAction() } // route constraint
);
The RouteConstraint would look like this:
public class IsNotAdminAction : IRouteConstraint
{
private string adminActions = "create~delete~edit";
public IsNotAdminAction()
{ }
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// return false if there is a match
return !adminActions.Contains(values[parameterName].ToString().ToLowerInvariant());
}
}
Related
I'm working on a project in ASP.NET MVC 4 and I'm at a bit of a loss with a particular routing. I have a lot of custom routes already in the project.
I am currently making a bunch of controllers for the frontend of the site (publicly visible part) to be able to do thing like abc.com/OurSeoFeatures that gets routed to /OurSeoFeatures/Index
Is there any way to do this so that the above would route to something like /frontend/OurSeoFeature and another page would route to /frontend/anotherpage and also still have my other routes correctly? It seems to me that the above would hit the default route and if I put something like the following it would just catch all the request and would not let me hit anything else.
routes.MapRoute(
name: "ImpossibleRoute",
url: "{action}/{id}",
defaults: new { controller = "frontend", id = UrlParameter.Optional }
);
Am I just stuck with making a bunch of controllers? I really don't want to make one controller like page and put a bunch of actions there as I don't think its very pretty. Any Ideas?
In order to do what you're asking, you simply need to add a route constraint:
routes.MapRoute(
name: "Frontend",
url: "frontend/{controller}/{action}/{id}",
defaults: new { controller = "OurSeoFeature", action = "Index", id = UrlParameter.Optional },
constraints: new { controller = "OurSeoFeature|Products" }
);
This constraint means the route will only match controllers with the names OurSeoFeatureController or ProductsController. Any other controller will trigger the default route. However, this wouldn't handle redirecting those controllers to /frontend/..., if that's what you're after. Instead, that gets a little more involved.
Firstly, you'll need to create a class that implements IRouteConstraint, in order to supply the controller names you want to redirect to /frontend/.... The reason we need this now, is because we'll need to access those names in an ActionFilter, and we can't do that if we supply a regex constraint like constraints: new { controller = "OurSeoFeature|Products" above. So, the constraint could look something like this:
public class FrontendControllerConstraint : IRouteConstraint
{
public FrontendControllerConstraint()
{
this.ControllerNames = new List<string> { "OurSeoFeature", "Products" };
}
public bool Match(HttpContextBase httpContext, Route route,
string parameterName, RouteValueDictionary values,
RouteDirection routeDirection)
{
string value = values[parameterName].ToString();
return ControllerNames.Contains(value, StringComparer.OrdinalIgnoreCase);
}
public List<string> ControllerNames { get; private set; }
}
Next up, the action filter could look like this:
public class RedirectToFrontendActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controller = filterContext.RouteData.Values["controller"].ToString();
var path = filterContext.HttpContext.Request.Url.AbsolutePath;
var controllersToMatch = new FrontendControllerConstraint().ControllerNames;
if (controllersToMatch.Contains(controller, StringComparer.OrdinalIgnoreCase)
&& path.IndexOf(pathPrefix, StringComparison.OrdinalIgnoreCase) == -1)
{
filterContext.Result =
new RedirectToRouteResult(routeName, filterContext.RouteData.Values);
}
base.OnActionExecuting(filterContext);
}
private string routeName = "Frontend";
private string pathPrefix = "Frontend";
}
Now that we have those in place, all that's left is to wire it all up. Firstly, the constraint is applied in a slightly different way:
routes.MapRoute(
name: "Frontend",
url: "frontend/{controller}/{action}/{id}",
defaults: new { controller = "OurSeoFeature", action = "Index", id = UrlParameter.Optional },
constraints: new { controller = new FrontendControllerConstraint() }
);
Finally, you need to add the filter to FilterConfig.cs:
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
filters.Add(new RedirectToFrontendActionFilter());
}
One warning here is that because I'm checking against Request.Url.AbsolutePath, you cannot pass anything in the path that contains the word frontend. So make sure all controllers, actions and route values added to the path, do not contain that. The reason is that I'm checking for the existence of /frontend/ in the path, to ensure that the matched controllers will only redirect to that route if they they're not already using it.
There are a lot of added things you could do with that setup, but I don't know your requirements. As such, you should treat this code simply as a skeleton to get started, making sure to test that it does what you want it to do.
Updated per comments
I'll leave everything above there, just in case someone finds that useful. To address what you'd like to do, however, we need a different approach. Again, we need some route constraints, but the way I see this working is to flip your idea on its head and make the frontend the default route. Like so:
routes.MapRoute(
name: "Backend",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
constraints: new { controller = "Home|Backend" }
);
routes.MapRoute(
name: "Default",
url: "{action}/{id}",
defaults: new { controller = "Frontend", action = "Index", id = UrlParameter.Optional },
constraints: new { action = "Index|OurSeoFeature" }
);
Just as before, I've applied some constraints to get the correct behaviour. In particular, for this constraint:
constraints: new { controller = "Home|Backend" }
if you have a lot of controllers that aren't part of the frontend, it might be an idea to implement IRouteConstraint to keep a list of the controller names there. You could even go as far as deriving all of your backend controllers from a base controller, so you can grab all of them with reflection in the IRouteConstraint implementation. Something like this:
public BackendController : Controller
{
//
}
Then:
public AdminController : BackendController
{
//
}
Constraint:
public class BackendConstraint : IRouteConstraint
{
// Get controller names based on types that
// BackendController
}
This same idea also applies to getting the action names of FrontendController for the second constraint. The only thing you need to be careful of here is that you don't have any backend controllers which have the same name as an action on your FrontendController, because it will match the wrong route.
I appreciate the question is over a year old with an accepted answer but the accepted answer involves route constraints when none are necessary. It's really just as simple as:
routes.MapRoute("SEO", "OurSeoFeatures",
new { controller = "frontEnd", action = "OurSeoFeatures"});
The basic idea of the route is controller/action.
So if you want to hit the OurSeoFeatures controller's index action then you have to give your route like
routes.MapRoute(
name: "BasicController",
url: "{controller}/{action}/{id}",
defaults: new { controller = "OurSeoFeatures",action="Index", id = UrlParameter.Optional }
);
In your case you have left out the controller from your route url. Please specifiy the controller also as part of URL and have a default controller.
Our app has multiple tenants. Every tenant has a short code assigned to them that users know them by. I want to use that code in my URLs as a route parameter, and have Ninject inject a DbContext with the tenant's database connection string into the tenant-specific controllers.
So for examine I have a CarController, and every tenant has their own products. The URLs would look like {tenantcode}/{controller}/{action}. I understand how to do this part.
However, I have several controllers that should NOT be instanced by tenant. Specifically, the home controller, and account controller for login/registration. These don't matter.
So example URLs I need:
myapp.com/ - HomeController
myapp.com/Account/Login - AccountController
myapp.com/GM/Car/Add - CarController that has GM's DbContext injected
myapp.com/Ford/Car/Add - CarController that has Ford's DbContext injected
How can I exclude certain controllers from routes? Running ASP.NET MVC 5.
Many thanks to Darko Z for starting me in the right direction. I ended up using a hybrid of traditional routes, and the new attribute based routing in MVC 5.
First, the "excluded" routes got decorated with the new RouteAttribute class
public class HomeController : Controller
{
private readonly TenantContext context;
public HomeController(TenantContext Context)
{
this.context = Context;
}
//
// GET: http://myapp.com/
// By decorating just this action with an empty RouteAttribute, we make it the "start page"
[Route]
public ActionResult Index(bool Error = false)
{
// Look up and make a nice list of the tenants this user can access
var tenantQuery =
from u in context.Users
where u.UserId == userId
from t in u.Tenants
select new
{
t.Id,
t.Name,
};
return View(tenantQuery);
}
}
// By decorating this whole controller with RouteAttribute, all /Account URLs wind up here
[Route("Account/{action}")]
public class AccountController : Controller
{
//
// GET: /Account/LogOn
public ActionResult LogOn()
{
return View();
}
//
// POST: /Account/LogOn
[HttpPost]
public ActionResult LogOn(LogOnViewModel model, string ReturnUrl)
{
// Log on logic here
}
}
Next, I register the tenant generic route that Darko Z suggested. It's important to call MapMvcAttributeRoutes() before making other routes. This is because my attribute based routes are the "exceptions", and like he said, those exceptions have to be at the top to make sure they are picked up first.
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// exceptions are the attribute-based routes
routes.MapMvcAttributeRoutes();
// tenant code is the default route
routes.MapRoute(
name: "Tenant",
url: "{tenantcode}/{controller}/{action}/{id}",
defaults: new { controller = "TenantHome", action = "Index", id = UrlParameter.Optional }
);
}
}
So as I'm sure you know you specify routes in MVC in the order from most specific to most generic. So in your case I would do something like this:
//exclusions - basically hardcoded, pacing this at the top will
//ensure that these will be picked up first. Of course this means
//you must make sure that tenant codes cannot be the same as any
//controller name here
routes.MapRoute(
"Home",
"Home/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
routes.MapRoute(
"Account",
"Account/{action}/{id}",
new { controller = "Account", action = "Index", id = "" }
);
//tenant generic route
routes.MapRoute(
"Default",
"{tenantcode}/{controller}/{action}",
new { tenantcode = "Default", controller = "Tenant", action = "Index" }
);
//default route
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
This is obviously only good if there are less excluded controllers than controllers that need the tenant code. If not then you can take the opposite approach and reverse the above. Main takeaway here is that (happy to be proven wrong) there is no way to have a generic ignore within an AddRoute call. While there is an IgnoreRoute, that just completely doesn't apply any routing rules and is used for static resources. Hope that helps.
I'm just learning MVC and want to add some custom routing to my site.
My site is split into brands so before accessing other parts of the site the user will select a brand. Rather than storing the chosen brand somewhere or passing it as a parameter I would like to make it part of the URL so to access the NewsControllers index action for example rather than "mysite.com/news" I would like to use "mysite.com/brand/news/".
I just really want to add a route which says if a URL has a brand, go to the controller/action as normal and pass through the brand...is this possible?
Thanks
C
Yes, this is possible. First, you must create a RouteConstraint to insure that a brand has been chosen. If a brand has not been chosen, this route should fail, and a route to an action to redirect to the brand selector should follow. The RouteConstraint should look like this:
using System;
using System.Web;
using System.Web.Routing;
namespace Examples.Extensions
{
public class MustBeBrand : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// return true if this is a valid brand
var _db = new BrandDbContext();
return _db.Brands.FirstOrDefault(x => x.BrandName.ToLowerInvariant() ==
values[parameterName].ToString().ToLowerInvariant()) != null;
}
}
}
Then, define your Routes as follows (assuming that your brand selector is the home page):
routes.MapRoute(
"BrandRoute",
"{controller}/{brand}/{action}/{id}",
new { controller = "News", action = "Index", id = UrlParameter.Optional },
new { brand = new MustBeBrand() }
);
routes.MapRoute(
"Default",
"",
new { controller = "Selector", action = "Index" }
);
routes.MapRoute(
"NotBrandRoute",
"{*ignoreThis}",
new { controller = "Selector", action = "Redirect" }
);
Then, in your SelectorController:
public ActionResult Redirect()
{
return RedirectToAction("Index");
}
public ActionResult Index()
{
// brand selector action
}
If your home page is not the brand selector, or there is other non-brand content on the site, then this routing is not correct. You will need additional routes between BrandRoute and Default which match routes to your other content.
I'm looking to make a really simple route in my ASP.NET MVC 2.0 website. I've been googling for help but all the examples I can find are for really complex routing.
Basically I want all the pages in my Home Controller to resolve after the domain as opposed to /Home/
For example I want http://www.MyWebsite.com/Home/LandingPage/
To become http://www.MyWebsite.com/LandingPage/
But only for the Home controller, I want the rest of my controllers to function as normal.
I thought about creating a controller for each and just using an index, but we need lots of landing pages for our marketing like this and it would quickly make the site loaded with controllers for a single page each, which is less than ideal.
One way to do this would be to have a separate route for each landing page. Another way would be to have a single route with a constraint that matches each landing page (and nothing else).
routes.MapRoute(
"LandingPage1"
"landingpage1/{id}",
new { controller = "home", action = "landingpage", id = UrlParameter.Optional } );
routes.MapRoute(
"LandingPage2"
"landingpage2/{id}",
new { controller = "home", action = "landingpage2", id = UrlParameter.Optional } );
Note that you could probably do this with a bit of reflection as well (untested).
foreach (var method on typeof(HomeController).GetMethods())
{
if (method.ReturnType.IsInstanceOf(typeof(ActionResult)))
{
routes.MapRoute(
method.Name,
method.Name + "/{id}",
new { controller = "home", action = method.Name, id = UrlParameter.Optional } );
}
}
The RouteConstraint solution would be similar except that you'd have a single route with a custom constraint that evaluated whether the appropriate route value matched one of the methods on the HomeController and, if so, replaced the controller and action with "home" and the matched value.
routes.MapRoute(
"LandingPage",
"{action}/{id}",
new { controller = "home", action = "index", id = UrlParameter.Optional },
new LandingPageRouteConstraint()
);
public LandingPageRouteContstraint : IRouteConstraint
{
public bool Match
(
HttpContextBase httpContext,
Route route,
string parameterName,
RouteValueDictionary values,
RouteDirection routeDirection
)
{
// simplistic, you'd also likely need to check that it has the correct return
// type, ...
return typeof(HomeController).GetMethod( values.Values["action"] ) != null;
}
}
Note that the route per page mechanism, even if you use reflection, is done only once. From then on you do a simple look up each time. The RouteConstraint mechanism will use reflection each time to see if the route matches (unless it caches the results, which I don't think it does).
I think you are missing the default route.
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
So, when you type www.mywebsite.com, the controller, action, and id parameters would have the following values:
controller : Home
action: Index
id : ""
I have a action on my controller (controller name is 'makemagic') called 'dosomething' that takes a nullable int and then returns the view 'dosomething.aspx'. At least this is what I am trying to do. Seems no matter I get routed to the Default() view.
public ActionResult dosomething(int? id)
{
var model = // business logic here to fetch model from DB
return View("dosomething", model);
}
There is a /Views/makemagic/dosomething.aspx file that has the Inherits System.Web.Mvc.ViewPage
Do I need to do something to my routes? I have just the 'stock' default routes in my global.aspx.cs file;
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
}
I am calling the action via a href like this in another page;
Click Me!
Seriously driving me nutso. Any suggestions on how to troubleshoot this? I attempted to debug break on my route definitions and seems a break there doesn't happen as one would expect.
Change it so the parameter isn't nullable so it will match the default route, or change the name to something other than id and supply it as a query parameter. An example of the latter would be:
public ActionResult dosomething(int? foo)
{
var model = // business logic here to fetch model from DB
return View("dosomething", model);
}
Click me
The it will work with the default routing implementation. Alternatively, you could do something that would distinguish it from the default route and then you would be able to have a route for it and not have to use query parameters.
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/foo/{id}", // URL with parameters
new { controller = "makemagic", action = "dosomething", id = "" } // Parameter defaults
);
Click Me!