Related
I'm trying to route to areas based on a subdomain while having the URL not include the Area parameter.
I'd like to be able to go the following routes for example
example1.domain.com/login
example1.domain.com/landingpage
example2.domain.com/login
example2.domain.com/landingpage
I'd like to have each subdomain route to a different Area. I've tried following this post Is it possible to make an ASP.NET MVC route based on a subdomain? which led me to http://benjii.me/2015/02/subdomain-routing-in-asp-net-mvc/. But I can't seem to figure out how to not have the Area parameter in the URL.
How can I get the correct URL schema I'm looking for? {subdomain}.domain.com/{action}/{id}
To use a route in an area, you need to set the DataTokens["area"] parameter to the correct area in addition to doing the other subdomain routing. Here is an example:
SubdomainRoute
public class SubdomainRoute : Route
{
// Passing a null subdomain means the route will match any subdomain
public SubdomainRoute(string subdomain, string url, IRouteHandler routeHandler)
: base(url, routeHandler)
{
this.Subdomain = subdomain;
}
public string Subdomain { get; private set; }
public override RouteData GetRouteData(HttpContextBase httpContext)
{
// A subdomain specified as a query parameter takes precedence over the hostname.
string subdomain = httpContext.Request.Params["subdomain"];
if (subdomain == null)
{
string host = httpContext.Request.Headers["Host"];
int index = host.IndexOf('.');
if (index >= 0)
subdomain = host.Substring(0, index);
}
// Check if the subdomain matches this route
if (this.Subdomain != null && !this.Subdomain.Equals(subdomain, StringComparison.OrdinalIgnoreCase))
return null;
var routeData = base.GetRouteData(httpContext);
if (routeData == null) return null; // The route doesn't match - exit early
// Store the subdomain as a datatoken in case it is needed elsewhere in the app
routeData.DataTokens["subdomain"] = subdomain;
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
// Read the current query string and cascade it to the current URL only if it exists
object subdomainParam = requestContext.HttpContext.Request.Params["subdomain"];
if (subdomainParam != null)
values["subdomain"] = subdomainParam;
return base.GetVirtualPath(requestContext, values);
}
}
RouteCollectionExtensions
These extension methods allow you to register routes in the non-area part of your application to work with subdomains.
public static class RouteCollectionExtensions
{
public static SubdomainRoute MapSubdomainRoute(
this RouteCollection routes,
string subdomain,
string url,
object defaults = null,
object constraints = null,
string[] namespaces = null)
{
return MapSubdomainRoute(routes, null, subdomain, url, defaults, constraints, namespaces);
}
public static SubdomainRoute MapSubdomainRoute(
this RouteCollection routes,
string name,
string subdomain,
string url,
object defaults = null,
object constraints = null,
string[] namespaces = null)
{
var route = new SubdomainRoute(subdomain, url, new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(defaults),
Constraints = new RouteValueDictionary(constraints),
DataTokens = new RouteValueDictionary()
};
if ((namespaces != null) && (namespaces.Length > 0))
{
route.DataTokens["Namespaces"] = namespaces;
}
routes.Add(name, route);
return route;
}
}
AreaRegistrationContextExtensions
These extension methods allow you to register area routes to work with subdomains.
public static class AreaRegistrationContextExtensions
{
public static SubdomainRoute MapSubdomainRoute(
this AreaRegistrationContext context,
string url,
object defaults = null,
object constraints = null,
string[] namespaces = null)
{
return MapSubdomainRoute(context, null, url, defaults, constraints, namespaces);
}
public static SubdomainRoute MapSubdomainRoute(
this AreaRegistrationContext context,
string name,
string url,
object defaults = null,
object constraints = null,
string[] namespaces = null)
{
if ((namespaces == null) && (context.Namespaces != null))
{
namespaces = context.Namespaces.ToArray<string>();
}
var route = context.Routes.MapSubdomainRoute(name,
context.AreaName, url, defaults, constraints, namespaces);
bool flag = (namespaces == null) || (namespaces.Length == 0);
route.DataTokens["UseNamespaceFallback"] = flag;
route.DataTokens["area"] = context.AreaName;
return route;
}
}
Usage
To get the URL pattern {subdomain}.domain.com/{action}/{id} to use a specific specific area, you just need to register it as part of the AreaRegistration.
public class AppleAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Apple";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapSubdomainRoute(
url: "{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}
}
NOTE: Your example URL has a limitation that you can only use a single controller per area. controller is a required route value, so you either need to supply it in the URL ({controller}/{action}/{id}) or default it as the above example - the latter case means you can only have 1 controller.
Of course, you will also need to setup the DNS server to use subdomains.
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(){}
I have hosting and domain like that:
www.EXAMPLE.com
I've created few subdomains like that:
www.PAGE1.EXAMPLE.com
www.PAGE2.EXAMPLE.com
www.PAGE3.EXAMPLE.com
... etc...
All of these subdomains point to one and the same ASP.NET MVC 5 Application.
I want to make system which will load data depending of subdomain.
Example:
I have Article object which could be a Auto Review or Game review or Book Review etc...
I would like to www.auto.example.com load data where type of article is Auto, to www.book.example.com I would like to load data with type Book etc.
There will be many types of the pages.
What is best practise to do that?
The top level domain www.example.com should display something else. It would be main page for the others.
You can do this by writing a custom Route. Here's how (adapted from Is it possible to make an ASP.NET MVC route based on a subdomain?)
public class SubdomainRoute : RouteBase
{
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var host = httpContext.Request.Url.Host;
var index = host.IndexOf(".");
string[] segments = httpContext.Request.Url.PathAndQuery.Split('/');
if (index < 0)
return null;
var subdomain = host.Substring(0, index);
string controller = (segments.Length > 0) ? segments[0] : "Home";
string action = (segments.Length > 1) ? segments[1] : "Index";
var routeData = new RouteData(this, new MvcRouteHandler());
routeData.Values.Add("controller", controller); //Goes to the relevant Controller class
routeData.Values.Add("action", action); //Goes to the relevant action method on the specified Controller
routeData.Values.Add("subdomain", subdomain); //pass subdomain as argument to action method
return routeData;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
//Implement your formating Url formating here
return null;
}
}
Add to the route table in Global.asax.cs like this:
routes.Add(new SubdomainRoute());
And your controller method:
public ActionResult Index(string subdomain)
{
//Query your database for the relevant articles based on subdomain
var viewmodel = MyRepository.GetArticles(subdomain);
Return View(viewmodel);
}
This is something I have wanted to do with ASP.NET MVC for a long time, but... This is not a concern that ASP.NET MVC is responsible for. This is a server concern (IIS). What you need to do is allow for wildcard subdomains on your IIS server and point them to your one application.
Then you can do something like this with the HttpContext:
HttpContext.Current.Request.Url.Host // user1.yourwebsite.com
Then you just need to parse that and push it into your ASP.NET MVC app anyway you see fit:
Push it into Session
Update the current route data and push a value in
Etc....
The choice is really up to you.
Note: The downside here is that this makes local development increasingly difficult, so you might want to mock up a way to fake a subdomain in your application.
I tried Paul Taylor answer above is pretty good but that didn't work entirely for me.
I use this implementation of Route class.
Add your custom domain into C:/Windows/System32/drivers/etc/hosts file
127.0.0.1 subdomain.localhost.com
DomainData.cs
public class DomainData
{
public string Protocol { get; set; }
public string HostName { get; set; }
public string Fragment { get; set; }
}
DomainRoute.cs
public class DomainRoute : Route
{
private Regex domainRegex;
private Regex pathRegex;
public string Domain { get; set; }
public DomainRoute(string domain, string url, RouteValueDictionary defaults)
: base(url, defaults, new MvcRouteHandler())
{
Domain = domain;
}
public DomainRoute(string domain, string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
: base(url, defaults, routeHandler)
{
Domain = domain;
}
public DomainRoute(string domain, string url, object defaults)
: base(url, new RouteValueDictionary(defaults), new MvcRouteHandler())
{
Domain = domain;
}
public DomainRoute(string domain, string url, object defaults, IRouteHandler routeHandler)
: base(url, new RouteValueDictionary(defaults), routeHandler)
{
Domain = domain;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
// Build regex
domainRegex = CreateRegex(Domain);
pathRegex = CreateRegex(Url);
// Request information
string requestDomain = httpContext.Request.Headers["host"];
if (!string.IsNullOrEmpty(requestDomain))
{
if (requestDomain.IndexOf(":") > 0)
{
requestDomain = requestDomain.Substring(0, requestDomain.IndexOf(":"));
}
}
else
{
requestDomain = httpContext.Request.Url.Host;
}
string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) +
httpContext.Request.PathInfo;
// Match domain and route
Match domainMatch = domainRegex.Match(requestDomain);
Match pathMatch = pathRegex.Match(requestPath);
// Route data
RouteData data = null;
if (domainMatch.Success && pathMatch.Success && requestDomain.ToLower() != "tg.local" &&
requestDomain.ToLower() != "tg.terrasynq.net" && requestDomain.ToLower() != "www.townsgossip.com" &&
requestDomain.ToLower() != "townsgossip.com")
{
data = new RouteData(this, RouteHandler);
// Add defaults first
if (Defaults != null)
{
foreach (KeyValuePair<string, object> item in Defaults)
{
data.Values[item.Key] = item.Value;
}
}
// Iterate matching domain groups
for (int i = 1; i < domainMatch.Groups.Count; i++)
{
Group group = domainMatch.Groups[i];
if (group.Success)
{
string key = domainRegex.GroupNameFromNumber(i);
if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
{
if (!string.IsNullOrEmpty(group.Value))
{
data.Values[key] = group.Value;
}
}
}
}
// Iterate matching path groups
for (int i = 1; i < pathMatch.Groups.Count; i++)
{
Group group = pathMatch.Groups[i];
if (group.Success)
{
string key = pathRegex.GroupNameFromNumber(i);
if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
{
if (!string.IsNullOrEmpty(group.Value))
{
data.Values[key] = group.Value;
}
}
}
}
}
return data;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return base.GetVirtualPath(requestContext, RemoveDomainTokens(values));
}
public DomainData GetDomainData(RequestContext requestContext, RouteValueDictionary values)
{
// Build hostname
string hostname = Domain;
foreach (KeyValuePair<string, object> pair in values)
{
hostname = hostname.Replace("{" + pair.Key + "}", pair.Value.ToString());
}
// Return domain data
return new DomainData
{
Protocol = "http",
HostName = hostname,
Fragment = ""
};
}
private Regex CreateRegex(string source)
{
// Perform replacements
source = source.Replace("/", #"\/?");
source = source.Replace(".", #"\.?");
source = source.Replace("-", #"\-?");
source = source.Replace("{", #"(?<");
source = source.Replace("}", #">([a-zA-Z0-9_\-]*))");
return new Regex("^" + source + "$");
}
private RouteValueDictionary RemoveDomainTokens(RouteValueDictionary values)
{
var tokenRegex =
new Regex(
#"({[a-zA-Z0-9_\-]*})*\.?\/?({[a-zA-Z0-9_\-]*})*\.?\/?({[a-zA-Z0-9_\-]*})*\.?\/?({[a-zA-Z0-9_\-]*})*\.?\/?({[a-zA-Z0-9_\-]*})*\.?\/?({[a-zA-Z0-9_\-]*})*\.?\/?({[a-zA-Z0-9_\-]*})*\.?\/?({[a-zA-Z0-9_\-]*})*\.?\/?({[a-zA-Z0-9_\-]*})*\.?\/?({[a-zA-Z0-9_\-]*})*\.?\/?({[a-zA-Z0-9_\-]*})*\.?\/?({[a-zA-Z0-9_\-]*})*\.?\/?");
Match tokenMatch = tokenRegex.Match(Domain);
for (int i = 0; i < tokenMatch.Groups.Count; i++)
{
Group group = tokenMatch.Groups[i];
if (group.Success)
{
string key = group.Value.Replace("{", "").Replace("}", "");
if (values.ContainsKey(key))
values.Remove(key);
}
}
return values;
}
}
Reference: http://www.howtobuildsoftware.com/index.php/how-do/UaR/aspnet-mvc-5-domain-routing-in-mvc5
I have an ASP.NET MVC 3 application where users can post suggestions along the lines of "bla bla would be better if yada yada yada".
For the suggestion detail page I have defined a nice SEO friendly route as follows:
routes.MapRoute(null, "suggestion/{id}/{it}/would-be-better-if-{if}",
new { controller = "suggestion", action = "details" });
As you can see I want the "would be better if" part to be fixed.
This route works perfectly for any old suggestion and generates links like suggestion/5/this-site/would-be-better-if-it-had-a-iphone-application, and clicking on the link actually requests the appropriate detail page.
A friend of mine, who ironically happens to be a tester, has managed to, involuntarily, post a suggestion that actually breaks the route: "This site would be better if 'would be better if' was always aligned in the middle".
The link generated for this suggestion is
/suggestion/84/this-site/would-be-better-if-would-be-better-if-was-always-alligned-in-the-middle.
I have tried Phil Haack's Routing Debugger and have confirmed that the route will actually work up until suggestion/84/this-site/would-be-better-if-would-be-better-if-, so the second "would-be-better-if" is actually accepted; adding anything after that will actually cause the url not to match any route (thanks to Omar -see comments- for help).
Please bear in mind that I really don't want to change the route definition since I think it is as good as I can manage for this case, SEO-wise.
So, how come having text equal to the fixed part of the route prevents the link from matching the route? why is the route breaking?
I am actually rahter more interested in the why, as I believe understanding the why will lead to a solution or at least a proper understanding of a rather interesting problem.
I'm not sure why it behaves this way, but you can use something like this:
public interface IRouteRule
{
object ProcessIncoming(object value);
object ProcessOutgoing(object value);
}
public class StartsWithRouteRule : IRouteRule
{
public StartsWithRouteRule(string value)
{
Value = value;
}
public string Value { get; protected set; }
public object ProcessIncoming(object value)
{
var result = value as string;
if (result == null)
return null;
if (!result.StartsWith(Value))
return null;
return result.Substring(Value.Length);
}
public object ProcessOutgoing(object value)
{
var result = value as string;
if (result == null)
return null;
return Value + result;
}
}
public class ComplexRoute : Route
{
public ComplexRoute(string url, object defaults, object rules)
: this(url, new RouteValueDictionary(defaults), rules)
{ }
public ComplexRoute(string url, RouteValueDictionary defaults, object rules)
: base(url, defaults, new MvcRouteHandler())
{
Rules = new Dictionary<string, IRouteRule>();
foreach (var pair in new RouteValueDictionary(rules))
Rules.Add(pair.Key, (IRouteRule)pair.Value);
}
public Dictionary<string, IRouteRule> Rules { get; protected set; }
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var result = base.GetRouteData(httpContext);
if (result == null)
return null;
foreach (var pair in Rules)
{
var currentValue = result.Values[pair.Key];
if (currentValue == null)
return null;
var value = pair.Value.ProcessIncoming(currentValue);
if (value == null)
return null;
result.Values[pair.Key] = value;
}
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
values = new RouteValueDictionary(values);
foreach (var pair in Rules)
{
var currentValue = values[pair.Key];
if (currentValue == null)
return null;
var value = pair.Value.ProcessOutgoing(currentValue);
if (value == null)
return null;
values[pair.Key] = value;
}
return base.GetVirtualPath(requestContext, values);
}
}
Usage:
routes.Add(new ComplexRoute(
"suggestion/{id}/{it}/{if}",
new { controller = "suggestion", action = "details" },
new { #if = new StartsWithRouteRule("would-be-better-if-") }));
This looks like a dupe of ASP.NET routing: Literal sub-segment between tokens, and route values with a character from the literal sub-segment which is a much simpler version of the bug. I'd recommend closing this one in favor of that one.
I answered that question.
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(){}