mvc custom route configuration - asp.net-mvc

Suppose that we want to transform an old version of asp.net web forms into mvc architecture.
But some users have bookmarked our urls or they have link it on other sites.
Now we don't want to loose that urls.instead we want to create a custom route that manages the old-typed urls.
This class gets the old url from user and creates a new route and redirects the user to the new page.
This is my custom route class:
public class LegacyRoute:RouteBase
{
private string[] urls;
public LegacyRoute(params string[] targetUrls)
{
urls = targetUrls;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
string requestedURL =
httpContext.Request.AppRelativeCurrentExecutionFilePath;
if (urls.Contains(requestedURL,StringComparer.OrdinalIgnoreCase))
{
result = new RouteData(this, new MvcRouteHandler());
result.Values.Add("controller", "Legacy");
result.Values.Add("action", "GetLegacyUrl");
result.Values.Add("legacyUrl", requestedURL);
}
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
and this is my RegisterRoutes function:
public static void RegisterRoutes(RouteCollection routes)
{
// routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new LegacyRoute("~/articles/windows_3.1_Overview.html", "~/old/.NET_1.0_Class_Library"));
routes.MapRoute(null, "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}
I have also created "Legacy" controller and "GetLegacyUrl" action.
But when I run the website and type
"localhost:14786/articles/windows_3.1_Overview.html"
in my browser I get the 404 Not Found error.
Where is the problem? Why I get this error?

I solved this problem (in Restful Routing) by creating a global attribute that looks at the route and then executes a redirect if it realizes the route is a "redirect route". In your case it would be a LegacyRoute.
public class RedirectFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.IsChildAction)
return;
var redirect = filterContext.RouteData.Route as RedirectRoute;
if (redirect != null)
{
var helper = new UrlHelper(filterContext.RequestContext);
var values = new RouteValueDictionary(filterContext.RequestContext.RouteData.Values);
var merged = new RouteValueDictionary(redirect.DataTokens["new_path"] as RouteValueDictionary);
// keep the values we specified, and add the other routeValues
// that we we didn't have overrides for.
foreach (var key in values.Keys.Where(key => !merged.ContainsKey(key)))
merged.Add(key, filterContext.RouteData.Values[key]);
var url = helper.RouteUrl(filterContext.RouteData.Values);
filterContext.Result = new RedirectResult(url, redirect.IsPermanent);
}
}
}

I know this is like a month old, but in your post, you did not mention the view. What view do you expect the user to arrive at? What is the view that the controller is returning as a result of GetLegacyUrl? If there is view being retured by GetLegacyUrl, then you will not get a 404. The original code you posted is fine.

Related

DateTime custom Model Binder for database multilanguage routing

I have an application with a custom database routing:
routes.Add("RouteWeb", new RouteWeb());
public override RouteData GetRouteData(HttpContextBase httpContext)
{
if (httpContext.Request.IsAjaxRequest() || httpContext.Request.Url == null) return null;
var page = FindPageFromDataBase(httpContext.Request.Url);
if (page == null) return null;
var pageResult = new RouteData(this, new MvcRouteHandler());
pageResult.Values["culture"] = page.Culture;
pageResult.Values["controller"] = page.controller;
pageResult.Values["action"] = page.action;
return pageResult;
}
As you see I get the pages from database, so the admin of the site could change the route of a page (www.site.com/page -> www.site.com/other-name) and the site works with the new name.
In the database I retrieve the culture of the page, because every page could be in different cultures, for example if you access to www.site.com/page it gets the content in English, while if you access to www.site.com/pagina it shows the content in Spanish.
This works perfect except for one detail, when the user can filter a page using a date.
#using (Ajax.BeginForm(null, null, new AjaxOptions { HttpMethod = FormMethod.Get.ToString(), InsertionMode = InsertionMode.Replace, UpdateTargetId = "content_list", Url = Url.Action("UsersItems", "Users"), OnComplete = "OnComplete" }, new { id = "formSearch" }))
{
...
#Html.DatePickerRangeFor(model => model.DateFrom, new { #class = "form-control", Value = Model.DateFrom == null ? "" : Convert.ToDateTime(Model.DateFrom).ToShortDateString()})
}
I have to use a custom ModelBinder to change the date to the correct format based on the language of the selected page.
public class NullableDateTimeBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var cultureInfo = new CultureInfo(controllerContext.RouteData.Values["culture"].ToString());
System.Threading.Thread.CurrentThread.CurrentUICulture = cultureInfo;
System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(cultureInfo.Name);
var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
return value?.ConvertTo(typeof(DateTime), CultureInfo.CurrentCulture);
}
}
My problem is
controllerContext.RouteData.Values["culture"]
is always null. When I first load the page RouteData gets the culture, but when doing the partial ajax request all the values are gone and the ModelBinder gives me an error.
I don't want to store the current culture in a session variable since I read the language from the page of the database and I have the problem only with ajax requests to load partial views.
Is there any way to pass the ModelBinder the culture of the current page?
Thanks in advance.
Rather than setting the current culture in a ModelBinder, it would be better to use an IAuthorizationFilter for this purpose. Note that IActionFilters run after the ModelBinder but IAuthorizationFilters run before the ModelBinder.
using System.Globalization;
using System.Threading;
using System.Web.Mvc;
public class CultureFilter : IAuthorizationFilter
{
private readonly string defaultCulture;
public CultureFilter(string defaultCulture)
{
this.defaultCulture = defaultCulture;
}
public void OnAuthorization(AuthorizationContext filterContext)
{
var values = filterContext.RouteData.Values;
string culture = (string)values["culture"] ?? this.defaultCulture;
CultureInfo ci = new CultureInfo(culture);
Thread.CurrentThread.CurrentCulture = ci;
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(ci.Name);
}
}
You can set the filter globally by registering it as a global filter.
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new CultureFilter(defaultCulture: "en"));
filters.Add(new HandleErrorAttribute());
}
}
Since it runs before the ModelBinder runs, there is no need to change the culture within the ModelBinder as it will pick up the ambient culture from the request. So you can eliminate the custom ModelBinder altogether.
When I first load the page RouteData gets the culture, but when doing the partial ajax request all the values are gone
This is because you are skipping your route whenever an AJAX request comes in:
if (httpContext.Request.IsAjaxRequest() || httpContext.Request.Url == null) return null;
You should eliminate that line.
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var page = FindPageFromDataBase(httpContext.Request.Url);
if (page == null) return null;
var pageResult = new RouteData(this, new MvcRouteHandler());
pageResult.Values["culture"] = page.Culture;
pageResult.Values["controller"] = page.controller;
pageResult.Values["action"] = page.action;
return pageResult;
}
Note also that you may be getting a null value for culture because it is not set in your default route. You should set a default culture for every route where it is not possible to pass the culture through the URL, and omit the default culture for every route where it is possible to pass the culture (which makes the URL segment required in order for the route to match).
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new RouteWeb());
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { culture = "en", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Related:
ASP.NET MVC 5 culture in route and url
Multiple levels in MVC custom routing

Single MVC Web Application with multiple URLs redirecting to diferent Controllers [duplicate]

Is it possible to have an ASP.NET MVC route that uses subdomain information to determine its route? For example:
user1.domain.example goes to one place
user2.domain.example goes to another?
Or, can I make it so both of these go to the same controller/action with a username parameter?
You can do it by creating a new route and adding it to the routes collection in RegisterRoutes in your global.asax. Below is a very simple example of a custom Route:
public class ExampleRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var url = httpContext.Request.Headers["HOST"];
var index = url.IndexOf(".");
if (index < 0)
return null;
var subDomain = url.Substring(0, index);
if (subDomain == "user1")
{
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values.Add("controller", "User1"); //Goes to the User1Controller class
routeData.Values.Add("action", "Index"); //Goes to the Index action on the User1Controller
return routeData;
}
if (subDomain == "user2")
{
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values.Add("controller", "User2"); //Goes to the User2Controller class
routeData.Values.Add("action", "Index"); //Goes to the Index action on the User2Controller
return routeData;
}
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//Implement your formating Url formating here
return null;
}
}
To capture the subdomain while retaining the standard MVC5 routing features, use the following SubdomainRoute class derived from Route.
Additionally, SubdomainRoute allows the subdomain optionally to be specified as a query parameter, making sub.example.com/foo/bar and example.com/foo/bar?subdomain=sub equivalent. This allows you to test before the DNS subdomains are configured. The query parameter (when in use) is propagated through new links generated by Url.Action, etc.
The query parameter also enables local debugging with Visual Studio 2013 without having to configure with netsh or run as Administrator. By default, IIS Express only binds to localhost when non-elevated; it won't bind to synonymous hostnames like sub.localtest.me.
class SubdomainRoute : Route
{
public SubdomainRoute(string url) : base(url, new MvcRouteHandler()) {}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var routeData = base.GetRouteData(httpContext);
if (routeData == null) return null; // Only look at the subdomain if this route matches in the first place.
string subdomain = httpContext.Request.Params["subdomain"]; // A subdomain specified as a query parameter takes precedence over the hostname.
if (subdomain == null) {
string host = httpContext.Request.Headers["Host"];
int index = host.IndexOf('.');
if (index >= 0)
subdomain = host.Substring(0, index);
}
if (subdomain != null)
routeData.Values["subdomain"] = subdomain;
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
object subdomainParam = requestContext.HttpContext.Request.Params["subdomain"];
if (subdomainParam != null)
values["subdomain"] = subdomainParam;
return base.GetVirtualPath(requestContext, values);
}
}
For convenience, call the following MapSubdomainRoute method from your RegisterRoutes method just as you would plain old MapRoute:
static void MapSubdomainRoute(this RouteCollection routes, string name, string url, object defaults = null, object constraints = null)
{
routes.Add(name, new SubdomainRoute(url) {
Defaults = new RouteValueDictionary(defaults),
Constraints = new RouteValueDictionary(constraints),
DataTokens = new RouteValueDictionary()
});
}
Finally, to conveniently access the subdomain (either from a true subdomain or a query parameter), it is helpful to create a Controller base class with this Subdomain property:
protected string Subdomain
{
get { return (string)Request.RequestContext.RouteData.Values["subdomain"]; }
}
This is not my work, but I had to add it on this answer.
Here is a great solution to this problem. Maartin Balliauw wrote code that creates a DomainRoute class that can be used very similarly to the normal routing.
http://blog.maartenballiauw.be/post/2009/05/20/ASPNET-MVC-Domain-Routing.aspx
Sample use would be like this...
routes.Add("DomainRoute", new DomainRoute(
"{customer}.example.com", // Domain with parameters
"{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
))
;
To capture the subdomain when using Web API, override the Action Selector to inject a subdomain query parameter. Then use the subdomain query parameter in your controllers' actions like this:
public string Get(string id, string subdomain)
This approach makes debugging convenient since you can specify the query parameter by hand when using localhost instead of the actual host name (see the standard MVC5 routing answer for details). This is the code for Action Selector:
class SubdomainActionSelector : IHttpActionSelector
{
private readonly IHttpActionSelector defaultSelector;
public SubdomainActionSelector(IHttpActionSelector defaultSelector)
{
this.defaultSelector = defaultSelector;
}
public ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDescriptor controllerDescriptor)
{
return defaultSelector.GetActionMapping(controllerDescriptor);
}
public HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
var routeValues = controllerContext.Request.GetRouteData().Values;
if (!routeValues.ContainsKey("subdomain")) {
string host = controllerContext.Request.Headers.Host;
int index = host.IndexOf('.');
if (index >= 0)
controllerContext.Request.GetRouteData().Values.Add("subdomain", host.Substring(0, index));
}
return defaultSelector.SelectAction(controllerContext);
}
}
Replace the default Action Selector by adding this to WebApiConfig.Register:
config.Services.Replace(typeof(IHttpActionSelector), new SubdomainActionSelector(config.Services.GetActionSelector()));
Yes but you have to create your own route handler.
Typically the route is not aware of the domain because the application could be deployed to any domain and the route would not care one way or another. But in your case you want to base the controller and action off the domain, so you will have to create a custom route that is aware of the domain.
I created library for subdomain routing which you can create such a route. It is working currently for a .NET Core 1.1 and .NET Framework 4.6.1 but will be updated in near future. This is how is it working:
1) Map subdomain route in Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
var hostnames = new[] { "localhost:54575" };
app.UseMvc(routes =>
{
routes.MapSubdomainRoute(
hostnames,
"SubdomainRoute",
"{username}",
"{controller}/{action}",
new { controller = "Home", action = "Index" });
)};
2) Controllers/HomeController.cs
public IActionResult Index(string username)
{
//code
}
3) That lib will also allow you to generate URLs and forms. Code:
#Html.ActionLink("User home", "Index", "Home" new { username = "user1" }, null)
Will generate User home
Generated URL will also depend on current host location and schema.
You can also use html helpers for BeginForm and UrlHelper. If you like you can also use new feature called tag helpers (FormTagHelper, AnchorTagHelper)
That lib does not have any documentation yet, but there are some tests and samples project so feel free to explore it.
In ASP.NET Core, the host is available via Request.Host.Host. If you want to allow overriding the host via a query parameter, first check Request.Query.
To cause a host query parameter to propagate into to new route-based URLs, add this code to the app.UseMvc route configuration:
routes.Routes.Add(new HostPropagationRouter(routes.DefaultHandler));
And define HostPropagationRouter like this:
/// <summary>
/// A router that propagates the request's "host" query parameter to the response.
/// </summary>
class HostPropagationRouter : IRouter
{
readonly IRouter router;
public HostPropagationRouter(IRouter router)
{
this.router = router;
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
if (context.HttpContext.Request.Query.TryGetValue("host", out var host))
context.Values["host"] = host;
return router.GetVirtualPath(context);
}
public Task RouteAsync(RouteContext context) => router.RouteAsync(context);
}
After defining a new Route handler that would look at the host passed in the URL, you can go with the idea of a base Controller that is aware of the Site it’s being accessed for. It looks like this:
public abstract class SiteController : Controller {
ISiteProvider _siteProvider;
public SiteController() {
_siteProvider = new SiteProvider();
}
public SiteController(ISiteProvider siteProvider) {
_siteProvider = siteProvider;
}
protected override void Initialize(RequestContext requestContext) {
string[] host = requestContext.HttpContext.Request.Headers["Host"].Split(':');
_siteProvider.Initialise(host[0]);
base.Initialize(requestContext);
}
protected override void OnActionExecuting(ActionExecutingContext filterContext) {
ViewData["Site"] = Site;
base.OnActionExecuting(filterContext);
}
public Site Site {
get {
return _siteProvider.GetCurrentSite();
}
}
}
ISiteProvider is a simple interface:
public interface ISiteProvider {
void Initialise(string host);
Site GetCurrentSite();
}
I refer you go to Luke Sampson Blog
If you are looking at giving MultiTenancy capabilities to your project with different domains/subdomains for each tenant, you should have a look at SaasKit:
https://github.com/saaskit/saaskit
Code examples can be seen here: http://benfoster.io/blog/saaskit-multi-tenancy-made-easy
Some examples using ASP.NET core: http://andrewlock.net/forking-the-pipeline-adding-tenant-specific-files-with-saaskit-in-asp-net-core/
EDIT:
If you do no want to use SaasKit in your ASP.NET core project you can have a look at Maarten's implementation of domain routing for MVC6: https://blog.maartenballiauw.be/post/2015/02/17/domain-routing-and-resolving-current-tenant-with-aspnet-mvc-6-aspnet-5.html
However those Gists are not maintained and need to be tweaked to work with the latest release of ASP.NET core.
Direct link to the code: https://gist.github.com/maartenba/77ca6f9cfef50efa96ec#file-domaintemplateroutebuilderextensions-cs
Few month ago I have developed an attribute that restricts methods or controllers to specific domains.
It is quite easy to use:
[IsDomain("localhost","example.com","www.example.com","*.t1.example.com")]
[HttpGet("RestrictedByHost")]
public IActionResult Test(){}
You can also apply it directly on a controller.
public class IsDomainAttribute : Attribute, Microsoft.AspNetCore.Mvc.Filters.IAuthorizationFilter
{
public IsDomainAttribute(params string[] domains)
{
Domains = domains;
}
public string[] Domains { get; }
public void OnAuthorization(AuthorizationFilterContext context)
{
var host = context.HttpContext.Request.Host.Host;
if (Domains.Contains(host))
return;
if (Domains.Any(d => d.EndsWith("*"))
&& Domains.Any(d => host.StartsWith(d.Substring(0, d.Length - 1))))
return;
if (Domains.Any(d => d.StartsWith("*"))
&& Domains.Any(d => host.EndsWith(d.Substring(1))))
return;
context.Result = new Microsoft.AspNetCore.Mvc.NotFoundResult();//.ChallengeResult
}
}
Restriction:
you may not be able to have two same routes on different methods with different filters
I mean the following may throw an exception for duplicate route:
[IsDomain("test1.example.com")]
[HttpGet("/Test")]
public IActionResult Test1(){}
[IsDomain("test2.example.com")]
[HttpGet("/Test")]
public IActionResult Test2(){}

