» Subdomain Routing on ASP.NET MVC - Geek Stuff
Subscribe

Subdomain Routing on ASP.NET MVC

When I was developing Fora da Rotina I came across a very important requirement. Aside from the main website, It would be necessary three more subdomains:

  • blog.foradarotina.com.br
  • admin.foradarotina.com.br
  • forum.foradarotina.com.br

The current website is all custom code and I want to deploy them all at once, in the same host, with the same database and running in the same process. That means if one of those is down, everything is down and so on.
Given this requirement, I started looking around on what would be the best way to do it. I finally decided to use one Area (ASP.NET MVC) for each subdomain. But how to do it with ASP.NET Routing? While looking for it on Google, I came across a lot of options but none of them fit well on my needs. So, I decided that the best option would be to do it myself. The “magic” is behind a single class that I called SubdomainRoute, and here is the code for it:

public class SubdomainRoute : Route, IRouteWithArea
{
    private string[] namespaces;
    public string Subdomain { get; private set; }
 
    public SubdomainRoute(string subdomain, string url, object defaults, string[] namespaces)
        : this(subdomain, url, defaults, new { }, namespaces)
    {
 
    }
 
    public SubdomainRoute(string subdomain, string url, object defaults, object constraints, string[] namespaces)
        : base(url, new RouteValueDictionary(defaults), new RouteValueDictionary(constraints), new MvcRouteHandler())
    {
        this.Subdomain = subdomain;
        this.namespaces = namespaces;
    }
 
    public string Area
    {
        get { return Convert.ToString(this.Defaults["area"]); }
    }
 
    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;
        string requestDomain = GetSubdomain(httpContext.Request.Headers["Host"]);
 
        RouteData data = null;
        Regex domainRegex = CreateRegex(this.Subdomain);
        Regex pathRegex = CreateRegex(this.Url);
        Match domainMatch = domainRegex.Match(requestDomain);
        Match pathMatch = pathRegex.Match(requestPath);
 
        if (domainMatch.Success && pathMatch.Success)
        {
            data = base.GetRouteData(httpContext);
            if (data == null)
                return null;
 
            data.DataTokens.Add("Area", data.Values["area"]);
            data.DataTokens.Add("namespaces", this.namespaces);
        }
 
        return data;
    }
 
    public static string GetSubdomain(string host)
    {
        if (host.IndexOf(":") >= 0)
            host = host.Substring(0, host.IndexOf(":"));
 
        Regex tldRegex = new Regex(@"\.[a-z]{2,3}\.[a-z]{2}$");
        host = tldRegex.Replace(host, "");
        tldRegex = new Regex(@"\.[a-z]{2,4}$");
        host = tldRegex.Replace(host, "");
 
        if (host.Split('.').Length > 1)
            return host.Substring(0, host.IndexOf("."));
        else
            return string.Empty;
    }
 
    private Regex CreateRegex(string source)
    {
        source = source.Replace("/", @"\/?");
        source = source.Replace(".", @"\.?");
        source = source.Replace("-", @"\-?");
        source = source.Replace("{", @"(?<");
        source = source.Replace("}", @">([a-zA-Z0-9_-]*))");
        return new Regex("^" + source + "$");
    }
}

How it works

What we do is pretty simple, just extract the subdomain from the Host header parameter and check to see if it matches the route subdomain. If this is correct, then I’ll extract the route values from the url and add to the RouteData.
Note: I really hate my current implementation of the GetSubdomain method, but it works. If you have a better ideia, please let me know.

The usage

Here is an example of a subdomain routed action:

public class AdminAreaRegistration : AreaRegistration
{
    public override string AreaName
    {
        get { return "Admin"; }
    }
 
    public override void RegisterArea(AreaRegistrationContext context)
    {
        context.Routes.Add("Admin_Home", new SubdomainRoute(
            "admin",
            "",
            new { area = this.AreaName, controller = "Home", action = "Index" },
            new string[] { "SubdomainRouting.Areas.Admin.Controllers" }
        ));
 
        context.Routes.Add("Admin_Login", new SubdomainRoute(
            "admin",
            "login",
            new { area = this.AreaName, controller = "Login", action = "Index" },
            new string[] { "SubdomainRouting.Areas.Admin.Controllers" }
        ));
    }
}

The first parameter is the subdomain, the seconds is the path and the rest are route values and namespaces. In my real project I’ve add another layer of abstraction to simplify the usage and remove duplicity.
Aside from that, I also had to write a custom RouteLink method for my HtmlHelper because the default method (that comes with ASP.NET MVC) does not handle add subdomains to the generated Url. This code is also avaiable in the project solution down here.

Full source code with working example

How to set up local environment

If you downloaded this solution and want to test it locally, you will need to change the “C:\Windows\System32\drivers\etc\hosts” file. In my computer I’ve add these two lines:

 127.0.0.1 admin.localhost
 127.0.0.1 blog.localhost

By doing this, when you browse to admin.localhost you will be redirected to 127.0.0.1, which is your own computer and will then be handled by the SubdomainRoute class.

Resources

The code I wrote is highly based on these resources.

UPDATE 02/29/2012: I refactored the Subdomain class and added constraint support.

447baadbf03b54862a8d2a68096b26ad

Related posts:

  1. Uniqueness validation with Fluent Validation, NHibernate and ASP.NET MVC
Tagged ,

4 thoughts on “Subdomain Routing on ASP.NET MVC

  1. Jacob says:

    Good article,
    But what about constraints? How do handle these?

    • Oenning says:

      I’m currently not using constraints in my projects, that’s why I totally forgot about them. I did a quick draft of how to handle constraints in this custom route and I’ve come to this: https://gist.github.com/1932971

      It worked as expected on my tests, I’ll work more on this and will update this post when I come to a definitive solution.

      Thanks for the visit ;-)

  2. REM says:

    A lot of searching led me to your solution, which seems up to date and thorough. I’m not a programmer, so things like this help out tremendously. I am wondering, however, whether or not something like this would work using Areas in a single MVC3 app but distinct domains (website1.com, website2.com, website3.com). The reason being that I want to derive from a single “layout” and “css” and be able to create distinct content on each site. Hopefully to be able to use another “css” file within the site (lets say to change colors). Is it possible with this solution? Thanks.

    • Oenning says:

      Hi REM

      No, this solution only works for subdomain. But it’s possible to achieve what you need with a custom Route, the code would be very similar to this one.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">