Default route interferes with defined path in attribute routing in ASP.MVC - c#

I have a controller with an action method and I have configured attribute routing:
[RoutePrefix("foos")]
public class FooController : BaseController
{
[HttpGet]
[Route("")]
public ActionResult List()
{
return View();
}
}
Here's routing configuration:
public static void RegisterRoutes(RouteCollection routes)
{
routes.MapMvcAttributeRoutes();
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
}
Everything works fine. When I navigate to http://webPageAddress/foo/ my action is called and list is returned.
Now I want to make this route default. I've added new attribute so:
[HttpGet]
[Route("~/")]
[Route("")]
public ActionResult List()
{
return View();
}
The result is default route (http://webPageAddress/) works, but the old one (http://webPageAddress/foo/) doesn't work anymore (http 404 code).
How can I mix it and have both configured properly?

You need to make sure the route for http://webPageAddress/foo/ is registered before the route for http://webPageAddress/. With attribute routing, the only way to do this is to use the Order property to set the order.
[HttpGet]
[Route("~/", Order = 2)]
[Route("", Order = 1)]
public ActionResult List()
{
return View();
}
Reference: Understanding Routing Precedence in ASP.NET MVC and Web API

Related

ASP.NET Core route of a controller and the index action

I'm trying to set a route of my controller while also be able to navigate the index without typing Index, here's what I tried:
My route configuration
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Try #1
// My controller
[Route("panel/admin")]
public class MyController...
// My index action
public IActionResult Index()...
Problem: This doesn't work, all the actions become accessible at panel/admin so I get an error saying Multiple actions matched.
Even when setting the route of my index action to Route(""), doesn't change anything.
Try #2
// My controller
[Route("panel/admin/[action]")]
public class MyController...
// My index action
[Route("")]
public IActionResult Index()...
Here, the index route doesn't change, it stays panel/admin/Index.
What I want
I want to be able to access my index action when navigating to panel/admin and I also want my other actions to work with just their method names like panel/admin/UsersList.
Complete controller
[Route("panel/admin/[action]")]
public class MyController
{
[Route("")]
public IActionResult Index()
{
return View();
}
public IActionResult UsersList()
{
var users = _db.Users.ToList();
return View(users);
}
// Other actions like UsersList
}
Thank you.
Reference Routing to controller actions in ASP.NET Core
With attribute routes you have to be very specific about desired routes to avoid route conflicts. Which also means that you will have to specify all the routes. Unlike convention-based routing.
Option #1
[Route("panel/admin")]
public class MyController {
[HttpGet]
[Route("")] //GET panel/admin
[Route("[action]")] //GET panel/admin/index
public IActionResult Index() {
return View();
}
[HttpGet]
[Route("[action]")] //GET panel/admin/UsersList
public IActionResult UsersList() {
var users = _db.Users.ToList();
return View(users);
}
// Other actions like UsersList
}
Option #2
[Route("panel/admin/[action]")]
public class MyController {
[HttpGet] //GET panel/admin/index
[Route("~/panel/admin")] //GET panel/admin
public IActionResult Index() {
return View();
}
[HttpGet] //GET panel/admin/UsersList
public IActionResult UsersList() {
var users = _db.Users.ToList();
return View(users);
}
// Other actions like UsersList
}
The tilde (~) in [Route("~/panel/admin")] overrides the route prefix on the controller.
Tip
While using multiple routes on actions can seem powerful, it's better
to keep your application's URL space simple and well-defined. Use
multiple routes on actions only where needed, for example to support
existing clients.

Set Order for route of controllers across all of them in ASP.NET MVC

I'm developing an online marketplace system in ASP.NET MVC where Users are able to create their own shops in my website with their own unique address (e.g MySite.com/UserShop) like some social medias such as Instagram which users' pages would be like "Instagram.com/YourPage".
I wanna do something similar in ASP.NET MVC. This is my stucture:
I have multiple controllers such as Home, Panel and ... and I also have a controller named ShopController which I excepted its Index action method to show users' pages (shops). It's like this:
[RoutePrefix("")]
public class ShopController : Controller
{
[Route("{shopName}")]
public ActionResult Index(string shopName)
{
return View();
}
}
When I enter the some address http://MySite/UserPage it works fine, but when I want to open the urls of my own website like http://MySite/Panel - I get exception: Multiple controller types were found that match the URL.
I think I have to set order for controllers and action methods (First my own action methods, and then ShopController), I know it can be done within single controllers by using [Order] attribute, but I don't know how to do this across all controllers.
How can I fix this and make it work properly?
Because of the desired flexibility with the custom user routes, you will end up with route conflicts as the user route is too general, which will make it also match your other site routes and cause route conflicts.
Attribute routes are checked before convention-based routes, so the shop controller will catch all requests.
Convention based routes would need to be used in this case if you want user routes to be on the root of the site. This is because the order in which routes are added to the route table are important as general routes will match before more specialised/targeted routes.
Consider mixing attribute routing and convention-based routing where the custom user routes will use convention based routes while your other controllers will use attribute routing.
[RoutePrefix("Home")]
public class HomeController : Controller {
[HttpGet]
[Route("")] //GET home
[Route("~/", Name = "default")] // Site root
public ActionResult Index() {
return View();
}
[HttpGet]
[Route("contact")] //GET home/contact
[Route("~/contact")] //GET contact
public ActionResult Contact() {
return View();
}
[HttpGet]
[Route("about")] //GET home/about
[Route("~/about")] //GET about
public ActionResult About() {
return View();
}
//...other actions
}
[RoutePrefix("Panel")]
public class PanelController : Controller {
[HttpGet]
[Route("")] //GET panel
public ActionResult Index() {
return View();
}
[HttpGet]
[Route("another-action")] //GET panel/another-action
public ActionResult Other() {
return View();
}
//...other actions
}
The attribute routes, because they are registered before convention routes will match before the user defined routes, which can use convention-based routing
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
//Attribute routing
routes.MapMvcAttributeRoutes();
//Convention based routing
//User route:
routes.MapRoute(
name: "UserRoot",
url: "{shopName}/{action}", //eg MySite.com/UserShop
defaults: new { controller = "Shop", action = "Index"}
);
//Catch-All InValid (NotFound) Routes
routes.MapRoute(
name: "NotFound",
url: "{*url}",
defaults: new { controller = "Error", action = "NotFound" }
);
}
}
The attribute routes would then need to be removed from the shop controller to avoid conflicts with the other site controllers
public class ShopController : Controller {
//eg MySite.com/UserShop
//eg MySite.com/UserShop/index
public ActionResult Index(string shopName) {
return View();
}
//eg MySite.com/UserShop/contact
public ActionResult Contact(string shopName) {
return View();
}
//eg MySite.com/UserShop/about
public ActionResult About(string shopName) {
return View();
}
//...other actions
}
So now calls to MySite.com/UserShop will be routed to the correct shop controller and still allow the site controllers to be accessed.
While it is however more labour intensive than the convention based user routes, that is the trade off to get the desired routing behavior.

