Routing with Areas and Controller Name (asp.net core) - c#

To stop my application from getting cluttered I started working with areas. But now I always have to call:
http://localhost:49358/Document/Document/
instead of:
http://localhost:49358/Document/
How can I change my route to access the Controllers by name of the Area?
(no HomeController)
I have the following folder structure inside of my Project:
The code for my route to the Areas looks like this:
routes.MapRoute(name: "areaRoute",template: "{area:exists}/{controller=Home}/{action=Index}");
And I placed the [Area("Document")] Tag in my DocumentController.
Edit:
As suggested by Shyju and Jamie Taylor I went with the HomeControllers. (Thank you both for the quick answers and explanations)
My Structure now looks like this and the routing is working like expected:
For me it's still a bit dissapointing to have so many HomeControllers and Index Files. Navigating code isn't that easy anymore:
Edit2:
After getting too annoyed with all those Homecontrollers, I went with the solution suggested by Jamie Taylor and rearanged everything in a Features folder. It needs a bit more configuration but is much cleaner in my opinion.
It is also further explained in this Microsoft Article (Just skip the Area stuff):
https://msdn.microsoft.com/en-us/magazine/mt763233.aspx
My structure now looks like this and routing works like a charm and the controller names are still meaningful:

I'm not sure that's what areas are for. Perhaps you should rethink your architecture.
In my ASP.NET MVC Core application template, I've leveraged Feature folders, which works a little like areas.
To do this, I've added the following to the ConfigureServices method:
serviceCollection.Configure<RazorViewEngineOptions>(options =>
{
options.ViewLocationExpanders.Add(new FeatureLocationExpander());
});
Where FeatureLocationExpander is:
public class FeatureLocationExpander : IViewLocationExpander
{
public void PopulateValues(ViewLocationExpanderContext context)
{
// Don't need anything here, but required by the interface
}
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
// The old locations are /Views/{1}/{0}.cshtml and /Views/Shared/{0}.cshtml
// where {1} is the controller and {0} is the name of the View
// Replace /Views with /Features
return new string[]
{
"/Api/{1}/{0}.cshtml",
"/Features/{1}/{0}.cshtml",
"/Features/Shared/{0}.cshtml"
};
}
}
Replacing the contents of the new string[] which is returned by ExpandViewLocations for your areas will mean that you won't have to add the routing attributes.
BUT, this does not fix your issue as that is not what areas are for.
Unless you added a razor page (named Index.cshtml) under the Documents area which acts as an index page for the Documents area. This Index.cshtml could provide all of the functionality of the Index.cshtml in your /Documents/Documents/Index.cshtml with the added bonus of having a code-behind like file (remember ASP.NET webforms?) which acts like your controller.

The default route registration for areas use HomeController as the default value for the controller in the url. If you want DocumentController to be the default, update it in the StatrtUp class.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller=Document}/{action=Index}/{id?}"
);
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Keep in mind that that registration code is for all the existing areas (because we have {area:exists} in the url template), not just Document area. That means, Any time a request comes like yourApp\someAreaName, the framework will send the request to the index action of DocumentController in someAreaName.
You already organized your code related to documents to the Document area. Now why you need your controller name to be document ? I feel, it is repetitive.
I would personally rename the DocumentController inside the Document area to HomeController and use the default route registration code for the area registration ( which uses HomeController as the default controller value in the url template). This way, It will work for your future areas and your code looks more clean. IMHO, A HomeController in any area make sense where a DocumentController in any area will be confusing.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areas",
template: "{area:exists}/{controller=Home}/{action=Index}/{id?}"
);
});

Related

How to use an area for login in ASP.NET Core 6 MVC

