Logo

Maarten Balliauw {blog}

ASP.NET, ASP.NET MVC, Azure, PHP, OpenXML, VSTS, ...

About the author

Maarten Balliauw is currently employed as .NET Technical Consultant at RealDolmen. His interests are mainly web applications developed in ASP.NET (C#) or PHP and the Windows Azure cloud platform.
More about me More about me
Send mail E-mail me


ASP.NET MVC Quickly Subscribe to my RSS feed Follow me on Twitter! View Maarten Balliauw's profile on LinkedIn
View Maarten Balliauw's MVP profile

Search

Latest Twitter

    Follow me on Twitter...

    My projects

    Disclaimer

    The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

    © Copyright Maarten Balliauw 2010

    ASP.NET MVC Domain Routing

    Routing Ever since the release of ASP.NET MVC and its routing engine (System.Web.Routing), Microsoft has been trying to convince us that you have full control over your URL and routing. This is true to a certain extent: as long as it’s related to your application path, everything works out nicely. If you need to take care of data tokens in your (sub)domain, you’re screwed by default.

    Earlier this week, Juliën Hanssens did a blog post on his approach to subdomain routing. While this is a good a approach, it has some drawbacks:

    • All routing logic is hard-coded: if you want to add a new possible route, you’ll have to code for it.
    • The VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) method is not implemented, resulting in “strange” urls when using HtmlHelper ActionLink helpers. Think of http://live.localhost/Home/Index/?liveMode=false where you would have just wanted http://develop.localhost/Home/Index.

    Unfortunately, the ASP.NET MVC infrastructure is based around this VirtualPathData class. That’s right: only tokens in the URL’s path are used for routing… Check my entry on the ASP.NET MVC forums on that one.

    Now for a solution… Here are some scenarios we would like to support:

    • Scenario 1: Application is multilingual, where www.nl-be.example.com should map to a route like “www.{language}-{culture}.example.com”.
    • Scenario 2: Application is multi-tenant, where www.acmecompany.example.com should map to a route like “www.{clientname}.example.com”.
    • Scenario 3: Application is using subdomains for controller mapping: www.store.example.com maps to "www.{controller}.example.com/{action}...."

    Sit back, have a deep breath and prepare for some serious ASP.NET MVC plumbing…

    kick it on DotNetKicks.com

    Defining routes

    Here are some sample route definitions we want to support. An example where we do not want to specify the controller anywhere, as long as we are on home.example.com:

    routes.Add("DomainRoute", new DomainRoute(
        "home.example.com", // Domain with parameters
        "{action}/{id}",    // URL with parameters
        new { controller = "Home", action = "Index", id = ""// Parameter defaults
    ));

    Another example where we have our controller in the domain name:

    routes.Add("DomainRoute", new DomainRoute(
        "{controller}.example.com",     // Domain with parameters< br />    "{action}/{id}",    // URL with parameters
        new { controller = "Home", action = "Index", id = ""// Parameter defaults
    ));

    Want the full controller and action in the domain?

    routes.Add("DomainRoute", new DomainRoute(
        "{controller}-{action}.example.com",     // Domain with parameters
        "{id}",    // URL with parameters
        new { controller = "Home", action = "Index", id = ""// Parameter defaults
    ));

    Here’s the multicultural route:

    routes.Add("DomainRoute", new DomainRoute(
        "{language}.example.com",     // Domain with parameters
        "{controller}/{action}/{id}",    // URL with parameters
        new { language = "en", controller = "Home", action = "Index", id = ""// Parameter defaults
    ));

    HtmlHelper extension methods

    Since we do not want all URLs generated by HtmlHelper ActionLink to be using full URLs, the first thing we’ll add is some new ActionLink helpers, containing a boolean flag whether you want full URLs or not. Using these, you can now add a link to an action as follows:

    <%= Html.ActionLink("About", "About", "Home", true)%>

    Not too different from what you are used to, no?

    Here’s a snippet of code that powers the above line of code:

    public static class LinkExtensions
    {
        public static string ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, bool requireAbsoluteUrl)
        {
            return htmlHelper.ActionLink(linkText, actionName, controllerName, new RouteValueDictionary(), new RouteValueDictionary(), requireAbsoluteUrl);
        }

        // more of these...

        public static string ActionLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, RouteValueDictionary routeValues, IDictionary<string, object> htmlAttributes, bool requireAbsoluteUrl)
        {
            if (requireAbsoluteUrl)
            {
                HttpContextBase currentContext = new HttpContextWrapper(HttpContext.Current);
                RouteData routeData = RouteTable.Routes.GetRouteData(currentContext);

                routeData.Values["controller"] = controllerName;
                routeData.Values["action"] = actionName;

                DomainRoute domainRoute = routeData.Route as DomainRoute;
                if (domainRoute != null)
                {
                    DomainData domainData = domainRoute.GetDomainData(new RequestContext(currentContext, routeData), routeData.Values);
                    return htmlHelper.ActionLink(linkText, actionName, controllerName, domainData.Protocol, domainData.HostName, domainData.Fragment, routeData.Values, null);
                }
            }
            return htmlHelper.ActionLink(linkText, actionName, controllerName, routeValues, htmlAttributes);
        }
    }

    Nothing special in here: a lot of extension methods, and some logic to add the domain name into the generated URL. Yes, this is one of the default ActionLink helpers I’m abusing here, getting some food from my DomainRoute class (see: Dark Magic).

    Dark magic

    You may have seen the DomainRoute class in my code snippets from time to time. This class is actually what drives the extraction of (sub)domain and adds token support to the domain portion of your incoming URLs.

    We will be extending the Route base class, which already gives us some properties and methods we don’t want to implement ourselves. Though there are some we will define ourselves:

    public class DomainRoute : Route

        // ...

        public string Domain { get; set; }

        // ...

        public override RouteData GetRouteData(HttpContextBase httpContext)
        {
            // Build regex
            domainRegex = CreateRegex(Domain);
            pathRegex = CreateRegex(Url);

            // Request information
            string requestDomain = httpContext.Request.Headers["host"];
            if (!string.IsNullOrEmpty(requestDomain))
            {
                if (requestDomain.IndexOf(":") > 0)
                {
                    requestDomain = requestDomain.Substring(0, requestDomain.IndexOf(":"));
                }
            }
            else
            {
                requestDomain = httpContext.Request.Url.Host;
            }
            string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;

            // Match domain and route
            Match domainMatch = domainRegex.Match(requestDomain);
            Match pathMatch = pathRegex.Match(requestPath);

            // Route data
            RouteData data = null;
            if (domainMatch.Success && pathMatch.Success)
            {
                data = new RouteData(this, RouteHandler);

                // Add defaults first
                if (Defaults != null)
                {
                    foreach (KeyValuePair<string, object> item in Defaults)
                    {
                        data.Values[item.Key] = item.Value;
                    }
                }

                // Iterate matching domain groups
                for (int i = 1; i < domainMatch.Groups.Count; i++)
                {
                    Group group = domainMatch.Groups[i];
                    if (group.Success)
                    {
                        string key = domainRegex.GroupNameFromNumber(i);
                        if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
                        {
                            if (!string.IsNullOrEmpty(group.Value))
                            {
                                data.Values[key] = group.Value;
                            }
                        }
                    }
                }

                // Iterate matching path groups
                for (int i = 1; i < pathMatch.Groups.Count; i++)
                {
                    Group group = pathMatch.Groups[i];
                    if (group.Success)
                    {
                        string key = pathRegex.GroupNameFromNumber(i);
                        if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
                        {
                            if (!string.IsNullOrEmpty(group.Value))
                            {
                                data.Values[key] = group.Value;
                            }
                        }
                    }
                }
            }

            return data;
        }

        public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
        {
            return base.GetVirtualPath(requestContext, RemoveDomainTokens(values));
        }

        public DomainData GetDomainData(RequestContext requestContext, RouteValueDictionary values)
        {
            // Build hostname
            string hostname = Domain;
            foreach (KeyValuePair<string, object> pair in values)
            {
                hostname = hostname.Replace("{" + pair.Key + "}", pair.Value.ToString());
            }

            // Return domain data
            return new DomainData
            {
                Protocol = "http",
                HostName = hostname,
                Fragment = ""
            };
        }

        // ...
    }

    Wow! That’s a bunch of code! What we are doing here is converting the incoming request URL into tokens we defined in our route, on the domain level and path level. We do this by converting {controller} and things like that into a regex which we then try to match into the route values dictionary. There are some other helper methods in our DomainRoute class, but these are the most important.

    Download the full code here: MvcDomainRouting.zip (250.72 kb)

    (if you want to try this using the development web server in Visual Studio, make sue to add some fake (sub)domains in your hosts file)

    kick it on DotNetKicks.com


    Categories: ASP.NET | C# | General | MVC | Projects

    Comments

    Juliën Netherlands | Reply

    Wednesday, May 20, 2009 8:38 AM

    Juliën

    Excellent work, Maarten! Nice in-depth post.

    Issa Qandil Jordan | Reply

    Wednesday, May 20, 2009 9:03 AM

    Issa Qandil

    Indeed this is a great and a one of a kind post.

    Thanks for sharing.
    I. Qandil

    microsoft.dailynewsabout.com | Reply

    Wednesday, May 20, 2009 9:11 AM

    pingback

    Pingback from microsoft.dailynewsabout.com

    Daily News About Microsoft :  A few links about Microsoft - Tuesday, 19 May 2009 23:46

    Parag Mehta India | Reply

    Wednesday, May 20, 2009 10:56 AM

    Parag Mehta

    Nice post! However I hope you understand the SEO implications of subdomains. A SubDomain is technically considered as a different site in Google. Doing subdomain for different languages is not good SEO practice. Because then you will have to take care of each subdomain's Submissions etc.

    DotNetShoutout | Reply

    Friday, May 22, 2009 2:22 AM

    trackback

    ASP.NET MVC Domain Routing - Maarten Balliauw

    Thank you for submitting this cool story - Trackback from DotNetShoutout

    Frobozz Russia | Reply

    Friday, May 22, 2009 4:02 AM

    Frobozz

    Great work!

    But DomainRoute class doesn't allow to use routing for subdomains that contain '-' symbol (for ex. '{section}.site.com').
    The problem is inside DomainRoute.CreateRegex method - it allows only alphanumeric and underscore for '{section}' in this example.
    Don't sure now if it's correct to change it like this
    source = source.Replace("}", @">([a-zA-Z0-9_\-]*))");

    maartenba Belgium | Reply

    Friday, May 22, 2009 7:16 AM

    maartenba

    You can do {sectionX}-{sectionY} if you want. Otherwise, in CreateRegex, apply the change you proposed.

    blog.cwa.me.uk | Reply

    Friday, May 22, 2009 11:10 AM

    pingback

    Pingback from blog.cwa.me.uk

    Reflective Perspective - Chris Alcock  » The Morning Brew #353

    bogdanbrinzarea.wordpress.com | Reply

    Thursday, May 28, 2009 3:38 PM

    pingback

    Pingback from bogdanbrinzarea.wordpress.com

    Summary 21.05.2009 – 28.05.2009 «  Bogdan Brinzarea’s blog

    dotnetwitter.wordpress.com | Reply

    Friday, May 29, 2009 2:30 AM

    pingback

    Pingback from dotnetwitter.wordpress.com

    links for 2009-05-28 « Praveen’s Blog

    Chad United States | Reply

    Saturday, July 25, 2009 9:30 AM

    Chad

    My site has membership. I tried implementing what you did here to do subdomain routing based on language.

    Problem is, when logged in, ASP.NET MVC's built-in membership system thinks the site is totally different... asking me to login every time I change subdomain... ;/

    Chad United States | Reply

    Saturday, July 25, 2009 9:34 AM

    Chad

    One more thing... seems {id} must be parsable to int in order to work. Most of my site is that way, but one section uses a string for {id}. Every visit to those urls fails, not caught by the route. Any ideas?

    Adrian Grigore Germany | Reply

    Monday, July 27, 2009 3:27 PM

    Adrian Grigore

    @Chad: I had the same problem. It's not caused by ASP.NET membership though, but by the fact that browser Cookies are only accessible by the domain in which they were created. That's done for security reasons, after all you would not want any site to spy into your logon cookies of other sites.

    The workaround I used is quite simple though: Once a user has supplied the correct user name and password, redirect him to a controller on the domain he wants to log in to. That controller just sets the authentication cookie and redirects again to the page that the user actually wanted to visit.

    blog.codingoutloud.com | Reply

    Sunday, September 27, 2009 6:20 PM

    pingback

    Pingback from blog.codingoutloud.com

    Supporting two domains in one ASP.NET MVC site – A Poor Man’s Approach « Coding Out Loud

    Fred United States | Reply

    Friday, October 02, 2009 3:45 AM

    Fred

    How can we test it on localhost?
    Am I missing some step or there is a special approach for that?

    Thanks in advance.

    Maarten Belgium | Reply

    Friday, October 02, 2009 7:55 AM

    Maarten

    You can add an entry to your hosts file (c:\windows\system32\drivers\etc\hosts) and map this to your local host. That way, you have multiple domains or subdomains pointing to your localhost.

    simonguest.com | Reply

    Thursday, October 15, 2009 2:09 AM

    trackback

    Screencasts from "Patterns for Cloud Computing" Presentation

    Since returning from Europe a couple of weeks ago, I've been asked if my presentations were recorded,

    agilecat.wordpress.com | Reply

    Sunday, October 25, 2009 10:43 PM

    pingback

    Pingback from agilecat.wordpress.com

    クラウド・コンピューティングの5つのパターン Take 2 « Agile Cat — Azure & Hadoop — Talking Book

    agilecat.wordpress.com | Reply

    Sunday, October 25, 2009 10:51 PM

    pingback

    Pingback from agilecat.wordpress.com

    クラウド・コンピューティングの5つのパターン Take 2 « Agile Cat — Azure & Hadoop — Talking Book

    Valer Romania | Reply

    Tuesday, November 10, 2009 5:57 AM

    Valer

    Maarten,

    How about removing some unwanted folders from the URLs when the app is hosted in a subdirectory? Godaddy lets you host multiple websites in the same hosting account (but in different folders) and although I have multiple domain names pointing to the respective folders, when I deploy an MVC app, the full hosting path is there... Ugly and annoying. See my question on Stack Overflow at stackoverflow.com/.../ugly-urls-when-an-asp-net-mvc-app-is-hosted-in-a-virtual-directory

    Regards,
    Valer

    Alessandro Brazil | Reply

    Monday, January 11, 2010 5:15 PM

    Alessandro

    Hello, i tried this solution and it worked well in my localhost, but when i tried using it in my webserver, something went wrong.

    I have the *.caeli.com.br A 75.125.142.234 in my dns records but it seems the mcv cant see it... or any subdomain is redirecting to another place.

    To see whats happening , the website is : www.caeli.com.br.  any subdomain you put goes to a default plesk page... any idea whats wrong ?

    Thanks
    Alessandro

    maartenba Belgium | Reply

    Monday, January 11, 2010 5:34 PM

    maartenba

    Is your IIS configured to listen on all addresses *.caeli.com.br or just www.caeli.com.br?

    Alessandro Brazil | Reply

    Monday, January 11, 2010 5:57 PM

    Alessandro

    I havent done anything in the IIS.  I created the website in Plesk (wich is suposed to define all the things i need in IIS ?)  And then i added the A record in DNS config inside Plesk pointing all subdomains to the same IP.

    I can ping all the subdomains and they respond to the right IP but thats about it.

    If im missing something in IIS could you tell me what to do ?  I know i could create the subdomains in IIS one by one and it should work fine, but the problem is i wanted to do something like www.user1.caeli.com.br and "filter" all the things i want for user 1... and then any new user should have this already set as well...

    maartenba Belgium | Reply

    Monday, January 11, 2010 6:13 PM

    maartenba

    Not sure how to do this in Plesk...

    Alessandro Brazil | Reply

    Monday, January 11, 2010 7:05 PM

    Alessandro

    Plesk dont prevent me from working directly in IIS too.  If i need to change something in IIS i can do it.

    Some friends said i would have to map *.caeli.com.br to some folder in my server too probably in IIS but i dont know how to do that other than creating all subdomains.

    Alessandro Brazil | Reply

    Monday, January 11, 2010 7:49 PM

    Alessandro

    I tried to bind *.caeli.com.br but IIS do not allow me to do that.

    I could select one subdomain and do the binding, it worked nicely, but it would require me to do that manually everytime a new user signs on.

    Is there a way to achieve that ? I read about the WMI SSLBinding.Create method  , could i use that to programatically add new bindings ?

    maartenba Belgium | Reply

    Monday, January 11, 2010 8:03 PM

    maartenba

    Should probably work.

    maartenba Belgium | Reply

    Wednesday, February 24, 2010 11:48 AM

    maartenba

    That's currently not implemented.

    William South Africa | Reply

    Wednesday, February 24, 2010 12:05 PM

    William

    Not fully tested but this seems to work if added to the DomainRoute class:

    public DomainRoute(string domain, string url, object defaults, object constraints, string[] namespaces)
                : base(url, new RouteValueDictionary(defaults), new MvcRouteHandler())
            {
                if (url == null) {
                    throw new ArgumentNullException("url");
                }

                Route route = new Route(url, new MvcRouteHandler()) {
                    Defaults = new RouteValueDictionary(defaults),
                    Constraints = new RouteValueDictionary(constraints)
                };

                if ((namespaces != null) && (namespaces.Length > 0)) {
                    route.DataTokens = new RouteValueDictionary();
                    route.DataTokens["Namespaces"] = namespaces;
                }

                Domain = domain;
            }

    Daniel Steigerwald Czech Republic | Reply

    Sunday, June 27, 2010 12:44 PM

    Daniel Steigerwald

    Nice work! You should consider put it to github or codeplex.

    Gorkem Pacaci Turkey | Reply

    Monday, June 28, 2010 10:17 PM

    Gorkem Pacaci

    There is a little problem with this solution. Say, you want to handle subdomains as different users:

    routes.Add("SD", new DomainRoute("user}.localhost", "", new { controller = "Home", action = "IndexForUser", user="u1" } ));

    It caches the homepage as well. This is because of the regex that's generated. In order to fix this, you can make a copy of the CreateRegex method in DomainRoute.cs, name it CreateDomainRegex, change the * on this line to +:

    source = source.Replace("}", @">([a-zA-Z0-9_]*))");

    and use this new method instead to generate domain regex in GetRouteData method:

    domainRegex = CreateDomainRegex(Domain);

    Then it won't catch the non-sub-domain homepage. (http://localhost/)

    Tristan United Kingdom | Reply

    Friday, July 02, 2010 4:11 PM

    Tristan

    Has anyone managed to get subdomain level routing working against areas within MVC.

    So:
    http://{area}.domain.com/{controller}/{index}

    Then you could have a Blog area, and a Shop area all with different controllers etc...

    This seems to me to be the perfect use of this extended functionality.

    Ken United Kingdom | Reply

    Saturday, August 28, 2010 11:48 AM

    Ken

    HI there,

    Has anyone done a version of this for Webforms 4.0?

    Regards

    maartenba Belgium | Reply

    Monday, August 30, 2010 7:27 AM

    maartenba

    This version should work with webforms as well.

    Ken United Kingdom | Reply

    Monday, August 30, 2010 12:01 PM

    Ken

    Hi there,

    Thanks for replying. I have a solution working now. I did the domain switching in a custom handler. It was mainly hard-coded, but that was ok for the project in question.

    Regards!

    Felipe Lima Brazil | Reply

    Monday, August 30, 2010 3:57 PM

    Felipe Lima

    Thanks a lot, man! This code fit perfectly my needs!! You rock Smile

    Hernan Argentina | Reply

    Wednesday, September 01, 2010 5:10 AM

    Hernan

    This implementation is not working with catch all routing. For instance,

    routes.Add("DomainRoute", new DomainRoute(
                    "{language}.localhost",     // Domain with parameters
                    "{*view}",    // URL with parameters
                    new { language = "es", controller = "Main", action = "Index", view = UrlParameter.Optional }  // Parameter defaults
                ));

    The parsing of  "{*view}" fails because of the asterisk. Can you please help me with this? Thanks.

    Add comment




      Country flag

    biuquote
    • Comment
    • Preview
    Loading