ASP MVC - Access Controller/Action using parameter name

I am using ASP MVC4 and I would like to know if I can open a specific page as follow:
www.domain.com?Area=area1&controller=myController&Action=MyAction
instead of
www.domain.com/area1/mycontroller/MyAction
It works for me when I use the area as parameter but when I use the controller and action too as query parameters, it fails. Is there a way to make it work as Url parameters?
By default, the built-in routing ignores query string values (it does not add them to the RouteData.Values dictionary). However, there is no reason why you can't extend routing to consider them.
public class QueryStringRoute : RouteBase
{
public RouteData GetRouteData(HttpContextBase httpContext)
{
var path = httpContext.Request.Path;
if (!string.IsNullOrEmpty(path))
{
// Don't handle URLs that have a path /controller/action
return null;
}
var queryString = httpContext.Request.QueryString;
if (!queryString.HasKeys())
{
// Don't handle the route if there is no query string.
return null;
}
if (!queryString.AllKeys.Contains("controller") && !queryString.AllKeys.Contains("action"))
{
// Don't handle the case where controller and action are missing.
return null;
}
var controller = queryString["controller"];
var action = queryString["action"];
var area = queryString["area"];
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values["controller"] = controller;
routeData.Values["action"] = action;
routeData.DataTokens["area"] = area;
return routeData;
}
public VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
// NOTE: MVC automatically tacks unrecognized route values onto
// the query string. So, it is sufficient to just call your
// ActionLink normally and returning an empty string for the URL
// will send it to mysite.com/?controller=foo&action=bar
return new VirtualPathData(this, string.Empty);
}
}
Usage
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Add the query string route
routes.Add(new new QueryStringRoute());
}
}
I haven't tested this, so it may need some tweaking to get it to work right, but this is how you can do it.
Do note however, this is bad for SEO and also you will need to add additional code to handle parameters other than controller, action, and area (such as id). You could pass values to match into the route constructor, and then register the QueryStringRoute class with different parameters in order to overcome this problem.