I used the default method to create an area. Right-click on project and add a new scaffolded item. Selected area, gave it a name and it created the folder structure.
My area is called Login. I created a controller (in the Areas/Login/Controllers folder) called AccountController with a method called Login that does nothing but return view();.
In my HomeController I added an [Authorize] attribute and the only action is the default index.
I have my routes setup:
app.UseEndpoints(endpoints => {
endpoints.MapControllerRoute(name: "login",
pattern: "{area:exists}/{controller=Account}/{action=Login}/{id?}");
endpoints.MapControllerRoute(name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
I tested with a break point and I am correctly hitting the AccountController > Login action inside the area, but when it returns the view it does not find the view Areas/Login/Views/Account/Login.cshtml.
It's actually looking in the main site. If I add a Login.cshtml file inside the main site's Views/Shared folder it will load it.
I feel like I'm missing a configuration step somewhere in the program.cs file but I don't know what it would be. Like Identity is not looking in the right spot, or I have to specify it's in an area. Help me Obi-wan.
I figured it out myself. This is what worked to create an identity redirect into an Area with my own custom controller/path/view instead of using the asp.net core built in magical identity pages.
Add an endpoint in program.cs to map the route.
endpoints.MapAreaControllerRoute(
name: "Buffoon",
areaName: "Buffoon",
pattern: "Buffoon/{controller=Account}/{action=Login}");
Create the Area in your code; Right click on the Website, select Add > New Scaffolded item, Select Area, Input Area Name.
Add your controller and view in the area to match with your route defaults.
Tag your controller inside your area with [Area("Area_Name")]. In my example above I would have used [Area("Buffoon")]
In the builder configuration area of your program.cs file set the login path variable inside the application cookie. Using my example above;
builder.Services.ConfigureApplicationCookie(cke => { cke.LoginPath = "/Buffoon/Account/Login"; });
Add your identity to the builder configuration area. I'm using Sql Server with Entity Framework so here is an example of what I already had configured prior to this. (User is a custom class inheriting from IdentityUser and MyDbContext is my custom EF context inheriting from IdentityDbContext<User>).
builder.Services.AddIdentity<User, IdentityRole>(cfg => { cfg.User.RequireUniqueEmail = true; }).AddEntityFrameworkStores<MyDbContext>();
Ensure you have added app.UseAuthentication(); and app.UseAuthorization(); to your application configuration section of the program.cs file.
Lastly I put an [Authorize] attribute on my default route home controller and upon hitting that, the redirect happened into my newly created Area and displayed my login view.

ASP.NET Core - Configurable routing

I have an ASP.NET Core 3.1 app. This app has an "Admin" area. I've put this area in it's own library to use in other apps. In my other app, I can currently access the "Admin" area by visiting https://{domain}/_admin. I want to reuse/share this library with other apps in my company. Now, other devs in my company have built other apps. So, I expect the _admin path may cause conflicts. For that reason, I want to make that part of the route configurable. Currently, I have the following
appsettings.json
{
"Miscellaneous": {
"AdminRoot":"_admin"
}
}
Startup.cs
...
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseEndpoints(endpoints =>
{
var adminRoot = Configuration["Miscellaneous:AdminRoot"];
adminRoot = String.IsNullOrEmpty(adminRoot) ? "_admin" : adminRoot;
endpoints.MapAreaControllerRoute(
name: "Admin",
areaName: "Admin",
pattern: (adminRoot + "/{controller=Home}/{action=Index}/{id?}")
);
}
...
}
...
AdminController.cs
[Area("Admin")]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
[Route("~/api/user-permissions")]
public IActionResult LoadUserPermissions()
{
var permissions = GetPermissions();
return Json(permissions);
}
}
This approach uses [convention-based routing][1]. At the same time, my Admin tool also has it's own REST API. For that reason, I want to mix convention-based routing with [attribute routing][2]. Basically, I want to have routes like /_admin/api/user-permissions. As this route demonstrates, the _admin creates a challenge if I add a Route attribute in my API since it's a configured value.
I thought if I used the ~ it would be relative to the area. However, I get a 404 when I try this approach. Is there a way to dynamically set parts of a route? For example, I would love to be able to either:
Use an approach relative to the area or
Use an attribute like [Route("{Configuration["Miscellaneous:AdminRoot"]}/api/user-permissions")]
I'm not finding a way to accomplish what I'm trying to do. Is there a way to do this? Please note, my Admin area is much more complex. I've tried to isolate this question to my actual problem. In addition, I'm looking to have a configurable approach for the sake of learning. For that reason, I'm not interested in solutions like changing "_admin" to a random value like a GUID or something. Thank you for your help!
I have been reviewing this for several hours. Essentially, since the one parameter is pulled from configuration, the routing requires convention based routing. However, some names, like those URL-friendly names that include dashes are invalid method names in C#. The way around this is use the following:
Rely on convention-based routing
Use attributes other than Route in the controllers. On actions, use the ActionName attribute.

Why is routing calling HomeController Index when it should be mapped to different controller?

I want the url https://localhost:44354/Bills to call the Index action in the BillsController.cs. Instead, it is calling the Index action from HomeController.cs. If I use https://localhost:44354/Bills/Bills it does call the BillsController's Index action.
The way I understand it, Bills should map to the controller and it should default to Index without a second parameter in the URL. To test, I tried https://localhost:44354/Bills/Index which give a 404. Can someone explain this behavior? It seems to not be behaving correctly.
The routing in my Startup.cs file:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
BillsController:
public class BillsController : Controller
{
public IActionResult Index()
{
return View();
}
}
My folder structure:
#NickOban, I tried creating a new MCV project and added a bills controller. I then added the Bills repository and added a index.cshtml
And the pathing worked for me. However, it seems that you might have implemented a prefix at some point with the name "Bills", which would cause the phenomenon you are experiencing.
Ensure you have no prefixes anywhere in your code. The MVC app should automatically fix any prefixes. Additionally try https://localhost:44354/Bills/Index.cshtml instead of simply Index ~ because then the MVC is expexting that Index is a Folder.

Asp.net MVC routing using subfolders

I m trying to use subfolders within the Controllers folder:
The structure looks like this:
Controllers (Folder)
LoginController.cs
WelcomeController.cs
Settings (Folder)
UsersController.cs
I've several problems.
When I perform a return RedirectToAction("Index", "welcome") from my LoginController, the url looks like http://mywebsite.local/settings/welcome
I thought I will get a 404 error..
How to make the redirection launches http://mywebsite.local/welcome and get a 404 error when I launch http://mywebsite.local/settings/welcome
Do I really need to use Areas?
This is my RouteConfig.cs
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "settings",
url: "settings/{controller}/{action}/{id}",
defaults: new { controller = "Users", action = "Index", id = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Login", action = "Index", id = UrlParameter.Optional }
);
Do I really need to use Areas?
No, but you're trying to re-invent the wheel - creating a structure a bit like Areas. I'd recommend you go with Areas, it will make your life easier.
Area Hype:
I wouldn't recommend applying Areas just because you can.
That would be like "giving a man a hammer and suddenly everything's a nail".
In this case we have a cool feature called "Areas" and (without knowing the underlying architecture)
others recommended it the instant someone asks about sub-folders.
Areas weren't designed for the sole purpose of giving you an extra Route Parameter.
The "Areas" original intent is to give you a clear SoC (Separation of Concerns).
For Example:
Say you have two separate web application experiences and have decided to roll them under one Solution.
Your call-center may need to look up detailed information and enter in data on individual customers, while your managers and executives will peruse higher-level reporting and rarely enter in data.
In this scenario it may make sense to split your business logic into "Reporting" and "CallCenter" Areas.
Sub-Directories:
In MVC, the Sub-Folders you use under "Controllers" are Ignored when it comes to Routing.
SubFolders are perfectly fine with how the Questioner is using them to organize his Controllers.
Adding a SubFolder name to his URL makes for a more human-readable URL too.
He just made a mistake in the exclusivity of his first Mapping Route.
The problem is it was matching on everything.
Just because you have "settings/" in your MapRoute doesn't mean it will apply only to incoming URL's.
MVC will use your MapRoute Logic to figure how you would like to write your URL's too!
The Fix (Option 1 - Use MapRoute):
routes.MapRoute(
name: "Settings",
url: "Settings/{controller}/{action}/{id}",
defaults: new { action = "Index", id = UrlParameter.Optional },//Remove the Default Controller as we want to explicitly require it and constrain it. - 08/26/2018 - MCR.
constraints: new { controller = "Users|Admins" }//If you have other Controllers under a SubFolder (say we also had AdminsController.cs), then simply Pipe-Delimit them. - 08/26/2018 - MCR.
);
This is the way I've chosen to go, but if you want tighter control, you could use the Option below instead.
The Fix (Option 2 - Use Route-Attributes):
First, make sure you enable this feature by adding routes.MapMvcAttributeRoutes():
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes();//Add this BEFORE MapRoute for Route-Attributes to work.
Next, Decorate your Controller with the following Attributes:
(You may even include Default/Optional Values, just like in your MapRoute.)
[RoutePrefix("Settings/Users")]//Add your ParentFolder and Include your Controller name too.
[Route("{action=Index}/{id?}")]//You need this if you are using the RoutePrefix Attribute. Without this, you will need to define "[Route]" above every Action-Method. - 08/26/2018 - MCR.
public class UsersController : Controller
Note:
If you use Route-Attributes instead of MapRoute, then you will not be able to hit the
Controller without the "Settings" Prefix.
With the Custom and Default MapRoutes, you could have accessed your controller either way.
By decorating your Controller with these Attributes, you now force it to only use this exact path.
This may be what you want, but if you start IIS Express from Visual Studio on one of your Views,
then it will not find it because Visual Studio does not know to add the RoutePrefix for you.
I say this, so you are not discouraged when you start debugging and think it doesn't work.
See this link for more information about Attribute-Routing:
https://blogs.msdn.microsoft.com/webdev/2013/10/17/attribute-routing-in-asp-net-mvc-5/
The folder structure of your controllers here has very little relevance to what you are seeing. You could maintain that structure and accomplish your goal if the route would work.
However, you've specified two matching routes that can be used to encode the same action, and it is simply giving the first one priority. See, routes don't just work forwards, MVC uses them in reverse, too, to process your ____Action() statements. You could specify a named route (e.g. "settings") in your RedirectToAction("Index", "welcome"), by using RedirectToRoute instead, or manually specify all the routes that have prefixes in your route table. but why start your project off with intentionally conflicting routes like this?
As Joe R said, Areas will make your life easier. Why? Mainly because there is an additional built-in route parameter to do exactly what you want. Really, the question becomes "why avoid Areas?"
Looks like the answer not answering the question, because IMHO, we use Area when we need mini program under our main program,
ie. Forum, (Comprehensive) Blog, Marketplace (under our main site) either forum.mainsite.com or mainsite.com/forum
So you DONT need Area in your case
Solutions :
FYI, routing are something that have nothing to do with your architecture / structure / foldering in your applications.
In Example your ControllerName is SettingsUsersController
routes.MapRoute(
name: "settings",
url: "settings/users/{action}/{id}",
defaults: new { controller = "SettingsUsers", action = "Index", id = UrlParameter.Optional }
);
in your case, you can fix your routing like this (this is for making sure you have pretty url but still simple Controller structure):
routes.MapRoute(
name: "settings",
url: "settings/{controller}/{action}/{id}",
defaults: new { controller = "SettingsUsers", action = "Index", id = UrlParameter.Optional }
);
Your ControllerName would be SettingsUsersController
SettingsUsersController.cs
public class SettingsUsersController : Controller
{
public ActionResult Index()
{
return View("~/Views/SettingsUsers/Index.cshtml", db.YourDBName.ToList());
}
}
And why 404? I think because you are not "routing" correctly your View, you should do make subfolder like this under your Views Folder Views/SettingsUsers/Index.cshtml

Asp.net MVC routing function

Can someone please explain what the following function does. I am learning Asp.net MVC and unable to understand which controller is called when and renders which view.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//register custom routes (plugins, etc)
var routePublisher = EngineContext.Current.Resolve<IRoutePublisher>();
routePublisher.RegisterRoutes(routes);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "Nop.Web.Controllers" }
);
}
This code is from nopCommerce source-code. I can't understand the URL routing for this project
The logic for this is in the System.Web.Mvc.MvcHandler class, the System.Web.Mvc.DefaultControllerFactory class, and the System.Web.Mvc.ControllerActionInvoker class. .NET Reflector is your friend.
Basically, the MVC framework:
Uses reflection to get all the controllers in the application project.
Then it does something like IEnumerable<string> controllerNames = controllerTypes.Select(controllerType => controllerType.Name.Replace("Controller",string.Empty));. It then tries to match the first path segment, {controller}, to one of these sanitized controller type names (case-insensitive).
Then, it looks at this controller's public methods that have a return type that is of type ActionResult or some derivative. It matches the method name to the second path segment, {action}, as the action method to be called.
If the selected method has a parameter that is named id, then it matches the third path segment {id} to that value, and passes it to the method. Otherwise, the optional id parameter is ignored.
If the ActionResult type that is returned is a derivative of ViewResultBase then the IViewEngine tries to locate a corresponding view in the project using whatever conventions have been specified for that view engine. The WebFormViewEngine, for example, looks in the project for ~/Views/{controller}/{action}.ascx, ~/Views/{controller}/{action}.aspx, ~/Views/Shared/{action}.ascx, ~/Views/Shared/{action}.aspx by default.
If you want to further understand how routing works in MVC, I would highly suggest Scott Gu's article on MVC Routing.
As far as the IRoutePublisher method, that looks like a nopCommerce specific method that automatically registers additional routes specific to nopCommerce's configuration. If you are interested in how nopCommerce's specific routing conventions work, you can download the source code from the nopCommerce codeplex page and do a search for its default IRoutePublisher implementation.
Update The default IRoutePublisher is here: http://nopcommerce.codeplex.com/SourceControl/changeset/view/7e34dd9d98f3#src%2fPresentation%2fNop.Web.Framework%2fMvc%2fRoutes%2fRoutePublisher.cs . Basically, it gets all implementations of IRouteProvider and registers their route definitions in order according to their priority.
The default route providers are: Nop.Web.Infrastructure.RouteProvider and Nop.Web.Infrastructure.UpgradeRouteProvider
nopCommerce employs a loosely coupled infrastructure that registers routes for each plugin separately.
So If you need to understand what's going on, check the nopCommerce source code and look for RouteProvider classes, that each plugin has. They are dynamically loaded on application start.
If you need to create your own routes, you can still do that the traditional way -- but be aware, that there might be some clashes.
(Disclaimer: I just looked at the source code, don't know anything else about it).

Categories

Resources