Attribute Routing with Default Route

Using attribute based routing I would like to match the following routes:
~/
~/Home
~/Home/Index
to HomeController.Index, and this route:
~/Home/Error
to HomeController.Error. Here is my configuration:
[Route("[controller]/[action]")]
public class HomeController : Controller {
[HttpGet, Route(""), Route("~/")]
public IActionResult Index() {
return View();
}
[HttpGet]
public IActionResult Error() {
return View();
}
}
I have tried adding Route("[action]"), Route("Index"), and other combinations but still don't get a match for:
/Home/Index
The empty route Route("") combines with the route on controller, use Route("/") to override it instead of combining with it:
[Route("[controller]/[action]")]
public class HomeController : Controller {
[HttpGet]
[Route("/")]
[Route("/[controller]")]
public IActionResult Index() {
return View();
}
[HttpGet]
public IActionResult Error() {
return View();
}
}
Alternatively, you can remove the /[action] on the controller, which makes it a bit easier (no need to override), but then you have to define a route on every action.
I'm assuming that you intentionally want to use attribute routing rather than conventional routing, so the above answer is what you need. However, just in case the assumption is wrong, this can be easily achieved with a simple conventional routing:
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
[Route("[controller]")]
public class HomeController : Controller
{
[Route("")] // Matches 'Home'
[Route("Index")] // Matches 'Home/Index'
public IActionResult Index(){}
[Route("Error")] // Matches 'Home/Error'
public IActionResult Error(){}
}

MVC Route match based on parameter type

