Incorrect URL after changing routing in Area - asp.net-mvc

I am having trouble with rendering URL addresses after changing routing.
Routing Configuration
public class AccountArea : AreaRegistration
{
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Account",
"Account/{controller}/{action}/{id}",
new { controller = "User", action = "Index", id = UrlParameter.Optional },
namespaces: new[] {
"Application.Controllers.Account"
}
);
}
public override string AreaName => "AccountArea";
}
public class FrontArea : AreaRegistration
{
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Front",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new[] {
"Application.Controllers.Front"
}
);
}
public override string AreaName => "FrontArea";
}
and RouteConfig:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
}
Controllers
// on folder Controllers/Front/
namespace Application.Controllers.Front
class HomeController : Controller
{
ActionResult Index(){ ... }
}
// on folder Controllers/Account/
Application.Controllers.Account
class UserController : Controller
{
ActionResult Index(){...}
}
I have changed routing because I want to separate the controllers into 2 subfolders, Front and Account.
This configuration allows me to divide the application into 2 parts, differentiated by using URL address.
localhost/Home/Index [namespace: Application.Controllers.Front]
localhost/Account/User/Index- [namespace: Application.Controllers.Account]
after entering the address to the browser
localhost/Home/Index - will start method Index from HomeController
localhost/Account/User/Index - will start method Index from UserController
A problem appears when on URL address localhost/Home/Index and I try rendering URL address belonging to localhost/Account/User/Index.
When I use #Url.Action("Index","User") address will be rendered, but without the "Account" prefix in the URL.
I receive: /User/Index
But, I expect: /Account/User/Index
And vice versa, on localhost/Account/User/Index I can't render correct URL address belonging to localhost/Home/Index.
This problem I can solve using Url.RouteUrl like
#Url.RouteUrl("Account", new {controller = "User", account = "Index"} )
but, this requires route name, which I don't want to provide.

When using Area routes, all of the UrlHelper URL resolution methods (such as ActionLink or Url.Action) will use the same area by default.
Linking from Front Area to Front Area
#Url.Action("Index", "User")
To specify to go to a different area, you must explicitly specify the area in the route values that are passed in to Url.Action.
Linking from Front Area to Account Area
#Url.Action("Index", "User", new { area = "Account" })
The same goes for links generated inside of the Account area. You will need to explicitly override the current area name with Front.
Linking from Account Area to Front Area
#Url.Action("Index", "User", new { area = "Front" })
this requires route name, which I don't want to provide.
Option 1 - Use Constants for Areas
Make a set of constants that you use for all of your area strings in the application.
public static class Areas
{
public const string Front = "Front";
public const string Account = "Account";
}
And use like
#Url.Action("Index", "User", new { area = Areas.Front })
Option 2 - Use T4MVC
There is a package called T4MVC that generates constants for your project so you can replace routing and URL generation without using magic strings.
#Url.Action(MVC.Areas.Front.User.Index)
Option 3 - Use Extension Methods
Another option would be to build your own extension methods to replace Url.Action, Html.ActionLink, RedirectToAction, etc.
#Url.ActionToFront("Index", "User")
This is a lot of action methods to create, though.

Related

#Html.ActionLink is not linking to current area by default

