asp mvc using dashes in controller names and routing? - asp.net-mvc

i have two questions. i'm fairly new to MVC and love the new way the controller and views are set up, but i can't figure out how to do the following:
1) make a url like www.homepage.com/coming-soon
for this type of url what is the right way to do it? do you create a controller named ComingSoonController and somehow magically insert a dash via routing? note i do NOT want underscores as that's not in the best interest of SEO. or is coming-soon some action name on some other controller that is not in the URL and use the [ActionName("name-with-dash")] attribute?
2) facebook, linkedin and twitter have urls like www.facebook.com/[profile name]. how would this be done in MVC? obviously the [profile name] is dynamic. and the code would obviously live in a controller called, say, profiles. so it seems to me that you would have to make MVC smart enough to know when that second part of the URL is a profile name and NOT a controller, and route it to the right action on the profiles controller? is this easier than it sounds?

1) It depends if coming-soon is dynamic part or not. I'll presume it is and would suggest something like this:
Global.asax
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"Page", // Route name
"{pageName}", // URL with parameters
new { controller = "Home", action = "Page"} // Parameter defaults
);
}
public class HomeController : Controller
{
public ActionResult Page(string pageName)
{
return View();
}
}
2) You can resolve this same way as I've shown above, but keep in mind that order of routes is important. And that first one that matches wins. If you want two actions that have different logic but similar url structure www.mysite.com/coming-soon and www.mysite.com/{profile name}, presuming that first url has static part and the later dynamic you could do something like this:
Global.asax
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"Coming-soon", // Route name
"coming-soon", // URL with parameters
new { controller = "Home", action = "ComingSoon" } // Parameter defaults
);
routes.MapRoute(
"Profiles", // Route name
"{profileName}", // URL with parameters
new { controller = "Home", action = "Profile"} // Parameter defaults
);
}
public class HomeController : Controller
{
public ActionResult ComingSoon()
{
return View();
}
public ActionResult Profile(string profileName)
{
return View();
}
}

You could create a custom route handler be allow hyphens in the urls:
Create a new handler
public class HyphenatedRouteHandler : MvcRouteHandler{
protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
{
requestContext.RouteData.Values["controller"] = requestContext.RouteData.Values["controller"].ToString().Replace("-", "_");
requestContext.RouteData.Values["action"] = requestContext.RouteData.Values["action"].ToString().Replace("-", "_");
return base.GetHttpHandler(requestContext);
}
}
...and the new route:
routes.Add(
new Route("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Default", action = "Index", id = "" }),
new HyphenatedRouteHandler())
);
MVC Hyphenated urls

