Controller inheritance and choosing which controller has prevalence - c#

I have a baseproject and different inheriting projects. The base project has controllers I may want to occasionally inherit and override (partially).
Base project:
public virtual ActionResult Index(string filter = "", int page = 1)
Sub project:
public override ActionResult Index(string filter = "", int page = 1)
Now I changed the routeConfig, so the routing is mapped to the logic from the correct namespace.
context.MapRoute(
"Routename",
"AreaName/{controller}/{action}/{id}",
new { controller = "ControllerName", action = "Index", id = UrlParameter.Optional },
new string[] { "ProjectName.Areas.AreaName.SpecificControllers"}
);
However, I want new added routes to be taken from the specific project should they exist there. The ones which are not existant should be taken from the base project's controller. (The specific controller basically starts out empty and will only contains methods for when overriding is desirable). To try and implement this functionality, I added the other project to the routing here:
context.MapRoute(
"Routename",
"AreaName/{controller}/{action}/{id}",
new { controller = "ControllerName", action = "Index", id = UrlParameter.Optional },
new string[] { "ProjectName.Areas.AreaName.SpecificControllers", "ProjectName.Areas.AreaName.GenericControllers"}
);
However, this obviously leads to the following error:
Multiple types were found that match the controller named 'MethodName'. This can happen if the route that services this request ('CRM/{controller}/{action}/{id}') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.
The request for 'MethodName' has found the following matching controllers:
ProjectName.Areas.AreaName.SpecificControllers.ControllerName
ProjectName.Areas.AreaName.GenericControllers.ControllerName
Is there a way to implement this so that my routing will always look at the specific controller first and only at the generic controller if it cannot find the method in the specific controller?

Generally routing choose the base controller method as far as i know.
There is no direct support to resolve the issue you mentioned in this question.
There are couple of workarounds to resolve this.
Option 1 (My Favourite): Admin on base and Route on inherited controller.
To Use [Area] on the base controller and [Route] on the inherited controllers.
I personally like this approach because it keeps the code inside controller clean.
[Area("Admin")]
AdminBaseController: Controller { }
[Route("Users"))
UserAdminController : AdminBaseController { }
Url would be /Admin/Users/Action
Option 2: Use Specific Route Prefix in derived controller actions
[Route("Admin")]
AdminBaseController: Controller { }
public static string UserAdminControllerPrefix = "/Users";
UserAdminController : AdminBaseController {
[Route(UserAdminControllerPrefix + "/ActionName")]
public void ActionName() { }
}
Formed URL would be /Admin/Users/ActionName
you can choose whichever option which fits your style.
Hope this helps.
Both the approaches mentioned in this answer : ASP.NET Core MVC Attribute Routing Inheritance

Related

Restrict route to controller namespace in ASP.NET Core

I'm trying to restrict the controllers of my ASP.NET Core routes to a certain namespace.
In previous versions of ASP.NET MVC there was an overload that provided a string[] namespaces parameter when adding routes. This is missing in ASP.NET MVC 6. So after some googling, I tried playing around with something like
app.UseMvc(routes => {
var dataTokens = new RouteValueDictionary {
{
"Namespaces", new[] {"ProjectA.SomeNamespace.Controllers"}
}
};
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}",
defaults: null,
constraints: null,
dataTokens: dataTokens
);
});
but it doesn't seem to do what I want. Is there a way to restrict the routing engine to a certain namespace?
Update
I just realized it may have to do something with the fact that I'm using attribute routing on each individual controller? Does attribute routing funk up the routes defined by app.UseMvc()?
Update 2
More details:
I've two completely independent Web API projects. Incidentally, a few of the routes are identical in both (ie. ~/api/ping). These projects are independent in Production, one is an endpoint for users, one is an endpoint for administrators.
I also have unit tests, using Microsoft.AspNet.TestHost. A few of these unit tests require functionality of both of these Web API projects (ie. need "admin" endpoint to fully setup a test case for "user"). But when I reference both API projects, the TestHost gets confused because of the identical routes and it complains about "multiple matching routes":
Microsoft.AspNet.Diagnostics.DeveloperExceptionPageMiddleware: Error: An unhandled exception has occurred while executing the request
Microsoft.AspNet.Mvc.Infrastructure.AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied:
ProjectA.SomeNamespace.Controllers.PingController.Ping
ProjectB.SomeNamespace.Controllers.PingController.Ping
at Microsoft.AspNet.Mvc.Infrastructure.DefaultActionSelector.SelectAsync(RouteContext context)
at Microsoft.AspNet.Mvc.Infrastructure.MvcRouteHandler.<RouteAsync>d__6.MoveNext()
Update:
I've found solution through using ActionConstraint. You have to add custom Action Constraint attribute about duplicate actions.
Example with duplicate Index methods.
First HomeController
namespace WebApplication.Controllers
{
public class HomeController : Controller
{
[NamespaceConstraint]
public IActionResult Index()
{
return View();
}
}
}
Second HomeController
namespace WebApplication
{
public class HomeController : Controller
{
[NamespaceConstraint]
public IActionResult Index()
{
return View();
}
}
}
Configure routing
app.UseMvc(cR =>
cR.MapRoute("default", "{controller}/{action}", null, null,
new { Namespace = "WebApplication.Controllers.HomeController" }));
Action constraint
namespace WebApplication
{
public class NamespaceConstraint : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
var dataTokenNamespace = (string)routeContext.RouteData.DataTokens.FirstOrDefault(dt => dt.Key == "Namespace").Value;
var actionNamespace = ((ControllerActionDescriptor)action).MethodInfo.DeclaringType.FullName;
return dataTokenNamespace == actionNamespace;
}
}
}
First answer:
Does attribute routing funk up the routes defined by app.UseMvc()?
Attribute routing and Convention-based routing (routes.MapRoute(...) work independently. And attribute routes have advantage over convention routes.
but it doesn't seem to do what I want. Is there a way to restrict the routing engine to a certain namespace?
Answer from developers:
Instead of using a list of namespaces to group your controllers we recommend using Areas. You can attribute your controllers (regardless of which assembly they are in) with a specific Area and then create a route for that Area.
You can see a test website that shows an example of using Areas in MVC 6 here: https://github.com/aspnet/Mvc/tree/dev/test/WebSites/RoutingWebSite.
Example using Area with convention-based routing
Controller:
//Reached through /admin/users
//have to be located into: project_root/Areas/Admin/
[Area("Admin")]
public class UsersController : Controller
{
}
Configure convention-based routing:
app.UseMvc(routes =>
{
routes.MapRoute(
"areaRoute",
"{area:exists}/{controller}/{action}",
new { controller = "Home", action = "Index" });
}
Example using Area with attribute-based routing
//Reached through /admin/users
//have to be located into: project_root/Areas/Admin/
[Area("Admin")]
[Route("[area]/[controller]/[action]", Name = "[area]_[controller]_[action]")]
public class UsersController : Controller
{
}

Is it possible for two areas to share the same route and still both be reachable?

I have two areas that register routes as shown below:
"Website" area:
context.MapRoute(
"Landing Controllers",
"{controller}/{action}",
new { controller = "Home", action = "Index" }
);
"Mobile" area:
context.MapRoute(
"Mobile Defaults",
"{controller}/{action}",
new { controller = "MobileHome", action = "Index" },
new { controller = "MobileHome", action = "Index" }
);
By default, one or the other of these routes would be consistently taken when trying to go to the root URL /. But suppose we decorated our controller actions with a custom AuthorizeAttribute, where the OnAuthorization method is overridden to redirect the user to the correct controller when appropriate, as below. (Idea taken from a great blog post.)
public class MobileRedirectAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
var result = // Logic to generate the ActionResult that conditionally
// takes us to the other route goes here.
filterContext.Result = result;
}
}
I've tried using a new RedirectResult and RedirectToRouteResult, neither of which work as I'd like because of the routing conflict. Is there a way to set AuthorizationContext.Result to a value that would take us to the action that we're not currently executing? (As a last resort, I can just prefix the mobile route with some sort of namespacing variable, but I'd like to avoid going down that road just yet.)
My question can probably also be summarized by having a look at Wikipedia's desktop/mobile routing. Their two sites, http://en.m.wikipedia.org/wiki/Main_Page and http://en.wikipedia.org/wiki/Main_Page also share identical routes, but, depending on which mode you're in, return very different results.
Would it be possible to set up Wikipedia's routing in an MVC project where each environment (mobile/desktop) is registered in its own area?
A colleague led me to a promising solution using a custom IRouteConstraint.
public class HelloWorldConstraint : IRouteConstraint
{
public bool Match(HttpContextBase httpContext, Route route,
string parameterName, RouteValueDictionary values,
RouteDirection routeDirection)
{
// Determine whether to accept the route for this request.
var browser = BrowserDetector.Parse(httpContext.Request.UserAgent);
if (browser == BrowserPlatform.Mobile)
{
return true;
}
return false;
}
}
And my route declaration now looks like the below, where the route constraint is attached to a route parameter chosen at random.
context.MapRouteLowercase(
"Mobile Defaults",
"{controller}/{action}",
new { controller = "MobileHome", action = "Index" },
// In this case, it's not so much necessary to attach the constraint to
// a particular route parameter as it is important to be able to inspect
// the HttpContextBase provided by the IRouteConstraint.
new {
controller = new HelloWorldConstraint()
}
);
Not with standard MVC Routing. You can probably do with attribute routing, available in either MVC 5 or via the nuget package, AttributeRouting.