I have 2 areas Admin and FrontEnd (in that order).
When I am in a view in my FrontEnd area, ActionLink always points to the Admin area:
#Html.ActionLink("Checkout", "Address", "Checkout")
Will be http://localhost:53600/admin/Checkout/Address but the Checkout controller is in my FrontEnd area.
I know I can solve this by specifying a routedata object in the action link and setting area = "FrontEnd" but I don't want to. I want the ActionLink helper to default to my current route.
Is this possible?
All the questions I've read on actionlink are people asking how to link to another area which indicates it defaults to the current area for them. Am I alone with this issue?
Edit, these are my routes which you can see are tied to the correct namespace:
Admin
public void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"Administration_default",
"admin/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", area = "Administration", id = UrlParameter.Optional },
new[] { "CC.Web.Areas.Administration.Controllers" }
);
}
FrontEnd
public void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("Default",
"{controller}/{action}/{id}",
new {controller = "Home", action = "Index", area = "FrontEnd", id = UrlParameter.Optional},
new[] {"CC.Web.Areas.FrontEnd.Controllers"}
);
}
Areas should be registered in classes deriving from AreaRegistration overriding the method void RegisterArea(AreaRegistrationContext context), see the msdn.
The AreaRegistrationContext defines its own methods to register area routes that will add the required dataTokens for the areas that are used when generating links and urls:
public Route MapRoute(string name, string url, object defaults, object constraints, string[] namespaces)
{
if (namespaces == null && Namespaces != null)
{
namespaces = Namespaces.ToArray();
}
Route route = Routes.MapRoute(name, url, defaults, constraints, namespaces);
route.DataTokens[RouteDataTokenKeys.Area] = AreaName;
// disabling the namespace lookup fallback mechanism keeps this areas from accidentally picking up
// controllers belonging to other areas
bool useNamespaceFallback = (namespaces == null || namespaces.Length == 0);
route.DataTokens[RouteDataTokenKeys.UseNamespaceFallback] = useNamespaceFallback;
return route;
}
It also looks like the FrontEnd shouldn't be an area, so you could just have the standard MVC controllers and views (instead of the fronteand area) with an extra Admin area:
public class AdminAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Administration";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Administration_default",
"admin/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "CC.Web.Areas.Administration.Controllers" }
);
}
}
Remember that you should be calling AreaRegistration.RegisterAllAreas(); at the beggining of your main RegisterRoutes method invoked on app start.

ASP MVC Rounting, Remove /Areas/ Prefix from uri

By default Areas have a /Area/ routing prefix. For instance a Blog area would be :
/Areas/Blog/Blog/Show/myId
or
/Areas/{area}/{controller}/{action}/{id}
The result is a really ugly and redundent uri for the web application. What I would like is something along the lines of :
/Blog/Show/myId
or
/{area / controller}/{action}/{id}
How can I achieve this ?
The reason I am using areas is because my 'blog' area is not a standard MVC application, but, a single-page application. It has a different project structure than the rest of the app, and so, I would like to just partition it into its own area.
Edit : source for Area Registration
public class BlogAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Blog";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Blog_default",
"Blog/{action}/{id}",
new { controller ="blog", action = "Index", id = UrlParameter.Optional }
);
}
}
Edit Source for Routeing
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Edit
After speaking /u/Eric Philips I have come up with a solution. The solution was rework my file structure to MVC compatible. Once I setup a controller and moved my index.cshtml page into a valid location, /Blog/ worked fine.
public class BlogController : Controller
{
// GET: Blog/Blog
public ActionResult Index()
{
return View();
}
}

Area routing, why is the area name needed?

I have a project with 2 areas. Its does work but I am a newbie to this and I want to understand why.
I have an Area called LogonArea
context.MapRoute(
"LogonArea_default",
"LogonArea/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
Why is the 'LogonArea/' part needed? Shouldn't it be able to find the controller without it?
When I tried removing it I could still reach controllers with that Area but strangely I couldn't reach other areas while on that page.
If this is really necessary how could I mask it so the Area wasn't visible in the url?
thanks
If you remove /LoginArea/ from the area route registration, it will be able to find your controller (as long as you don't have any conflicting controller names such as HomeController in the main section and HomeController in the area).
It's mainly there for your convenience. If you have an Admin area, everything in your site will be accessible via /Admin/{controller}. It's mostly just an organizational thing.
public class AdminAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Admin";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Admin_default",
"{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
I created a single controller called FooController in this project, and I was able to go to the url /Foo to reach it without needing to go to /Admin/Foo
When you create a link to a controller outside of the area you need to specify which area it's in (or specify that there is no area):
#Html.ActionLink("Go Home", "Index", "Home", new { area = "" }, null)

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 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