Skip to content

Instantly share code, notes, and snippets.

@IDisposable
Last active January 14, 2020 16:22
Show Gist options
  • Star 19 You must be signed in to star a gist
  • Fork 12 You must be signed in to fork a gist
  • Save IDisposable/77f11c6f7693f9d181bb to your computer and use it in GitHub Desktop.
Save IDisposable/77f11c6f7693f9d181bb to your computer and use it in GitHub Desktop.
Domain (hostname) Routing for Asp.Net MVC and WebAPI
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Http;
using System.Web.Http.Routing;
using System.Web.Mvc;
using System.Web.Routing;
namespace YourApplication
{
internal static class DomainRegexCache
{
// since we're often going to have the same pattern used in multiple routes, it's best to build just one regex per pattern
private static ConcurrentDictionary<string, Regex> _domainRegexes = new ConcurrentDictionary<string, Regex>();
internal static Regex CreateDomainRegex(string domain)
{
return _domainRegexes.GetOrAdd(domain, (d) =>
{
d = d.Replace("/", @"\/")
.Replace(".", @"\.")
.Replace("-", @"\-")
.Replace("{", @"(?<")
.Replace("}", @">(?:[a-zA-Z0-9_-]+))");
return new Regex("^" + d + "$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture);
});
}
}
public class DomainRoute : Route
{
private const string DomainRouteMatchKey = "DomainRoute.Match";
private const string DomainRouteInsertionsKey = "DomainRoute.Insertions";
private Regex _domainRegex;
public string Domain { get; private set; }
public DomainRoute(string domain, string url, RouteValueDictionary defaults)
: this(domain, url, defaults, new MvcRouteHandler())
{
}
public DomainRoute(string domain, string url, object defaults)
: this(domain, url, new RouteValueDictionary(defaults), new MvcRouteHandler())
{
}
public DomainRoute(string domain, string url, object defaults, IRouteHandler routeHandler)
: this(domain, url, new RouteValueDictionary(defaults), routeHandler)
{
}
public DomainRoute(string domain, string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
: base(url, defaults, routeHandler)
{
Domain = domain;
_domainRegex = DomainRegexCache.CreateDomainRegex(Domain);
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
var requestDomain = httpContext.Request.Url.Host;
var domainMatch = _domainRegex.Match(requestDomain);
if (!domainMatch.Success)
return null;
var existingMatch = httpContext.Items[DomainRouteMatchKey] as string;
if (existingMatch == null)
httpContext.Items[DomainRouteMatchKey] = Domain;
else if (existingMatch != Domain)
return null;
var data = base.GetRouteData(httpContext);
if (data == null)
return null;
var myInsertions = new HashSet<string>();
for (var i = 1; i < domainMatch.Groups.Count; i++)
{
var group = domainMatch.Groups[i];
if (group.Success)
{
var key = _domainRegex.GroupNameFromNumber(i);
if (!String.IsNullOrEmpty(key) && !String.IsNullOrEmpty(group.Value))
{
// could throw here if data.Values.ContainsKey(key) if we wanted to prevent multiple matches
data.Values[key] = group.Value;
myInsertions.Add(key);
}
}
}
httpContext.Items[DomainRouteInsertionsKey] = myInsertions;
return data;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return base.GetVirtualPath(requestContext, RemoveDomainTokens(requestContext, values));
}
private RouteValueDictionary RemoveDomainTokens(RequestContext requestContext, RouteValueDictionary values)
{
var myInsertions = requestContext.HttpContext.Items[DomainRouteInsertionsKey] as HashSet<string>;
if (myInsertions != null)
{
foreach (var key in myInsertions)
{
if (values.ContainsKey(key))
values.Remove(key);
}
}
return values;
}
}
// For MVC routes
public class DomainRouteCollection
{
private string Domain { get; set; }
private RouteCollection Routes { get; set; }
public DomainRouteCollection(string domain, RouteCollection routes)
{
Domain = domain;
Routes = routes;
}
public Route MapRoute(string name, string url)
{
return MapRoute(name, url, null, null, null);
}
public Route MapRoute(string name, string url, object defaults)
{
return MapRoute(name, url, defaults, null, null);
}
public Route MapRoute(string name, string url, string[] namespaces)
{
return MapRoute(name, url, null, null, namespaces);
}
public Route MapRoute(string name, string url, object defaults, object constraints)
{
return MapRoute(name, url, defaults, constraints, null);
}
public Route MapRoute(string name, string url, object defaults, string[] namespaces)
{
return MapRoute(name, url, defaults, null, namespaces);
}
public Route MapRoute(string name, string url, object defaults, object constraints, string[] namespaces)
{
if (name == null)
throw new ArgumentNullException("name");
if (url == null)
throw new ArgumentNullException("url");
var route = new DomainRoute(Domain, url, defaults, new MvcRouteHandler())
{
Constraints = new RouteValueDictionary(constraints),
DataTokens = new RouteValueDictionary()
};
if (namespaces != null && namespaces.Length > 0)
route.DataTokens["Namespaces"] = namespaces;
Routes.Add(name, route);
return route;
}
}
// For Areas routes
public class DomainAreaRegistrationContext
{
private AreaRegistrationContext Context { get; set; }
private DomainRouteCollection Routes { get; set; }
public DomainAreaRegistrationContext(string domain, AreaRegistrationContext context)
{
Context = context;
Routes = new DomainRouteCollection(domain, Context.Routes);
}
public Route MapRoute(string name, string url)
{
return MapRoute(name, url, null, null, null);
}
public Route MapRoute(string name, string url, object defaults)
{
return MapRoute(name, url, defaults, null, null);
}
public Route MapRoute(string name, string url, string[] namespaces)
{
return MapRoute(name, url, null, null, namespaces);
}
public Route MapRoute(string name, string url, object defaults, object constraints)
{
return MapRoute(name, url, defaults, constraints, null);
}
public Route MapRoute(string name, string url, object defaults, string[] namespaces)
{
return MapRoute(name, url, defaults, null, namespaces);
}
public Route MapRoute(string name, string url, object defaults, object constraints, string[] namespaces)
{
if (namespaces == null && Context.Namespaces != null)
namespaces = Context.Namespaces.ToArray();
var route = Routes.MapRoute(name, url, defaults, constraints, namespaces);
route.DataTokens["area"] = Context.AreaName;
// disabling the namespace lookup fallback mechanism keeps this area from accidentally picking up
// controllers belonging to other areas
bool useNamespaceFallback = (namespaces == null || namespaces.Length == 0);
route.DataTokens["UseNamespaceFallback"] = useNamespaceFallback;
return route;
}
}
// WebApi Routes
public class DomainHttpRoute : HttpRoute
{
private const string DomainRouteMatchKey = "DomainHttpRoute.Match";
private const string DomainRouteInsertionsKey = "DomainHttpRoute.Insertions";
private Regex _domainRegex;
public string Domain { get; private set; }
public DomainHttpRoute(string domain)
: this(domain, (string)null, (HttpRouteValueDictionary)null, (HttpRouteValueDictionary)null, (HttpRouteValueDictionary)null, (HttpMessageHandler)null)
{
}
public DomainHttpRoute(string domain, string routeTemplate)
: this(domain, routeTemplate, (HttpRouteValueDictionary)null, (HttpRouteValueDictionary)null, (HttpRouteValueDictionary)null, (HttpMessageHandler)null)
{
}
public DomainHttpRoute(string domain, string routeTemplate, HttpRouteValueDictionary defaults)
: this(domain, routeTemplate, defaults, (HttpRouteValueDictionary)null, (HttpRouteValueDictionary)null, (HttpMessageHandler)null)
{
}
public DomainHttpRoute(string domain, string routeTemplate, HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints)
: this(domain, routeTemplate, defaults, constraints, (HttpRouteValueDictionary)null, (HttpMessageHandler)null)
{
}
public DomainHttpRoute(string domain, string routeTemplate, HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints, HttpRouteValueDictionary dataTokens)
: this(domain, routeTemplate, defaults, constraints, dataTokens, (HttpMessageHandler)null)
{
}
public DomainHttpRoute(string domain, string routeTemplate, HttpRouteValueDictionary defaults, HttpRouteValueDictionary constraints, HttpRouteValueDictionary dataTokens, HttpMessageHandler handler)
: base(routeTemplate, defaults, constraints, dataTokens, handler)
{
Domain = domain;
_domainRegex = DomainRegexCache.CreateDomainRegex(Domain);
}
public override IHttpRouteData GetRouteData(string virtualPathRoot, HttpRequestMessage request)
{
var requestDomain = request.RequestUri.Host;
var domainMatch = _domainRegex.Match(requestDomain);
if (!domainMatch.Success)
return null;
object existingMatch;
if (!request.Properties.TryGetValue(DomainRouteMatchKey, out existingMatch))
request.Properties[DomainRouteMatchKey] = Domain;
else if (Domain != existingMatch as string)
return null;
var data = base.GetRouteData(virtualPathRoot, request);
if (data == null)
return null;
var myInsertions = new HashSet<string>();
for (var i = 1; i < domainMatch.Groups.Count; i++)
{
var group = domainMatch.Groups[i];
if (group.Success)
{
var key = _domainRegex.GroupNameFromNumber(i);
if (!String.IsNullOrEmpty(key) && !String.IsNullOrEmpty(group.Value))
{
// could throw here if data.Values.ContainsKey(key) if we wanted to prevent multiple matches
data.Values[key] = group.Value;
myInsertions.Add(key);
}
}
}
request.Properties[DomainRouteInsertionsKey] = myInsertions;
return data;
}
public override IHttpVirtualPathData GetVirtualPath(HttpRequestMessage request, IDictionary<string, object> values)
{
return base.GetVirtualPath(request, RemoveDomainTokens(request, values));
}
private IDictionary<string, object> RemoveDomainTokens(HttpRequestMessage request, IDictionary<string, object> values)
{
var myInsertions = request.Properties[DomainRouteInsertionsKey] as HashSet<string>;
if (myInsertions != null)
{
foreach (var key in myInsertions)
{
if (values.ContainsKey(key))
values.Remove(key);
}
}
return values;
}
}
// for WebApi routes
public class DomainHttpRouteCollection
{
private string Domain { get; set; }
private HttpRouteCollection Routes { get; set; }
public DomainHttpRouteCollection(string domain, HttpRouteCollection routes)
{
Domain = domain;
Routes = routes;
}
public IHttpRoute MapDomainHttpRoute(string name, string routeTemplate)
{
return MapDomainHttpRoute(name, routeTemplate, null, null, null);
}
public IHttpRoute MapDomainHttpRoute(string name, string routeTemplate, object defaults)
{
return MapDomainHttpRoute(name, routeTemplate, defaults, null, null);
}
public IHttpRoute MapDomainHttpRoute(string name, string routeTemplate, object defaults, object constraints)
{
return MapDomainHttpRoute(name, routeTemplate, defaults, constraints, null);
}
public IHttpRoute MapDomainHttpRoute(string name, string routeTemplate, object defaults, object constraints, HttpMessageHandler handler)
{
if (name == null)
throw new ArgumentNullException("name");
if (routeTemplate == null)
throw new ArgumentNullException("routeTemplate");
var route = new DomainHttpRoute(Domain, routeTemplate, new HttpRouteValueDictionary(defaults), new HttpRouteValueDictionary(constraints));
Routes.Add(name, route);
return route;
}
}
}
using System.Web.Mvc;
using YourApplication;
namespace YourApplication.Areas.Login
{
public class LoginAreaRegistration : AreaRegistration
{
public override string AreaName
{
get
{
return "Login";
}
}
public override void RegisterArea(AreaRegistrationContext context)
{
var loginAreaContext = new DomainAreaRegistrationContext("login" + MvcApplication.DnsSuffix, context);
loginAreaContext.MapRoute(
name: "Login",
url: "",
defaults: new { controller = "Login", action = "Login", id = UrlParameter.Optional }
);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace YourApplication
{
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
var clientRoutes = new DomainRouteCollection("{clientHost}" + MvcApplication.DnsSuffix, routes);
RegisterRootRoutes(clientRoutes);
}
private static void RegisterRootRoutes(DomainRouteCollection routes)
{
routes.MapRoute(
name: "Root/Index",
url: "",
defaults: new { controller = "Root", action = "Index" }
);
}
}
}
using System.Web.Http;
using System.Web.Http.ExceptionHandling;
using System.Web.Http.ModelBinding;
using YourApi;
namespace YourApi
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var clientApiRoutes = new DomainHttpRouteCollection("{clientHost}" + MvcApplication.ClientSuffix, config.Routes);
clientApiRoutes.MapDomainHttpRoute(
name: "DefaultApi",
routeTemplate: "api/v1/{controller}/{action}"
);
}
}
}
@IDisposable
Copy link
Author

