Skip to content

Instantly share code, notes, and snippets.

@peters
Forked from IDisposable/DomainRoute.cs
Created October 22, 2017 00:43

Revisions

  1. @IDisposable IDisposable revised this gist May 27, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion DomainRoute.cs
    Original file line number Diff line number Diff line change
    @@ -10,7 +10,7 @@
    using System.Web.Mvc;
    using System.Web.Routing;

    namespace Alerts.Helpers
    namespace YourApplication
    {
    internal static class DomainRegexCache
    {
  2. @IDisposable IDisposable revised this gist Nov 20, 2014. 2 changed files with 193 additions and 19 deletions.
    192 changes: 173 additions & 19 deletions DomainRoute.cs
    Original file line number Diff line number Diff line change
    @@ -2,21 +2,41 @@
    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 Alerts.Helpers
    {
    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";

    // 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>();

    private Regex _domainRegex;
    public string Domain { get; private set; }

    @@ -39,7 +59,7 @@ public DomainRoute(string domain, string url, RouteValueDictionary defaults, IRo
    : base(url, defaults, routeHandler)
    {
    Domain = domain;
    _domainRegex = CreateDomainRegex(Domain);
    _domainRegex = DomainRegexCache.CreateDomainRegex(Domain);
    }

    public override RouteData GetRouteData(HttpContextBase httpContext)
    @@ -87,20 +107,6 @@ public override VirtualPathData GetVirtualPath(RequestContext requestContext, Ro
    return base.GetVirtualPath(requestContext, RemoveDomainTokens(requestContext, values));
    }

    private 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);
    });
    }

    private RouteValueDictionary RemoveDomainTokens(RequestContext requestContext, RouteValueDictionary values)
    {
    var myInsertions = requestContext.HttpContext.Items[DomainRouteInsertionsKey] as HashSet<string>;
    @@ -118,6 +124,7 @@ private RouteValueDictionary RemoveDomainTokens(RequestContext requestContext, R
    }
    }

    // For MVC routes
    public class DomainRouteCollection
    {
    private string Domain { get; set; }
    @@ -156,6 +163,8 @@ public Route MapRoute(string name, string url, object defaults, string[] namespa

    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");

    @@ -174,6 +183,7 @@ public Route MapRoute(string name, string url, object defaults, object constrain
    }
    }

    // For Areas routes
    public class DomainAreaRegistrationContext
    {
    private AreaRegistrationContext Context { get; set; }
    @@ -226,4 +236,148 @@ public Route MapRoute(string name, string url, object defaults, object constrain
    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;
    }
    }
    }
    20 changes: 20 additions & 0 deletions ExampleWebApiRegistration.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,20 @@
    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}"
    );
    }
    }
    }
  3. @IDisposable IDisposable revised this gist Nov 19, 2014. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion DomainRoute.cs
    Original file line number Diff line number Diff line change
    @@ -95,7 +95,7 @@ private static Regex CreateDomainRegex(string domain)
    .Replace(".", @"\.")
    .Replace("-", @"\-")
    .Replace("{", @"(?<")
    .Replace("}", @">(?:[a-zA-Z0-9_-]*))");
    .Replace("}", @">(?:[a-zA-Z0-9_-]+))");

    return new Regex("^" + d + "$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture);
    });
  4. @IDisposable IDisposable revised this gist Nov 19, 2014. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions ExampleAreaRegistration.cs
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,7 @@
    using System.Web.Mvc;
    using Alerts.Helpers;
    using YourApplication;

    namespace Alerts.Admin.Areas.Login
    namespace YourApplication.Areas.Login
    {
    public class LoginAreaRegistration : AreaRegistration
    {
  5. @IDisposable IDisposable created this gist Nov 19, 2014.
    229 changes: 229 additions & 0 deletions DomainRoute.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,229 @@
    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text.RegularExpressions;
    using System.Web;
    using System.Web.Mvc;
    using System.Web.Routing;

    namespace Alerts.Helpers
    {
    public class DomainRoute : Route
    {
    private const string DomainRouteMatchKey = "DomainRoute.Match";
    private const string DomainRouteInsertionsKey = "DomainRoute.Insertions";

    // 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>();

    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 = 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 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);
    });
    }

    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;
    }
    }

    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 (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;
    }
    }

    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;
    }
    }
    }
    27 changes: 27 additions & 0 deletions ExampleAreaRegistration.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,27 @@
    using System.Web.Mvc;
    using Alerts.Helpers;

    namespace Alerts.Admin.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 }
    );
    }
    }
    }
    30 changes: 30 additions & 0 deletions ExampleRouteConfig.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,30 @@
    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" }
    );
    }
    }
    }