In the same solution, there is a ASP.NET MVC4 application Slick.App and class library Awesome.Mvc.Lib. Awesome.Mvc.Lib contains one controller class.
public class ShinnyController : Controller
{
[HttpGet]
public string Index()
{
return "Hello, from Awesome.Mvc.Lib";
}
}
If I just add the reference from Slick.App to Awesome.Mvc.Lib, run the application and point brower to /shinny, I will actually see the response "Hello, from Awesome.Mvc.Lib".
This is something I don't expect at all. All the time I thought ASP.NET MVC respects the namespaces there the controllers placed in. So, the controllers from another namespaces are not exposed, at least before I didn't ask to.
I tried to change the default route registration, to use namespaces parameter.
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new [] { "Slick.App.Controllers" }
);
Still, the ShinnyController route still match for '/shinny'.
I have a concerns this is right default behaviour. My question is, how to explicitly say which controllers are exposed and prevent default route to match controllers in separate class library?
The namespaces list on the route only gives priority to certain namespaces over the others, which are not listed :
new [] {"Namespace1", "Namespace2"}
doesn't give higher priority to Namespace1 as one would expect but just gives priority to both namespaces over the others.
This means that the namespaces in the list are first searched for controllers and then, if no match is found the rest of the available controllers with that name are used.
You can suppress the use of non prioritized controllers by doing this:
var myRoute = routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new [] { "Slick.App.Controllers" }
);
myRoute.DataTokens["UseNamespaceFallback"] = false;
You can inherit from DefaultControllerFactory like this:
public class CustomControllerFactory : DefaultControllerFactory
{
protected override Type GetControllerType(System.Web.Routing.RequestContext requestContext, string controllerName)
{
var type = base.GetControllerType(requestContext, controllerName);
if (type != null && IsIngored(type))
{
return null;
}
return type;
}
public static bool IsIngored(Type type)
{
return type.Assembly.GetCustomAttributes(typeof(IgnoreAssemblyAttribute), false).Any()
|| type.GetCustomAttributes(typeof(IgnoreControllerAttribute), false).Any();
}
}
Then some changes to Global.asax
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
ControllerBuilder.Current.SetControllerFactory(new CustomControllerFactory());
}
And here you are! Any type marked with IgnoreControllerAttribute won't be visible. You can even hide the whole assembly.
If you need some configuration based behaviour, it is not a great matter to make all necessary changes ;)
Related
I have a MVC Web Application that runs on www.domain.com and I need to configure a different URL binding for another domain www.domain2.com for the same web application.
The new domain www.domain2.com will have to return a specific Controller Action View like /Category/Cars:
routes.MapRoute(
name: "www.domain2.com",
url: "www.domain2.com",
defaults: new { controller = "Category", action = "Cars", id = UrlParameter.Optional }
);
How can I achieve this without changing the URL, so the visitor inserts the url www.domain2.com and receives the view www.domain.com/category/cars but the url remains www.domain2.com?
EDIT:
I have tried this approach but it's not working:
routes.MapRoute(
"Catchdomain2",
"{www.domain2.com}",
new { controller = "Category", action = "Cars" }
);
Domains are normally not part of routes, which is why your examples don't work. To make routes that work only on specific domains you have to customize routing.
By default, all of the routes in your route configuration will be available on all domains that can reach the web site.
The simplest solution for this is to create a custom route constraint and use it to control the domains that a specific URL will match.
DomainConstraint
public class DomainConstraint : IRouteConstraint
{
private readonly string[] domains;
public DomainConstraint(params string[] domains)
{
this.domains = domains ?? throw new ArgumentNullException(nameof(domains));
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
string domain =
#if DEBUG
// A domain specified as a query parameter takes precedence
// over the hostname (in debug compile only).
// This allows for testing without configuring IIS with a
// static IP or editing the local hosts file.
httpContext.Request.QueryString["domain"];
#else
null;
#endif
if (string.IsNullOrEmpty(domain))
domain = httpContext.Request.Headers["HOST"];
return domains.Contains(domain);
}
}
Usage
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// This ignores Category/Cars for www.domain.com and www.foo.com
routes.IgnoreRoute("Category/Cars", new { _ = new DomainConstraint("www.domain.com", "www.foo.com") });
// Matches www.domain2.com/ and sends it to CategoryController.Cars
routes.MapRoute(
name: "HomePageDomain2",
url: "",
defaults: new { controller = "Category", action = "Cars" },
constraints: new { _ = new DomainConstraint("www.domain2.com") }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
// This constraint allows the route to work either
// on "www.domain.com" or "www.domain2.com" (excluding any other domain)
constraints: new { _ = new DomainConstraint("www.domain.com", "www.domain2.com") }
);
}
}
If you fire this up in a new project in Visual Studio, you will notice it shows an error. This is because localhost:<port> is not a configured domain. However, if you navigate to:
/?domain=www.domain.com
You will see the home page.
This is because for the debug build only, it allows you to override the "local" domain name for testing purposes. You can configure your local IIS server to use a local static IP address (added to your network card) and add a local hosts file entry to test it locally without the query string parameter.
Note that when doing a "Release" build, there is no way to test using a query string parameter, as that would open up a potential security vulnerability.
If you use the URL:
/?domain=www.domain2.com
it will run the CategoryController.Cars action method (if one exists).
Note that since the Default route covers a wide range of URLs, most of the site will be available to both www.domain.com and www.domain2.com. For example, you will be able to reach the About page both at:
/Home/About?domain=www.domain.com
/Home/About?domain=www.domain2.com
You can use the IgnoreRoute extension method to block URLs that you don't want (and it accepts route constraints, so this solution will work there, too).
This solution will work if you largely want to share functionality between domains. If you would rather have 2 domains in one web site, but make them act like separate web sites, it would be easier to manage if you use an Area for each "web site" in your project by using the above route constraint for the Area routes.
public class Domain2AreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Domain2";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
name: "Domain2_default",
url: "{controller}/{action}/{id}",
defaults: new { action = "Index", id = UrlParameter.Optional },
constraints: new { _ = DomainConstraint("www.domain2.com") }
);
}
}
The above configuration would make every URL (that is 0, 1, 2, or 3 segments long) for www.domain2.com route to a controller in the Domain2 Area.
in the default action of the application make sure that the url is the one of the second domain, then return the method that needs. something like:
public ActionResult Index()
{
if (Request.Url.Host.Equals("domain2"))
return AnotherAction();
}
Agreed with the answer above.
If you want more beautiful implementation - try action filters.
Sample of action filters usage from there.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var controller = (SomeControllerBase) filterContext.Controller;
filterContext.Result = controller.RedirectToAction("index", "home");
}
Sample of getting the URL inside action filter from there.
var url = filterContext.HttpContext.Request.Url;
Put the things together and have fun :)
I know about routing in MVC. I added a new MapRoute under RegisterRoute method in RouteConfig.cs class and successfully called my function with the URL http://localhost:53363/package/PackageDetail/mypackage/5.
However, my question is do i have to add different Map Routes for every method or is there any better way ? Like in PackageController class you can see i have two methods one methods takes PackageId and PackageName and the other takes only PackageId. So do i have to register different Map Routes or not ?
RouteConfig
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Package",
url: "Package/PackageDetail/{packageName}/{packageId}",
defaults: new { controller = "Package", action = "PackageDetail", packageName = UrlParameter.Optional, packageId = UrlParameter.Optional }
);
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
PackageController.cs :
[HttpGet]
public ActionResult PackageListing(int packageId = 0)
{
return View();
}
[HttpGet]
public ActionResult PackageDetail(string packageName = "", int packageId = 0)
{
return View();
}
Despite the fact that Muhammed's answer will work, it is very repetitive, especially if you're using the same style of routes for multiple types.
There are a few things to consider before deciding upon a single approach to routing. The main one is why have both the name and ID in the route? If you want a more SEO friendly URL structure, don't bother with the ID at all.
If you have multiple products within the same type that have identical names, then there's no point in including the name as part of the URL since that won't get a user where they want to go by itself. In that event, just leave the original route.
However, if you have several different controllers (or actions) with a similar name/id structure for the routes, you'll be far better served with making your custom route more generic.
routes.MapRoute(
name: "NameAndId",
url: "{controller}/{action}/{name}/{id:int}",
defaults: new
{
controller = "Package",
action = "PackageDetail",
name = UrlParameter.Optional,
id = UrlParameter.Optional
});
Keep this above the default route, and this will redirect not just
/Package/PackageDetail/Deluxe/5
but also allow you to have stuff like this:
/Meals/Menu/Dinner/3
That may not necessarily be applicable for you in this project, but since you're learning MVC, this is a good skill to pick up. The more generic you're able to maintain your route definitions, the less you'll need to repeat it. Of course, if this is a one-time special route, there's nothing wrong with using the attributes.
Also to answer your final question, you do not need to create another custom route, because your PackageListing method will be routed through the default route that was provided when you created your project.
If you want to override default route url and generate custom url then you need to register route in route config file.
You can pass Package name and package Id as below.
http://sitename/Package/PackageListing?packageId=1
http://sitename/Package/PackageDetail?packageName=packagename&packageId=1
but if you want to generate URL as below than you need to add route in route.config file.
http://sitename/Package/PackageListing/1
http://sitename/Package/PackageDetail/packageName/1
Recently I had to update my mvc webapplication so that a basic entity of the system is displayed in the UI with a different literal.
Lets say
Previously I had: "Vessels"
Now I am asked to make it: "Ships"
The urls where mapped as by convention: mysite/{controller}/{action}/{id}
So I had urls like :
mysite/Vessels/Record/1023
mysite/Vessels/CreateVessel
I did all the renaming in the User Interface so that the titles and labels are changed from Vessel to Ship and now I a m asked to take care of the urls as well.
Now, I do not want to rename the Controller names or the ActionResult method names, because it is some heavy refactoring and because it is VERY likely, that the literals will soon be required to change again... ;-)
Is there any quick solution for me by editing just the RouteConfig, or something like that, that could do the work with a couple of lines coding?
Yeah, just register the route that will map your VesselsController actions:
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Vessels", // Route name
"Ship/{action}Ship/{id}", // URL with parameters
new { controller = "Vessel", id = "" } // Parameter defaults
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
}
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}
Also make sure to register your route before default one. Because, in other case, default route will be executed first and you will get an exception because no ShipController is defined in your application.
In my application I have an area "Member".
Outside this member area I have a folder named "Generic" which is having a controller "DataBindController".
This controller will be used in all areas. So to keep it common, i am keeping it in a separate folder outside of areas.
My route config is as follows:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
var ObjRoute = routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults:new {controller = "Login", action = "MemberLogin", id = UrlParameter.Optional},
namespaces: new string[] { "MyApp.Generic.*" }).
DataTokens = new RouteValueDictionary(new { area = "Member"});
//ObjRoute.DataTokens["UseNamespaceFallback"] = false;
}
}
Here's the project directory structure.
The Test controller inside Generic folder is as follows:
namespace MyApp.Generic
{
public class DataBindController : Controller
{
public ActionResult Test()
{
return Content("Test");
}
}
}
I am getting following error when I call the test controller using "http://localhost/MyApp/Generic/DataBind/Test"
Error in Path :/MyApp/Generic/DataBind/Test The controller for path
'/MyApp/Generic/DataBind/Test' was not found or does not implement
IController.
Please give me some idea on this issue.
your address doesn't match your defined route. Generic is just a folder. MVC doesn't care about the folder your controller is in.
the correct one should be like this :
http://localhost/MyApp/DataBind/Test
so the DataBind will be the controller and Test the Action.
Update:
your route is
{controller}/{action}/{id}
When you have Generic in your address, Asp.net matchs parts this way : Generic is the Controller, DataBind is Action and Test is the Id. of course it can't find such a thing. But when you remove Generic , every part goes to its real place.
To have Generic in the address, you should change your route to this:
Generic/{controller}/{action}/{id}
To read more about routing :
http://www.asp.net/mvc/overview/older-versions-1/controllers-and-routing/asp-net-mvc-routing-overview-cs
I have an Area named Admin with a model named CMSPage. My controller is named CMSPagesController. I would like to create a custom route so I can use simply Page instead of CMSPage, so I thought by creating the following custom route, it would work but nope:
routes.MapRoute(
"AdminPages",
"Admin/Pages/{action}/{id}",
new { controller = "CMSPages", action = "Index", id = UrlParameter.Optional }
);
Could someone please lead me in the right direction?
using System.Web.Mvc;
using System.Web.Routing;
namespace MvcApplication1
{
public class MvcApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Blog", // Route name
"Archive/{entryDate}", // URL with parameters
new { controller = "Archive", action = "Entry" } // Parameter defaults
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
);
}
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
}
}
}
The order of the routes that you add to the route table is important. Our new custom Blog route is added before the existing Default route. If you reversed the order, then the Default route always will get called instead of the custom route.
The custom Blog route matches any request that starts with /Archive/. So, it matches all of the following URLs:
/Archive/12-25-2009
/Archive/10-6-2004
/Archive/apple
The custom route maps the incoming request to a controller named Archive and invokes the Entry() action. When the Entry() method is called, the entry date is passed as a parameter named entryDate.
Oops on my part. I forgot that there was the area registration process that happens as well. The issue is that I want to access this controller from the area in which I created it (Admin). So, the custom route registration has to happen there. Not in my RouteConfig.cs (see below). Thanks for the response Neeraj, you're answer is not wrong, just not correct for my question which was in regards to an area.
using System.Web.Mvc;
namespace WebApplication1.Areas.Admin
{
public class AdminAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Admin";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
// This is where the custom route has to be registered for me to access
// it from my area.
context.MapRoute(
"Admin_pages",
"Admin/Pages/{action}/{id}",
new { action = "Index",
controller = "CMSPages",
id = UrlParameter.Optional }
);
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
}