Based on ideas from http://blog.maartenballiauw.be/post/2009/05/20/ASPNET-MVC-Domain-Routing.aspx

  • Made it accept hyphens in the domain segment pattern.
  • Made it require the domain segment pattern to match one or more characters per comment https://stackoverflow.com/questions/278668/is-it-possible-to-make-an-asp-net-mvc-route-based-on-a-subdomain#comment3221346_2723863
  • Made the domain patterns use a ConcurrentDictionary of Regex patterns, since in most uses you will have multiple routes with the same pattern.
  • Made RemoveDomainTokens use the list of tokens it inserted (cached away in the Items collection) to remove from the value returned in GetRouteData instead of using another expensive Regex to parse it back out.
  • Ensures that once a route is matched we don't try others that might match when GetRouteData is called.
  • Added helpers to let you do the routing more idiot proof using DomainRouteCollection and DomainAreaRegistrationContext adapters

@IDisposable
Copy link
Author

We can't do areas or namespaces with WebApi because DefaultHttpControllerSelector.cs is idiotic in implementation of the cache of HttpControllerDescriptor and filters out duplicates using ONLY the controller name (and not qualifying on anything else). This is broken and something I need to change, but alas don't have time right now. Will add a pull request on AspNetWebStack for the change when I get time.

@IDisposable
Copy link
Author

Updates:

  • Add DomainHttpRoute for WebAPI domain-based routes.
  • Now throws if the route name is null (like the base class would have)
  • Moved the Regex cache to a separate static class shared between the DomainRoute and DomainHttpRoute classes

Note that at this time MapHttpAttributeRoutes is not supported as we cannot wedge ourselves into that route factory food chain.

@BrewDawg
Copy link

BrewDawg commented Mar 6, 2015

Are there any instructions at all as how to use this, I want to use it for multi-tenant vantity urls and multi-lingual too. I don't see any examples here, just the code itself. Am I missing something?

I basically want to use it like this

www.mycompany.com/Company1
www.mycompany.com/Company2

where the Company1 and Company2 are chosen by the user when they regsiter

I'd also like to be able to add mulit-lingual like this

www.mycompany.com/en/Company2
www.mycompany.com/fr/Company2
www.mycompany.com/de/Company2

I don't see any examples showing how to do any of these though I am pretty sure your code can do it

@NightOwl888
Copy link

To make a route support areas, you should implement IRouteWithArea.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment