Last active
January 14, 2020 16:22
-
-
Save IDisposable/77f11c6f7693f9d181bb to your computer and use it in GitHub Desktop.
Domain (hostname) Routing for Asp.Net MVC and WebAPI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | |
); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" } | |
); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}" | |
); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
To make a route support areas, you should implement
IRouteWithArea
.