Skip to content

Instantly share code, notes, and snippets.

@IDisposable
Last active January 14, 2020 16:22
Show Gist options
  • 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}"
);
}
}
}
@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