I've developed an open source NuGet library for the first problem which implicitly converts EveryMvc/Url to every-mvc/url.
Dashed urls are much more SEO friendly and easier to read. (More on my blog post)
NuGet Package: https://www.nuget.org/packages/LowercaseDashedRoute/
To install it, simply open the NuGet window in the Visual Studio by right clicking the Project and selecting NuGet Package Manager, and on the "Online" tab type "Lowercase Dashed Route", and it should pop up.
Alternatively, you can run this code in the Package Manager Console:
Install-Package LowercaseDashedRoute
After that you should open App_Start/RouteConfig.cs and comment out existing route.MapRoute(...) call and add this instead:
routes.Add(new LowercaseDashedRoute("{controller}/{action}/{id}",
new RouteValueDictionary(
new { controller = "Home", action = "Index", id = UrlParameter.Optional }),
new DashedRouteHandler()
)
);
That's it. All the urls are lowercase, dashed, and converted implicitly without you doing anything more.
Open Source Project Url: https://github.com/AtaS/lowercase-dashed-route
When it comes to the second problem, you can do this either by making your own routes or handling not found with a custom error handling mechanism, but the routing will be faster if you restrict the profile urls to concur to some rules (like don't have any slashes) so that you can distinguish it from other urls much more easily, like from content file urls i.e. .css .js etc.

Related

ASP.NET MVC 4 Routing I just can't figure out

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.

How do I use routes for multi-tenancy in all but one 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.

ASP.NET MVC4 Searching for controller in wrong area

I'm using default MVC routing setup:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
I have area defined as:
public class AdministrationAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Administration";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Administration_default",
"Administration/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
And I have a controller in that area:
namespace XXX.Areas.Administration.Controllers
{
public class CountryController : Controller
{
public ActionResult Index()
{
///
}
}
}
When I type
/Administration/Country
it works good as it is desired.
When I type
/Country
action still gets invoked, though view is not found so I get an error.
Why is MVC accepting
/Country
as valid route? I don't have another CountryController in non-area zone.
Add NameSpace in the Global.asax file for the default Route.
var route = routes.MapRoute(
"Default", // Route name
"{controller}/{action}", // URL with parameters
new { controller = "Home", action = "Index" }, // Parameter defaults,
new[] { "YourNameSpace.Controllers" }
);
Add NameSpace in the AreaRegistration class present in your Area
public class MyArea : AreaRegistration
{
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"test",
"Test/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new[] { "MyProjectNameSpace.Controllers" }
);
}
}
Explanation
I have a following area in my application. So the below highlighted section is out concerned Controller. ok.
Figure -1
I typed Url : http://localhost:2474/ActionFilterAttribute/index
Before moving toward the destination. I will show you some how I initialized my test. I added a Reference of RoureDebugger. You can get the Dll from this location. Then I added a line of code in my Global.asax file under Application_Start Handler.
RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);
So, finally using the above mentioned Url, I started debugging the application. Finally I saw the below picture.
Figure -2
Question
action still gets invoked, though view is not found so I get an error.
Answer
So if you pay attention to the above highlighted Route, well, this is the Default Route. This pattern is matched with Url as mentioned above. But View will not be found in this case and that's the reason your Controller Action Method is Invoked.
Before moving to the next part that why did I get 404. I will show you some test I did in my sample application.
I created a class which derives from ActionFilterAttribute like below. This contains only one Override that's called OnResultExecuting. This Handler executes before executing the View corresponding to particular Action
The purpose of Creating this class is just to verify what is happening with RouteData and DataTokens.
public class MyActionFilter : ActionFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
var viewResult = filterContext.Result as ViewResult;
if (viewResult != null)
{
var razorEngine = viewResult
.ViewEngineCollection
.OfType<RazorViewEngine>()
.Single();
var viewName = !String.IsNullOrEmpty(viewResult.ViewName) ?
viewResult.ViewName :
filterContext.RouteData.Values["action"].ToString();
var razorview = razorengine
.FindView
(
filtercontext.Controller.ControllerContext,
viewname,
viewResult.MasterName,
false
).View as RazorView;
}
base.OnResultExecuting(filterContext);
}
}
Ok. Let's come back to the original question. Why did I get 404?
Your Controller will be picked up by the default base route {controller}/{action} before it checks the Area Route and therefore will look for the View in the Root/Views instead of in the Area/views.
To examine this, I set the debugger in the Action Method of Controller inside the Area and found that that there is no DataToken Information when the Requested url is without Area Name. let's see more details about DataToken in Debug Mode
Figure -3
If you pay attention to the ControllerContext, I enumerated the DataTokens, which is showing no key/Value. It's because no view is found pertaining to that controller under the Root Directory
How can you say that the currently located Directory is Root Directory? Proof is below
Figure -4
There is no Namespace or any Area mentioned in the RouteData values. right?
Now let's move to the RouteData that matched the pattern which is containing the Area Name. So, this time my Url is : http://localhost:2474/mypractise/ActionFilterAttribute/index and below is the RouteData matched by URLRoutingModule
Figure -5
Please pay attention to the highlighted section, this time the Route matched belongs to AreaName pattern and matched value is false for the Default Route which belongs to some RouteData at Root Directory. Right?
My final details for the DataTokens in case of above mentioned requested Url. You can see the Namespace details and Area details this time.
Figure -6
Conclusion :
When the Controller is inside the Area and your DataTokens are not showing the information for Area , NameSpace and UseNameSpaceFallback informations. That means you will get 404. As mentioned in Figure-4, your requested Url was correct, so you got the DataTokens and As mentioned in Figure 3, DataTokens were not shown because the requested Url does not contains the Area Name and despite of the fact that you have got the RouteData as mentioned in Figure 2 because it's a default Url Pattern. Finally try to execute the third line of code in OnResultExecuting. It will show null because View is not found.
Hope this explanation will help you.
Check it. Modify the default route in your Global.ascx.cs file like so.
var route = routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "APPLICATION_NAMESPACE.Controllers.*" }
);
route.DataTokens["UseNamespaceFallback"] = false;
EDIT:
My apologies. It seemed like you didn't want it to do this as well as know why.
You are running into the fact that the default routing will look for anything that is a controller. Even if it's in an Area. You can overcome this default behavior by simply adding the namespaces parameter to the route and specify what the default routing should be looking for with controllers.
The solution that I provided above is merely a fix if you wanted to not serve the view of an area outside the area itself.
There is a great article on why this is occurring here.