How to set up routing so that Index does show?

So I know google can penalize a site if you have the same content on multiple urls... unfortunately, in MVC this is too common i can have example.com/, example.com/Home/ and example.com/Home/Index and all three urls would take me to the same page... so how do I make sure that whenever Index is in the url, that it redirects to the same without the Index and of course the same thing with the Home
Perhaps this little library may be useful for you.
This library is not very convinient in your case, but it should work.
var route = routes.MapRoute(name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });
routes.Redirect(r => r.MapRoute("home_index", "/home/index")).To(route);
routes.Redirect(r => r.MapRoute("home", "/home")).To(route);
The way I handle this is for default pages like Index is to simply create an explicit route for only one of them. I.e. "example.com/People" would be the route for People/Index, and there would be no valid page at the url "/example.com/People/Index".
The Home example is unique in that it has potentially three different URLs. Again in this case I'd simply create a route for "example.com" for that Index action, and not support the other two urls. In other words, you would never link to the other forms of the URL, so their absence should never cause a problem.
We use a Nuget package called AttributeRouting to support this. When you specifiy a GET route for a page, it overrides the defaults for MVC.
Using AttributeRouting usually you'd map the index to [GET("")] but for the special case of Home where you also want to also support the root URL that omits the controller name , I think you'd also add an additional attribute with IsAbsoluteUrl:
public class HomeController : BaseController
{
[GET("")]
[GET("", IsAbsoluteUrl = true)]
public ActionResult Index()
{...
So I found a way to do it without any external Library...
In my RouteConfig I had to add these two routes at the top, just below the IgnoreRoute
routes.MapRoute(
"Root",
"Home/",
new { controller = "Redirect", action = "Home" }
);
routes.MapRoute(
"Index",
"{action}/Index",
new { controller = "Redirect", action = "Home" }
);
Then I had to create a new Controller called Redirect and I created a method for each of my other Controllers like this:
public class RedirectController : Controller
{
public ActionResult Home()
{
return RedirectPermanent("~/");
}
public ActionResult News()
{
return RedirectPermanent("~/News/");
}
public ActionResult ContactUs()
{
return RedirectPermanent("~/ContactUs/");
}
// A method for each of my Controllers
}
That's it, now my site looks legit. No more Home, no more Index in my URLs, this of course has the limitation of not being able to accept parameters to any of the Index methods of your Controllers though if it was really necessary, you should be able to tweak this to achieve what you want.
Just an FYI, if you want to pass an argument to your Index Action, then you can add a third route like this:
routes.MapRoute(
name: "ContactUs",
url: "ContactUs/{id}/{action}",
defaults: new { controller = "ContactUs", action = "Index", id = UrlParameter.Optional }
);
This will create a URL like this: /ContactUs/14

make routing be more generic, pagination

I have enabled pagination and routing settings in global.asax like this
routes.MapRoute("Users", "Index/{page}",
new { controller = "Users", action = "Index", page = UrlParameter.Optional },
new[] { "MyProject.Controllers" });
Now I need to apply these to every controller which sends page parameter. How can I do this?
Thank you
There are two ways you can approach it.
Add a page parameter to all your Action methods:
public ActionResult SomeAction(int? page)`
{
if (page.HasValue) ...
}
Access the RouteData directly using:
RouteData.Values["page"]
I guess you might want to consider creating a Base Controller that handles repetitive tasks related to paging.

How to name/divide controllers in MVC3 application

At the moment I have 3 controllers: Home, Summary and Detail
However, each has only one action: Index, Display and Display respectively.
This smell bad to me.
I was hoping to use the MapRoute to allow:
myapp/Home
myapp/Summary/prop1/prop2
myapp/Detail/prop1/prop2/prop3
instead of
myapp/Home
myapp/Summary/Display/prop1/prop2
myapp/Detail/Display/prop1/prop2/prop3
and thereby miss out the "Display" part...but again, this doesn't smell right. Although it works, it means manually adding links instead of using Html.ActionLink(...)
Would it be better to have Home/Index, Home/Summary and Home/Detail all in one controller?
I was hoping to provide a simple URL structure so users who know what they are doing could simply type it in as above...the "Home" part seems wasted?
I agree with #Tim that you should use a single controller. A controller is a logical grouping of actions; for example the CRUD operations for Foo. NerdDinner is a good example of this.
I disagree with the routes. You can do whatever you want with the routing; but it should be meaningful to the user. You probably just have a single catchall route similar to the one below.
routes.MapRoute("Default", //RouteName
"{controller}/{action}/{id}", //RouteUrl
new { //RouteDefaults
controller = "Home",
action = "Index",
id = UrlParameter.Optional}
)
You can have the routes you want by using a single controller.
Your desired urls:
myapp/Home
myapp/Summary/prop1/prop2
myapp/Detail/prop1/prop2/prop3
The controller setup:
public class HomeController : Controller
{
public ActionResult Index() { ... }
public ActionResult Summary() { ... }
public ActionResult Details() { ... }
}
The routing setup:
routes.MapRoute("Home-Index", //RouteName
"myapp/Home", //RouteUrl
new { //RouteDefaults
controller = "Home",
action = "Index"});
routes.MapRoute("Home-Summary", //RouteName
"myapp/Summary/prop1/prop2", //RouteUrl
new { //RouteDefaults
controller = "Home",
action = "Summary",
prop1 = UrlParameter.Optional,
prop2 = UrlParameter.Optional});
routes.MapRoute("Default", //RouteName
"{controller}/{action}/{id}", //RouteUrl
new { //RouteDefaults
controller = "Home",
action = "Index",
id = UrlParameter.Optional}
)
Now there are a few important things to note:
Routing works like a switch statement, the first url that matches is
the one it will use, that's why you have the catchall as the last
one.
The url defined in your map route can be whatever you want. It
doesn't have to match with the controller, because once you remove
the placeholders ({controller}, etc) it uses the default for
navigation. So the Home-Index route url could be myapp/foo/bar/baz
and id would still take you to the Home index action.
The placeholders work automagically. I have not been able to find a good resource explaining how the default route works.
Hope this helps.
Not sure if I totally get your question, but what about creating a base controller class that inherits from Controller, and have your shared actions there instead? That way you don't need to repeat yourself as much.
You need only one controller and inside it multiple actions..
public class HomeController : Controller
{
public ActionResult Index()
{
}
public ActionResult Summary()
{
}
public ActionResult Details()
{
}
}
In the action link
#Html.ActionLink("Details", "Details","Home");
that's enough, no need to add multiple controller..
Hope these helps..
You can pretty much go down any route you want when it comes to this, all just depends on what you want to achieve.
You can stick all the actions in the Home controller so your routes would be
myapp/Home
myapp/Home/Summary/prop1/prop2
myapp/Home/Details/prop1/prop2/prop3
In this option you have 1 controller, 3 actions, with 2 additional routes to handle the URls
It depends on what the summary and details are of though? Like if it is the summary of an order, i would prefer
myapp/Orders/Summary/prop1/prop2
myapp/Orders/Details/prop1/prop2/prop3
In this you would have your Home controller and the Index action, then an Orders controller with two actions. I would say that Summary and Details would generally suggest that you are displaying something anyway, so you would not need the "Display" part as you have in your suggestions.
If you want your URLS to be
myapp/Home
myapp/Summary/prop1/prop2
myapp/Detail/prop1/prop2/prop3
Then you make 3 controllers
HomeController
SummaryController
DetailController
Each of these will have 1 Action
public ActionResult Index() {
}
For the SUmmary and Detail controller you will just pass in some extra paramaters to the Index action

Categories

Resources