How to set a Default Route (To an Area) in MVC

Ok this has been asked before but there is no solid solution out there. So for purpose of myself and others who may find this useful.
In MVC2 (ASP.NET) I want it so when someone navigates to the website, there is a default area specified. So navigating to my site should send you to ControllerX ActionY in AreaZ.
Using the following route in the Global.asax
routes.MapRoute(
"Area",
"",
new { area = "AreaZ", controller = "ControllerX ", action = "ActionY " }
);
Now this works as in it does try to serve the correct page. However MVC proceeds to look for the View in the root of the site and not in the Area folder.
Is there a way to resolve this?
EDIT
There is a 'Solution' and that is in ControllerX, ActionY return the full path of the view. Bit of a hack but it does work. However I'm hoping there is a better solution.
public ActionResult ActionY()
{
return View("~/Areas/AreaZ/views/ActionY.aspx");
}
Edit:
This also becomes an issue when having a HTML ActionLink of the page. If the area is not set the Action Link is output blank.
Is all of this by design or a flaw?
This one interested me, and I finally had a chance to look into it. Other folks apparently haven't understood that this is an issue with finding the view, not an issue with the routing itself - and that's probably because your question title indicates that it's about routing.
In any case, because this is a View-related issue, the only way to get what you want is to override the default view engine. Normally, when you do this, it's for the simple purpose of switching your view engine (i.e. to Spark, NHaml, etc.). In this case, it's not the View-creation logic we need to override, but the FindPartialView and FindView methods in the VirtualPathProviderViewEngine class.
You can thank your lucky stars that these methods are in fact virtual, because everything else in the VirtualPathProviderViewEngine is not even accessible - it's private, and that makes it very annoying to override the find logic because you have to basically rewrite half of the code that's already been written if you want it to play nice with the location cache and the location formats. After some digging in Reflector I finally managed to come up with a working solution.
What I've done here is to first create an abstract AreaAwareViewEngine that derives directly from VirtualPathProviderViewEngine instead of WebFormViewEngine. I did this so that if you want to create Spark views instead (or whatever), you can still use this class as the base type.
The code below is pretty long-winded, so to give you a quick summary of what it actually does: It lets you put a {2} into the location format, which corresponds to the area name, the same way {1} corresponds to the controller name. That's it! That's what we had to write all this code for:
BaseAreaAwareViewEngine.cs
public abstract class BaseAreaAwareViewEngine : VirtualPathProviderViewEngine
{
private static readonly string[] EmptyLocations = { };
public override ViewEngineResult FindView(
ControllerContext controllerContext, string viewName,
string masterName, bool useCache)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (string.IsNullOrEmpty(viewName))
{
throw new ArgumentNullException(viewName,
"Value cannot be null or empty.");
}
string area = getArea(controllerContext);
return FindAreaView(controllerContext, area, viewName,
masterName, useCache);
}
public override ViewEngineResult FindPartialView(
ControllerContext controllerContext, string partialViewName,
bool useCache)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
if (string.IsNullOrEmpty(partialViewName))
{
throw new ArgumentNullException(partialViewName,
"Value cannot be null or empty.");
}
string area = getArea(controllerContext);
return FindAreaPartialView(controllerContext, area,
partialViewName, useCache);
}
protected virtual ViewEngineResult FindAreaView(
ControllerContext controllerContext, string areaName, string viewName,
string masterName, bool useCache)
{
string controllerName =
controllerContext.RouteData.GetRequiredString("controller");
string[] searchedViewPaths;
string viewPath = GetPath(controllerContext, ViewLocationFormats,
"ViewLocationFormats", viewName, controllerName, areaName, "View",
useCache, out searchedViewPaths);
string[] searchedMasterPaths;
string masterPath = GetPath(controllerContext, MasterLocationFormats,
"MasterLocationFormats", masterName, controllerName, areaName,
"Master", useCache, out searchedMasterPaths);
if (!string.IsNullOrEmpty(viewPath) &&
(!string.IsNullOrEmpty(masterPath) ||
string.IsNullOrEmpty(masterName)))
{
return new ViewEngineResult(CreateView(controllerContext, viewPath,
masterPath), this);
}
return new ViewEngineResult(
searchedViewPaths.Union<string>(searchedMasterPaths));
}
protected virtual ViewEngineResult FindAreaPartialView(
ControllerContext controllerContext, string areaName,
string viewName, bool useCache)
{
string controllerName =
controllerContext.RouteData.GetRequiredString("controller");
string[] searchedViewPaths;
string partialViewPath = GetPath(controllerContext,
ViewLocationFormats, "PartialViewLocationFormats", viewName,
controllerName, areaName, "Partial", useCache,
out searchedViewPaths);
if (!string.IsNullOrEmpty(partialViewPath))
{
return new ViewEngineResult(CreatePartialView(controllerContext,
partialViewPath), this);
}
return new ViewEngineResult(searchedViewPaths);
}
protected string CreateCacheKey(string prefix, string name,
string controller, string area)
{
return string.Format(CultureInfo.InvariantCulture,
":ViewCacheEntry:{0}:{1}:{2}:{3}:{4}:",
base.GetType().AssemblyQualifiedName,
prefix, name, controller, area);
}
protected string GetPath(ControllerContext controllerContext,
string[] locations, string locationsPropertyName, string name,
string controllerName, string areaName, string cacheKeyPrefix,
bool useCache, out string[] searchedLocations)
{
searchedLocations = EmptyLocations;
if (string.IsNullOrEmpty(name))
{
return string.Empty;
}
if ((locations == null) || (locations.Length == 0))
{
throw new InvalidOperationException(string.Format("The property " +
"'{0}' cannot be null or empty.", locationsPropertyName));
}
bool isSpecificPath = IsSpecificPath(name);
string key = CreateCacheKey(cacheKeyPrefix, name,
isSpecificPath ? string.Empty : controllerName,
isSpecificPath ? string.Empty : areaName);
if (useCache)
{
string viewLocation = ViewLocationCache.GetViewLocation(
controllerContext.HttpContext, key);
if (viewLocation != null)
{
return viewLocation;
}
}
if (!isSpecificPath)
{
return GetPathFromGeneralName(controllerContext, locations, name,
controllerName, areaName, key, ref searchedLocations);
}
return GetPathFromSpecificName(controllerContext, name, key,
ref searchedLocations);
}
protected string GetPathFromGeneralName(ControllerContext controllerContext,
string[] locations, string name, string controllerName,
string areaName, string cacheKey, ref string[] searchedLocations)
{
string virtualPath = string.Empty;
searchedLocations = new string[locations.Length];
for (int i = 0; i < locations.Length; i++)
{
if (string.IsNullOrEmpty(areaName) && locations[i].Contains("{2}"))
{
continue;
}
string testPath = string.Format(CultureInfo.InvariantCulture,
locations[i], name, controllerName, areaName);
if (FileExists(controllerContext, testPath))
{
searchedLocations = EmptyLocations;
virtualPath = testPath;
ViewLocationCache.InsertViewLocation(
controllerContext.HttpContext, cacheKey, virtualPath);
return virtualPath;
}
searchedLocations[i] = testPath;
}
return virtualPath;
}
protected string GetPathFromSpecificName(
ControllerContext controllerContext, string name, string cacheKey,
ref string[] searchedLocations)
{
string virtualPath = name;
if (!FileExists(controllerContext, name))
{
virtualPath = string.Empty;
searchedLocations = new string[] { name };
}
ViewLocationCache.InsertViewLocation(controllerContext.HttpContext,
cacheKey, virtualPath);
return virtualPath;
}
protected string getArea(ControllerContext controllerContext)
{
// First try to get area from a RouteValue override, like one specified in the Defaults arg to a Route.
object areaO;
controllerContext.RouteData.Values.TryGetValue("area", out areaO);
// If not specified, try to get it from the Controller's namespace
if (areaO != null)
return (string)areaO;
string namespa = controllerContext.Controller.GetType().Namespace;
int areaStart = namespa.IndexOf("Areas.");
if (areaStart == -1)
return null;
areaStart += 6;
int areaEnd = namespa.IndexOf('.', areaStart + 1);
string area = namespa.Substring(areaStart, areaEnd - areaStart);
return area;
}
protected static bool IsSpecificPath(string name)
{
char ch = name[0];
if (ch != '~')
{
return (ch == '/');
}
return true;
}
}
Now as stated, this isn't a concrete engine, so you have to create that as well. This part, fortunately, is much easier, all we need to do is set the default formats and actually create the views:
AreaAwareViewEngine.cs
public class AreaAwareViewEngine : BaseAreaAwareViewEngine
{
public AreaAwareViewEngine()
{
MasterLocationFormats = new string[]
{
"~/Areas/{2}/Views/{1}/{0}.master",
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.master",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Views/{1}/{0}.master",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.master"
"~/Views/Shared/{0}.cshtml"
};
ViewLocationFormats = new string[]
{
"~/Areas/{2}/Views/{1}/{0}.aspx",
"~/Areas/{2}/Views/{1}/{0}.ascx",
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.aspx",
"~/Areas/{2}/Views/Shared/{0}.ascx",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.aspx"
"~/Views/Shared/{0}.ascx"
"~/Views/Shared/{0}.cshtml"
};
PartialViewLocationFormats = ViewLocationFormats;
}
protected override IView CreatePartialView(
ControllerContext controllerContext, string partialPath)
{
if (partialPath.EndsWith(".cshtml"))
return new System.Web.Mvc.RazorView(controllerContext, partialPath, null, false, null);
else
return new WebFormView(controllerContext, partialPath);
}
protected override IView CreateView(ControllerContext controllerContext,
string viewPath, string masterPath)
{
if (viewPath.EndsWith(".cshtml"))
return new RazorView(controllerContext, viewPath, masterPath, false, null);
else
return new WebFormView(controllerContext, viewPath, masterPath);
}
}
Note that we've added few entries to the standard ViewLocationFormats. These are the new {2} entries, where the {2} will be mapped to the area we put in the RouteData. I've left the MasterLocationFormats alone, but obviously you can change that if you want.
Now modify your global.asax to register this view engine:
Global.asax.cs
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new AreaAwareViewEngine());
}
...and register the default route:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Area",
"",
new { area = "AreaZ", controller = "Default", action = "ActionY" }
);
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
}
Now Create the AreaController we just referenced:
DefaultController.cs (in ~/Controllers/)
public class DefaultController : Controller
{
public ActionResult ActionY()
{
return View("TestView");
}
}
Obviously we need the directory structure and view to go with it - we'll keep this super simple:
TestView.aspx (in ~/Areas/AreaZ/Views/Default/ or ~/Areas/AreaZ/Views/Shared/)
<%# Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
<h2>TestView</h2>
This is a test view in AreaZ.
And that's it. Finally, we're done.
For the most part, you should be able to just take the BaseAreaAwareViewEngine and AreaAwareViewEngine and drop it into any MVC project, so even though it took a lot of code to get this done, you only have to write it once. After that, it's just a matter of editing a few lines in global.asax.cs and creating your site structure.
This is how I did it. I don't know why MapRoute() doesn't allow you to set the area, but it does return the route object so you can continue to make any additional changes you would like. I use this because I have a modular MVC site that is sold to enterprise customers and they need to be able to drop dlls into the bin folder to add new modules. I allow them to change the "HomeArea" in the AppSettings config.
var route = routes.MapRoute(
"Home_Default",
"",
new {controller = "Home", action = "index" },
new[] { "IPC.Web.Core.Controllers" }
);
route.DataTokens["area"] = area;
Edit: You can try this as well in your AreaRegistration.RegisterArea for the area you want the user going to by default. I haven't tested it but AreaRegistrationContext.MapRoute does sets route.DataTokens["area"] = this.AreaName; for you.
context.MapRoute(
"Home_Default",
"",
new {controller = "Home", action = "index" },
new[] { "IPC.Web.Core.Controllers" }
);
even it was answered already - this is the short syntax (ASP.net 3, 4, 5):
routes.MapRoute("redirect all other requests", "{*url}",
new {
controller = "UnderConstruction",
action = "Index"
}).DataTokens = new RouteValueDictionary(new { area = "Shop" });
Thanks to Aaron for pointing out that it's about locating the views, I misunderstood that.
[UPDATE] I just created a project that sends the user to an Area per default without messing with any of the code or lookup paths:
In global.asax, register as usual:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = ""} // Parameter defaults,
);
}
in Application_Start(), make sure to use the following order;
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
}
in you area registration, use
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"ShopArea_default",
"{controller}/{action}/{id}",
new { action = "Index", id = "", controller = "MyRoute" },
new { controller = "MyRoute" }
);
}
An example can be found at
http://www.emphess.net/2010/01/31/areas-routes-and-defaults-in-mvc-2-rc/
I really hope that this is what you were asking for...
////
I don't think that writing a pseudo ViewEngine is the best solution in this case. (Lacking reputation, I can't comment). The WebFormsViewEngine is Area aware and contains AreaViewLocationFormats which is defined per default as
AreaViewLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.aspx",
"~/Areas/{2}/Views/{1}/{0}.ascx",
"~/Areas/{2}/Views/Shared/{0}.aspx",
"~/Areas/{2}/Views/Shared/{0}.ascx",
};
I believe you don't adhere to this convention. You posted
public ActionResult ActionY()
{
return View("~/Areas/AreaZ/views/ActionY.aspx");
}
as a working hack, but that should be
return View("~/Areas/AreaZ/views/ControllerX/ActionY.aspx");
IF you don't want to follow the convention, however, you might want to take a short path by either deriving from the WebFormViewEngine (that is done in MvcContrib, for example) where you can set the lookup paths in the constructor, or -a little hacky- by specifying your convention like this on Application_Start:
((VirtualPathProviderViewEngine)ViewEngines.Engines[0]).AreaViewLocationFormats = ...;
This should be performed with a little more care, of course, but I think it shows the idea. These fields are public in VirtualPathProviderViewEngine in MVC 2 RC.
I guess you want user to be redirected to ~/AreaZ URL once (s)he has visited ~/ URL.
I'd achieve by means of the following code within your root HomeController.
public class HomeController
{
public ActionResult Index()
{
return RedirectToAction("ActionY", "ControllerX", new { Area = "AreaZ" });
}
}
And the following route in Global.asax.
routes.MapRoute(
"Redirection to AreaZ",
String.Empty,
new { controller = "Home ", action = "Index" }
);
First, what version of MVC2 are you using? There have been significant changes from preview2 to RC.
Assuming you use the RC, I think you route-mapping should look differently. In the AreaRegistration.cs in your area, you can register some kind of default route, e.g.
context.MapRoute(
"ShopArea_default",
"{controller}/{action}/{id}",
new { action = "Index", id = "", controller="MyRoute" }
);
The code above will send the user to the MyRouteController in our ShopArea per default.
Using an empty string as a second parameter should throw an exception, because a controller must be specified.
Of course you will have to change the default route in Global.asax so it doesn't interfere with this default route, e.g. by using a prefix for the main site.
Also see this thread and Haack's answer: MVC 2 AreaRegistration Routes Order
Hope this helps.
Adding the following to my Application_Start works for me, although I'm not sure if you have this setting in RC:
var engine = (WebFormViewEngine)ViewEngines.Engines.First();
// These additions allow me to route default requests for "/" to the home area
engine.ViewLocationFormats = new string[] {
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Areas/{1}/Views/{1}/{0}.aspx", // new
"~/Areas/{1}/Views/{1}/{0}.ascx", // new
"~/Areas/{1}/Views/{0}.aspx", // new
"~/Areas/{1}/Views/{0}.ascx", // new
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx"
};
What I did to get this to work is the following:
I created a default controller in the root/Controllers folder. I named my controller DefaultController.
In the controller I added the following code:
namespace MyNameSpace.Controllers {
public class DefaultController : Controller {
// GET: Default
public ActionResult Index() {
return RedirectToAction("Index", "ControllerName", new {area = "FolderName"});
}
} }
In my RouterConfig.cs I added the following:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new {controller = "Default", action = "Index", id = UrlParameter.Optional});
The trick behind all this is that I made a default constructor which will always be the startup controller every time my app starts. When it hits that default controller it will redirect to any controller I specify in the default Index Action. Which in my case is
www.myurl.com/FolderName/ControllerName
.
routes.MapRoute(
"Area",
"{area}/",
new { area = "AreaZ", controller = "ControlerX ", action = "ActionY " }
);
Have you tried that ?
Locating the different building blocks is done in the request life cycle. One of the first steps in the ASP.NET MVC request life cycle is mapping the requested URL to the correct controller action method. This process is referred to as routing. A default route is initialized in the Global.asax file and describes to the ASP.NET MVC framework how to handle a request. Double-clicking on the Global.asax file in the MvcApplication1 project will display the following code:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing;
namespace MvcApplication1 {
public class GlobalApplication : System.Web.HttpApplication
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
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);
}
}
}
In the Application_Start() event handler, which is fired whenever the application is compiled or the web server is restarted, a route table is registered. The default route is named Default, and responds to a URL in the form of http://www.example.com/{controller}/{action}/{id}. The variables between { and } are populated with actual values from the request URL or with the default values if no override is present in the URL. This default route will map to the Home controller and to the Index action method, according to the default routing parameters. We won't have any other action with this routing map.
By default, all the possible URLs can be mapped through this default route. It is also possible to create our own routes. For example, let's map the URL http://www.example.com/Employee/Maarten to the Employee controller, the Show action, and the firstname parameter. The following code snippet can be inserted in the Global.asax file we've just opened. Because the ASP.NET MVC framework uses the first matching route, this code snippet should be inserted above the default route; otherwise the route will never be used.
routes.MapRoute(
"EmployeeShow", // Route name
"Employee/{firstname}", // URL with parameters
new { // Parameter defaults
controller = "Employee",
action = "Show",
firstname = ""
}
);
Now, let's add the necessary components for this route. First of all, create a class named EmployeeController in the Controllers folder. You can do this by adding a new item to the project and selecting the MVC Controller Class template located under the Web | MVC category. Remove the Index action method, and replace it with a method or action named Show. This method accepts a firstname parameter and passes the data into the ViewData dictionary. This dictionary will be used by the view to display data.
The EmployeeController class will pass an Employee object to the view. This Employee class should be added in the Models folder (right-click on this folder and then select Add | Class from the context menu). Here's the code for the Employee class:
namespace MvcApplication1.Models {
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
}
Well, while creating a custom view engine can work for this, still you can have an alternative:
Decide what you need to show by default.
That something has controller and action (and Area), right?
Open that Area registration and add something like this:
public override void RegisterArea(AreaRegistrationContext context)
{
//this makes it work for the empty url (just domain) to act as current Area.
context.MapRoute(
"Area_empty",
"",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "Area controller namespace" }
);
//other routes of the area
}
Cheers!
Accepted solution to this question is, while correct in summing up how to create a custom view engine, does not answer the question correctly. Issue here is that Pino is incorrectly specifying his default route. Particularly his "area" definition is incorrect. "Area" is checked via DataTokens collection and should be added as such:
var defaultRoute = new Route("",new RouteValueDictionary(){{"controller","Default"},{"action","Index"}},null/*constraints*/,new RouteValueDictionary(){{"area","Admin"}},new MvcRouteHandler());
defaultRoute.DataTokens.Add("Namespaces","MyProject.Web.Admin.Controller");
routes.Add(defaultRoute);
Specified "area" in defaults object will be ignored. Code above creates a default route, which catches on requests to your site's root and then calls Default controller, Index action in Admin area. Please also note "Namespaces" key being added to DataTokens, this is only required if you have multiple controllers with same name. This solution is verified with Mvc2 and Mvc3 .NET 3.5/4.0
ummm, I don't know why all this programming, I think the original problem is solved easily by specifying this default route ...
routes.MapRoute("Default", "{*id}",
new { controller = "Home"
, action = "Index"
, id = UrlParameter.Optional
}
);

