Search code examples
asp.net-mvcmodel-view-controllerurl-routingasp.net-mvc-routing

Routing based on subdomain to areas while keeping same URL parameters


I'm trying to route to areas based on a subdomain while having the URL not include the Area parameter.

I'd like to be able to go the following routes for example

example1.domain.com/login

example1.domain.com/landingpage

example2.domain.com/login

example2.domain.com/landingpage

I'd like to have each subdomain route to a different Area. I've tried following this post Is it possible to make an ASP.NET MVC route based on a subdomain? which led me to http://benjii.me/2015/02/subdomain-routing-in-asp-net-mvc/. But I can't seem to figure out how to not have the Area parameter in the URL.

How can I get the correct URL schema I'm looking for? {subdomain}.domain.com/{action}/{id}


Solution

  • To use a route in an area, you need to set the DataTokens["area"] parameter to the correct area in addition to doing the other subdomain routing. Here is an example:

    SubdomainRoute

    public class SubdomainRoute : Route
    {
        // Passing a null subdomain means the route will match any subdomain
        public SubdomainRoute(string subdomain, string url, IRouteHandler routeHandler) 
            : base(url, routeHandler)
        {
            this.Subdomain = subdomain;
        }
    
        public string Subdomain { get; private set; }
    
        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            // A subdomain specified as a query parameter takes precedence over the hostname.
            string subdomain = httpContext.Request.Params["subdomain"]; 
            if (subdomain == null)
            {
                string host = httpContext.Request.Headers["Host"];
                int index = host.IndexOf('.');
                if (index >= 0)
                    subdomain = host.Substring(0, index);
            }
            // Check if the subdomain matches this route
            if (this.Subdomain != null && !this.Subdomain.Equals(subdomain, StringComparison.OrdinalIgnoreCase))
                return null;
    
            var routeData = base.GetRouteData(httpContext);
            if (routeData == null) return null; // The route doesn't match - exit early
    
            // Store the subdomain as a datatoken in case it is needed elsewhere in the app
            routeData.DataTokens["subdomain"] = subdomain;
    
            return routeData;
        }
    
        public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
        {
            // Read the current query string and cascade it to the current URL only if it exists
            object subdomainParam = requestContext.HttpContext.Request.Params["subdomain"];
            if (subdomainParam != null)
                values["subdomain"] = subdomainParam;
            return base.GetVirtualPath(requestContext, values);
        }
    }
    

    RouteCollectionExtensions

    These extension methods allow you to register routes in the non-area part of your application to work with subdomains.

    public static class RouteCollectionExtensions
    {
        public static SubdomainRoute MapSubdomainRoute(
            this RouteCollection routes, 
            string subdomain, 
            string url, 
            object defaults = null, 
            object constraints = null, 
            string[] namespaces = null)
        {
            return MapSubdomainRoute(routes, null, subdomain, url, defaults, constraints, namespaces);
        }
    
        public static SubdomainRoute MapSubdomainRoute(
            this RouteCollection routes,
            string name,
            string subdomain, 
            string url, 
            object defaults = null, 
            object constraints = null, 
            string[] namespaces = null)
        {
            var route = new SubdomainRoute(subdomain, url, new MvcRouteHandler())
            {
                Defaults = new RouteValueDictionary(defaults),
                Constraints = new RouteValueDictionary(constraints),
                DataTokens = new RouteValueDictionary()
            };
            if ((namespaces != null) && (namespaces.Length > 0))
            {
                route.DataTokens["Namespaces"] = namespaces;
            }
            routes.Add(name, route);
            return route;
        }
    }
    

    AreaRegistrationContextExtensions

    These extension methods allow you to register area routes to work with subdomains.

    public static class AreaRegistrationContextExtensions
    {
        public static SubdomainRoute MapSubdomainRoute(
            this AreaRegistrationContext context, 
            string url, 
            object defaults = null, 
            object constraints = null, 
            string[] namespaces = null)
        {
            return MapSubdomainRoute(context, null, url, defaults, constraints, namespaces);
        }
    
        public static SubdomainRoute MapSubdomainRoute(
            this AreaRegistrationContext context, 
            string name, 
            string url, 
            object defaults = null, 
            object constraints = null, 
            string[] namespaces = null)
        {
            if ((namespaces == null) && (context.Namespaces != null))
            {
                namespaces = context.Namespaces.ToArray<string>();
            }
            var route = context.Routes.MapSubdomainRoute(name, 
                context.AreaName, url, defaults, constraints, namespaces);
            bool flag = (namespaces == null) || (namespaces.Length == 0);
            route.DataTokens["UseNamespaceFallback"] = flag;
            route.DataTokens["area"] = context.AreaName;
            return route;
        }
    }
    

    Usage

    To get the URL pattern {subdomain}.domain.com/{action}/{id} to use a specific specific area, you just need to register it as part of the AreaRegistration.

    public class AppleAreaRegistration : AreaRegistration
    {
        public override string AreaName
        {
            get
            {
                return "Apple";
            }
        }
    
        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.MapSubdomainRoute(
                url: "{action}/{id}", 
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });
        }
    }
    

    NOTE: Your example URL has a limitation that you can only use a single controller per area. controller is a required route value, so you either need to supply it in the URL ({controller}/{action}/{id}) or default it as the above example - the latter case means you can only have 1 controller.

    Of course, you will also need to setup the DNS server to use subdomains.