-
-
Save IDisposable/77f11c6f7693f9d181bb to your computer and use it in GitHub Desktop.
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}" | |
); | |
} | |
} | |
} |
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.
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 theDomainRoute
andDomainHttpRoute
classes
Note that at this time MapHttpAttributeRoutes
is not supported as we cannot wedge ourselves into that route factory food chain.
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
To make a route support areas, you should implement IRouteWithArea
.
Based on ideas from http://blog.maartenballiauw.be/post/2009/05/20/ASPNET-MVC-Domain-Routing.aspx
ConcurrentDictionary
ofRegex
patterns, since in most uses you will have multiple routes with the same pattern.RemoveDomainTokens
use the list of tokens it inserted (cached away in the Items collection) to remove from the value returned inGetRouteData
instead of using another expensiveRegex
to parse it back out.GetRouteData
is called.DomainRouteCollection
andDomainAreaRegistrationContext
adapters