Is it possible to make an ASP.NET MVC route based on a subdomain?

Is it possible to have an ASP.NET MVC route that uses subdomain information to determine its route? For example:
user1.domain.example goes to one place
user2.domain.example goes to another?
Or, can I make it so both of these go to the same controller/action with a username parameter?
You can do it by creating a new route and adding it to the routes collection in RegisterRoutes in your global.asax. Below is a very simple example of a custom Route:
public class ExampleRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var url = httpContext.Request.Headers["HOST"];
var index = url.IndexOf(".");
if (index < 0)
return null;
var subDomain = url.Substring(0, index);
if (subDomain == "user1")
{
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values.Add("controller", "User1"); //Goes to the User1Controller class
routeData.Values.Add("action", "Index"); //Goes to the Index action on the User1Controller
return routeData;
}
if (subDomain == "user2")
{
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values.Add("controller", "User2"); //Goes to the User2Controller class
routeData.Values.Add("action", "Index"); //Goes to the Index action on the User2Controller
return routeData;
}
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//Implement your formating Url formating here
return null;
}
}
To capture the subdomain while retaining the standard MVC5 routing features, use the following SubdomainRoute class derived from Route.
Additionally, SubdomainRoute allows the subdomain optionally to be specified as a query parameter, making sub.example.com/foo/bar and example.com/foo/bar?subdomain=sub equivalent. This allows you to test before the DNS subdomains are configured. The query parameter (when in use) is propagated through new links generated by Url.Action, etc.
The query parameter also enables local debugging with Visual Studio 2013 without having to configure with netsh or run as Administrator. By default, IIS Express only binds to localhost when non-elevated; it won't bind to synonymous hostnames like sub.localtest.me.
class SubdomainRoute : Route
{
public SubdomainRoute(string url) : base(url, new MvcRouteHandler()) {}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var routeData = base.GetRouteData(httpContext);
if (routeData == null) return null; // Only look at the subdomain if this route matches in the first place.
string subdomain = httpContext.Request.Params["subdomain"]; // A subdomain specified as a query parameter takes precedence over the hostname.
if (subdomain == null) {
string host = httpContext.Request.Headers["Host"];
int index = host.IndexOf('.');
if (index >= 0)
subdomain = host.Substring(0, index);
}
if (subdomain != null)
routeData.Values["subdomain"] = subdomain;
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
object subdomainParam = requestContext.HttpContext.Request.Params["subdomain"];
if (subdomainParam != null)
values["subdomain"] = subdomainParam;
return base.GetVirtualPath(requestContext, values);
}
}
For convenience, call the following MapSubdomainRoute method from your RegisterRoutes method just as you would plain old MapRoute:
static void MapSubdomainRoute(this RouteCollection routes, string name, string url, object defaults = null, object constraints = null)
{
routes.Add(name, new SubdomainRoute(url) {
Defaults = new RouteValueDictionary(defaults),
Constraints = new RouteValueDictionary(constraints),
DataTokens = new RouteValueDictionary()
});
}
Finally, to conveniently access the subdomain (either from a true subdomain or a query parameter), it is helpful to create a Controller base class with this Subdomain property:
protected string Subdomain
{
get { return (string)Request.RequestContext.RouteData.Values["subdomain"]; }
}
This is not my work, but I had to add it on this answer.
Here is a great solution to this problem. Maartin Balliauw wrote code that creates a DomainRoute class that can be used very similarly to the normal routing.
http://blog.maartenballiauw.be/post/2009/05/20/ASPNET-MVC-Domain-Routing.aspx
Sample use would be like this...
routes.Add("DomainRoute", new DomainRoute(
"{customer}.example.com", // Domain with parameters
"{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
))
;
To capture the subdomain when using Web API, override the Action Selector to inject a subdomain query parameter. Then use the subdomain query parameter in your controllers' actions like this:
public string Get(string id, string subdomain)
This approach makes debugging convenient since you can specify the query parameter by hand when using localhost instead of the actual host name (see the standard MVC5 routing answer for details). This is the code for Action Selector:
class SubdomainActionSelector : IHttpActionSelector
{
private readonly IHttpActionSelector defaultSelector;
public SubdomainActionSelector(IHttpActionSelector defaultSelector)
{
this.defaultSelector = defaultSelector;
}
public ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDescriptor controllerDescriptor)
{
return defaultSelector.GetActionMapping(controllerDescriptor);
}
public HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
{
var routeValues = controllerContext.Request.GetRouteData().Values;
if (!routeValues.ContainsKey("subdomain")) {
string host = controllerContext.Request.Headers.Host;
int index = host.IndexOf('.');
if (index >= 0)
controllerContext.Request.GetRouteData().Values.Add("subdomain", host.Substring(0, index));
}
return defaultSelector.SelectAction(controllerContext);
}
}
Replace the default Action Selector by adding this to WebApiConfig.Register:
config.Services.Replace(typeof(IHttpActionSelector), new SubdomainActionSelector(config.Services.GetActionSelector()));
Yes but you have to create your own route handler.
Typically the route is not aware of the domain because the application could be deployed to any domain and the route would not care one way or another. But in your case you want to base the controller and action off the domain, so you will have to create a custom route that is aware of the domain.
I created library for subdomain routing which you can create such a route. It is working currently for a .NET Core 1.1 and .NET Framework 4.6.1 but will be updated in near future. This is how is it working:
1) Map subdomain route in Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
var hostnames = new[] { "localhost:54575" };
app.UseMvc(routes =>
{
routes.MapSubdomainRoute(
hostnames,
"SubdomainRoute",
"{username}",
"{controller}/{action}",
new { controller = "Home", action = "Index" });
)};
2) Controllers/HomeController.cs
public IActionResult Index(string username)
{
//code
}
3) That lib will also allow you to generate URLs and forms. Code:
#Html.ActionLink("User home", "Index", "Home" new { username = "user1" }, null)
Will generate User home
Generated URL will also depend on current host location and schema.
You can also use html helpers for BeginForm and UrlHelper. If you like you can also use new feature called tag helpers (FormTagHelper, AnchorTagHelper)
That lib does not have any documentation yet, but there are some tests and samples project so feel free to explore it.
In ASP.NET Core, the host is available via Request.Host.Host. If you want to allow overriding the host via a query parameter, first check Request.Query.
To cause a host query parameter to propagate into to new route-based URLs, add this code to the app.UseMvc route configuration:
routes.Routes.Add(new HostPropagationRouter(routes.DefaultHandler));
And define HostPropagationRouter like this:
/// <summary>
/// A router that propagates the request's "host" query parameter to the response.
/// </summary>
class HostPropagationRouter : IRouter
{
readonly IRouter router;
public HostPropagationRouter(IRouter router)
{
this.router = router;
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
if (context.HttpContext.Request.Query.TryGetValue("host", out var host))
context.Values["host"] = host;
return router.GetVirtualPath(context);
}
public Task RouteAsync(RouteContext context) => router.RouteAsync(context);
}
After defining a new Route handler that would look at the host passed in the URL, you can go with the idea of a base Controller that is aware of the Site it’s being accessed for. It looks like this:
public abstract class SiteController : Controller {
ISiteProvider _siteProvider;
public SiteController() {
_siteProvider = new SiteProvider();
}
public SiteController(ISiteProvider siteProvider) {
_siteProvider = siteProvider;
}
protected override void Initialize(RequestContext requestContext) {
string[] host = requestContext.HttpContext.Request.Headers["Host"].Split(':');
_siteProvider.Initialise(host[0]);
base.Initialize(requestContext);
}
protected override void OnActionExecuting(ActionExecutingContext filterContext) {
ViewData["Site"] = Site;
base.OnActionExecuting(filterContext);
}
public Site Site {
get {
return _siteProvider.GetCurrentSite();
}
}
}
ISiteProvider is a simple interface:
public interface ISiteProvider {
void Initialise(string host);
Site GetCurrentSite();
}
I refer you go to Luke Sampson Blog
If you are looking at giving MultiTenancy capabilities to your project with different domains/subdomains for each tenant, you should have a look at SaasKit:
https://github.com/saaskit/saaskit
Code examples can be seen here: http://benfoster.io/blog/saaskit-multi-tenancy-made-easy
Some examples using ASP.NET core: http://andrewlock.net/forking-the-pipeline-adding-tenant-specific-files-with-saaskit-in-asp-net-core/
EDIT:
If you do no want to use SaasKit in your ASP.NET core project you can have a look at Maarten's implementation of domain routing for MVC6: https://blog.maartenballiauw.be/post/2015/02/17/domain-routing-and-resolving-current-tenant-with-aspnet-mvc-6-aspnet-5.html
However those Gists are not maintained and need to be tweaked to work with the latest release of ASP.NET core.
Direct link to the code: https://gist.github.com/maartenba/77ca6f9cfef50efa96ec#file-domaintemplateroutebuilderextensions-cs
Few month ago I have developed an attribute that restricts methods or controllers to specific domains.
It is quite easy to use:
[IsDomain("localhost","example.com","www.example.com","*.t1.example.com")]
[HttpGet("RestrictedByHost")]
public IActionResult Test(){}
You can also apply it directly on a controller.
public class IsDomainAttribute : Attribute, Microsoft.AspNetCore.Mvc.Filters.IAuthorizationFilter
{
public IsDomainAttribute(params string[] domains)
{
Domains = domains;
}
public string[] Domains { get; }
public void OnAuthorization(AuthorizationFilterContext context)
{
var host = context.HttpContext.Request.Host.Host;
if (Domains.Contains(host))
return;
if (Domains.Any(d => d.EndsWith("*"))
&& Domains.Any(d => host.StartsWith(d.Substring(0, d.Length - 1))))
return;
if (Domains.Any(d => d.StartsWith("*"))
&& Domains.Any(d => host.EndsWith(d.Substring(1))))
return;
context.Result = new Microsoft.AspNetCore.Mvc.NotFoundResult();//.ChallengeResult
}
}
Restriction:
you may not be able to have two same routes on different methods with different filters
I mean the following may throw an exception for duplicate route:
[IsDomain("test1.example.com")]
[HttpGet("/Test")]
public IActionResult Test1(){}
[IsDomain("test2.example.com")]
[HttpGet("/Test")]
public IActionResult Test2(){}

Resources