ASP.NET MVC + WebForms - route conflict

I'm building a simple report Web app for rendering reports using ASP.NET MVC3 + WebForms. The reports themselves are rendered by the ReportViewer ASP.NET WebForms control, but I'd like use ASP.NET MVC to create the parameter entry.
I'd like to have that all requests follow the default routing scheme of '~/{controller}/{action}/{parameters}', except requests for ~/Report, which should go to the report rendering WebForm. What's the right way to do this?
Expanding a bit..
I have two routes in Global.asax.cs - the default one and one for the WebForms page.
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 = UrlParameter.Optional } // Parameter defaults
);
routes.MapPageRoute("report-rendering", "Report", "~/Render.aspx");
}
The URLs get rendered fine, but the problem with this is that when the request comes in, the first route also eats the URLs for the second one, i.e. ~/Report?id=7 tries to call the Index method on the ReportController (which doesn't exist).
If I change it so that the 'report-rendering' route comes before the 'Default' route, like so:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapPageRoute("report-rendering", "Report", "~/Render.aspx");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}
Now calls to the Html.ActionLink() render incorrect URLs, i.e.
`#Html.ActionLink("Report list", "Index", "ReportList")`
Renders
`http://localhost:49910/Report?action=Index&controller=ReportList`
My current workaround puts the 'Default' route first, while adding a regex constraint to ignore requests for the 'Report' controller, like so:
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
new { controller = #"(?!report$).*" }
);
This doesn't feel clean. Again, What's the right way of doing this?
Also, I haven't yet decided how I'll pass the parameters to the rendering form: I could use both query parameters or POST them. I'm guessing that query params are more flexible. What's the best practice here?
EDIT:
While researching the answer by #LeftyX, seems like I've found an answer. To quote P. Haack from his Routing chapter in the Professional ASP.NET MVC 3 (Named Routes, Chapter 9, page 233):
... Use names for all your routes and
always use the route name when generating URLs. Most of the time, letting Routing sort out which
route you want to use to generate a URL is really leaving it to chance, which is not something that
sits well with the obsessive-compulsive control freak developer. When generating a URL, you generally
know exactly which route you want to link to, so you might as well specify it by name.
The mentioned section discusses a very similar situation to the one I described.
But since Html.ActionLink() doesn't have an overload with the route name parameter, does this mean I cannot reliably use it anywhere in the entire app if have a route like this?
This is the best solution I've figured out.
I've registered my route with MapPageRoute (I've put my Report page under a folder called Reports)
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapPageRoute(
"report-rendering",
"Report/{id}",
"~/Reports/Report.aspx"
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
I've created my link using RouteLink so you can specify the route to use:
#Html.RouteLink("Report list", "report-rendering", new { id = 7 })
and I can get the id in my WebForm page like this:
protected void Page_Load(object sender, EventArgs e)
{
var id = Page.RouteData.Values["id"] as string;
}
Hope it helps.
UPDATE:
I've created an Extension Method to make your life easier:
public static class ExtensionMethods
{
public static MvcHtmlString WebFormActionLink(this HtmlHelper htmlHelper, string linkText, string ruoteName, object routeValues)
{
var helper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
var anchor = new TagBuilder("a");
anchor.Attributes["href"] = helper.RouteUrl(routeName, routeValues);
anchor.SetInnerText(linkText);
return MvcHtmlString.Create(anchor.ToString());
}
}
The best would have been to use ActionLink instead of WebFormActionLink but I have problems with signatures and I am not an expert on this.

ASP.NET MVC3 Routing various subfolders to the same controller

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

Resources