I have a situation where I want the ThisAction controller to look like this :
public ActionResult Index()...
public ActionResult Index(int programId)...
public ActionResult Index(string programKey)...
With the goal of a route being set up like so
www.website.com/ThisAction/ <- matches first function
www.website.com/ThisAction/123 <- matches second function
www.website.com/ThisAction/ABC <- matches third function
Is this possible to set up in the global.asx route?
You would need to use attribute routing with route constraints to easily get that flexibility.
[RoutePrefix("ThisAction")]
public class ThisActionController : Controller {
[HttpGet]
[Route("")] //Matches GET ThisAction
public ActionResult Index() {
//...
}
[HttpGet]
[Route("{programId:int}")] //Matches GET ThisAction/123
public ActionResult Index(int programId) {
//...
}
[HttpGet]
[Route("{programKey}")] //Matches GET ThisAction/ABC
public ActionResult Index(string programKey) {
//...
}
}
Make sure that attribute routeing is enabled in RouteConfig
public class RouteConfig {
public static void RegisterRoutes(RouteCollection routes) {
//...other code removed for brevity
//Attribute routes
routes.MapMvcAttributeRoutes();
//convention-based routes
//...other code removed for brevity
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
}
}
The route will work along side convention-based routing.
Just note that once you use it on a controller you have to use it on the entire controller. So the controller is either all convention-based or all attribute routing.

Is it possible to use attribute routing and convention based routing in the same controller?

I have a controller called HotelsController to insert and edit hotels.
It has the following setup (method implementation removed for simplicity):
[RoutePrefix("{member_id:int}/hotels")]
public class HotelsController : ApplicationController
{
[Route("delete/{id:int}", Name = NamedRoutes.HotelDelete)]
public ActionResult Delete(int id)
{
}
[Route("new", Name = NamedRoutes.HotelNew)]
public ActionResult New()
{
}
[HttpPost]
[ValidateInput(false)]
public ActionResult New(HotelDataEntry hotel)
{
}
[Route("edit/{id:int}", Name = NamedRoutes.HotelEdit)]
public ActionResult Edit(int id)
{
}
[HttpPost]
[ValidateInput(false)]
public ActionResult Edit(HotelDataEntry hotel)
{
}
}
As you can see the following routes are using attribute routing:
Delete
New (without parameters)
Edit (without parameters)
The following routes use no attribute routing:
New (with parameters)
Edit (with parameters)
The routing is setup in Global.asax.cs as follows:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("{resource}.aspx/{*pathInfo}");
routes.IgnoreRoute("{resource}.ashx/{*pathInfo}");
routes.IgnoreRoute("{resource}.asmx/{*pathInfo}");
routes.MapMvcAttributeRoutes();
routes.MapRoute(
Routen.Standard.ToString(),
"{member_id}/{controller}/{action}/{id}",
new { action = "browse", id = UrlParameter.Optional },
new { id = AllowedIdsRegExOptional }
);
}
Problem: Attribute routing works. I can call the Edit action with http://localhost:54868/301011/hotels/edit but the form on that page should post to the same uri and call the action that uses no attribute routing. But instead the action using the attribute based routing is called again. Why?
The form is supplied with method="post". Do you have any idea why the convention based route is not used? Thank you for your help.
Edit: I tried to add [HttpGet] in front of the attribute-routed New and Edit actions. The result is that on posting the form ASP.NET shows an error that the route is invalid. So for some reasons, the convention based routing is not working on the controller.
It seems that you cannot use both (attribute-based and convention-based) routing techniques in the same controller.
So what I did to resolve the issue is to add attribute-based routes to the two "unreachable" action methods. The route of these methods is the same as the route of the actions with the same name, but the name of the route is different (since route-names must be unique).
[RoutePrefix("{member_id:int}/hotels")]
public class HotelsController : ApplicationController
{
[Route("delete/{id:int}", Name = NamedRoutes.HotelDelete)]
public ActionResult Delete(int id)
{
}
[Route("new", Name = NamedRoutes.HotelNew)]
public ActionResult New()
{
}
[HttpPost]
[ValidateInput(false)]
[Route("new", Name = NamedRoutes.HotelNewPost)]
public ActionResult New(HotelDataEntry hotel)
{
}
[Route("edit/{id:int}", Name = NamedRoutes.HotelEdit)]
public ActionResult Edit(int id)
{
}
[HttpPost]
[ValidateInput(false)]
[Route("edit/{id:int}", Name = NamedRoutes.HotelEditPost)]
public ActionResult Edit(HotelDataEntry hotel)
{
}
}

Categories