Asp.Net MVC 2 Routing SubDomains to Areas

I’ve been building an Asp.Net MVC 2 site with Tom on the new Thap site and we hit a stumbling point regarding sub-domains and areas; you can probably guess what the problem was from the title.

Any way after a bit of googling it looks like no one has figured this out, or that they arn’t sharing. So it’s time I shared the solution that worked for us. This requires no libraries or esoteric settings or anything like that, just a little bit of code that will end up making your routes look like this:

context.Routes.MapSubDomainRoute(
        "Admin_default", // Name
        "admin", // SubDomain
        "{controller}/{action}/{id}", // Url
        new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Defaults
        new[] { typeof(Controllers.HomeController).Namespace }); // Namespace

First we need to create a Route class that can handle subdomains, lucky for you I just happen to have one. What this class does is check the incoming request, if the sub-domain matches it then checks to see if the rest of the url matches the route you specified:

Update: Since publishing this post I’ve added an update to the code. The GetVirtualPath function  has been overridden to check if the area of the value matches the sub-domain . This was needed because it messed up the url generation for everything.

namespace Your.App
{
  using System.Web;
  using System.Web.Routing;

  /// <summary>
  /// A route class to work with a specific subDomain
  /// </summary>
  public class SubDomainRoute : Route
  {
    /// <summary>
    /// The subDomain to route against
    /// </summary>
    private readonly string subDomain;

    /// <summary>
    /// Initializes a new instance of the <see cref="SubDomainRoute"/> class.
    /// </summary>
    /// <param name="subDomain">The sub domain.</param>
    /// <param name="url">The URL.</param>
    /// <param name="routeHandler">The route handler.</param>
    public SubDomainRoute(string subDomain, string url, IRouteHandler routeHandler) : base(url, routeHandler)
    {
      this.subDomain = subDomain.ToLower();
    }

    /// <summary>
    /// Returns information about the requested route.
    /// </summary>
    /// <param name="httpContext">An object that encapsulates information about the HTTP request.</param>
    /// <returns>
    /// An object that contains the values from the route definition.
    /// </returns>
    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
      var url = httpContext.Request.Headers["HOST"];
      var index = url.IndexOf(".");

      if (index < 0)
      {
        return null;
      }

      var possibleSubDomain = url.Substring(0, index).ToLower();

      if (possibleSubDomain == subDomain)
      {
        var result =  base.GetRouteData(httpContext);
        return result;
      }

      return null;
    }

    /// <summary>
    /// Returns information about the URL that is associated with the route.
    /// </summary>
    /// <param name="requestContext">An object that encapsulates information about the requested route.</param>
    /// <param name="values">An object that contains the parameters for a route.</param>
    /// <returns>
    /// An object that contains information about the URL that is associated with the route.
    /// </returns>
    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
      // Checks if the area to generate the route against is this same as the subdomain
      // If so we remove the area value so it won't be added to the URL as a query parameter
      if(values != null && values.ContainsKey("Area"))
      {
        if(values["Area"].ToString().ToLower() == this.subDomain)
        {
          values.Remove("Area");
          return base.GetVirtualPath(requestContext, values);
        }
      }

      return null;
    }
  }
}

The next step we take is to create a bunch of extensions methods to make mapping the sub-domain a bit easier. I lifted this code straight from the MVC source and made a tiny adjustment.

namespace Your.App
{
  using System;
  using System.Diagnostics.CodeAnalysis;
  using System.Web.Mvc;
  using System.Web.Routing;

  public static class RouteCollectionExtensions
  {
    [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#",
        Justification = "This is not a regular URL as it may contain special routing characters.")]
    public static Route MapSubDomainRoute(this RouteCollection routes, string name, string subDomain, string url)
    {
      return MapSubDomainRoute(routes, name, subDomain, url, null /* defaults */, (object)null /* constraints */);
    }

    [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#",
        Justification = "This is not a regular URL as it may contain special routing characters.")]
    public static Route MapSubDomainRoute(this RouteCollection routes, string name, string subDomain, string url, object defaults)
    {
      return MapSubDomainRoute(routes, name, subDomain, url, defaults, (object)null /* constraints */);
    }

    [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#",
        Justification = "This is not a regular URL as it may contain special routing characters.")]
    public static Route MapSubDomainRoute(this RouteCollection routes, string name, string subDomain, string url, object defaults, object constraints)
    {
      return MapSubDomainRoute(routes, name, subDomain, url, defaults, constraints, null /* namespaces */);
    }

    [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#",
        Justification = "This is not a regular URL as it may contain special routing characters.")]
    public static Route MapSubDomainRoute(this RouteCollection routes, string name, string subDomain, string url, string[] namespaces)
    {
      return MapSubDomainRoute(routes, name, subDomain, url, null /* defaults */, null /* constraints */, namespaces);
    }

    [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#",
        Justification = "This is not a regular URL as it may contain special routing characters.")]
    public static Route MapSubDomainRoute(this RouteCollection routes, string name, string subDomain, string url, object defaults, string[] namespaces)
    {
      return MapSubDomainRoute(routes, name, subDomain, url, defaults, null /* constraints */, namespaces);
    }

    [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "2#",
        Justification = "This is not a regular URL as it may contain special routing characters.")]
    public static Route MapSubDomainRoute(this RouteCollection routes, string name, string subDomain, string url, object defaults, object constraints, string[] namespaces)
    {
      if (routes == null)
      {
        throw new ArgumentNullException("routes");
      }
      if (url == null)
      {
        throw new ArgumentNullException("url");
      }
      if (subDomain == null)
      {
        throw new ArgumentNullException("subDomain");
      }

      Route route = new SubDomainRoute(subDomain, 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;
      }

      routes.Add(name, route);

      return route;
    }
  }
}

The final thing to do is set up your routes in the AreaRegistration class

context.Routes.MapSubDomainRoute(
        "Admin_default", // Name
        "admin", // SubDomain
        "{controller}/{action}/{id}", // Url
        new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Defaults
        new[] { typeof(Controllers.HomeController).Namespace }); // Namespace

Look familiar?

The namespace section is used to distinguish between Identically name controllers, so if you have a home controller in two areas that line will stop any conflicts.

There is one last item to take note of, when you create a new action in a controller of an area you’ll need to specify the location of the view. If your using the MvcContrib like we do then the T4MVC template comes in vary handy. All the Actions in the areas now look something like this:

    public virtual ActionResult Index()
        {
            return View(this.Views.Index);
        }

Or if you’re not using the T4MVC then this:

    public virtual ActionResult Index()
        {
            return View("~/Areas/Admin/Views/Home/Index.aspx");
        }

21 Comments

  • April 20, 2011 - 10:02 am | Permalink

    Just an exellent article! It looks like exactly what I need. I have same problem with my site (I decide to use areas and map them to subdomains and try to find best solution to do it). There are some solutions of this problem:

    http://hanssens.org/post/ASPNET-MVC-Subdomain-Routing.aspx
    http://www.nqbao.com/2010/04/aspnet-domain-based-routing
    blog.­maartenballiauw.­be/­post/­2009/­05/­20/­ASPNET-­MVC-­Domain-­Routing.­aspx

    But your solution it the best for me. I’ll try it as soon as possible. Many thanks for sharing this experience!

  • trebormf
    May 16, 2011 - 7:42 pm | Permalink

    Areas and subdomains seem tailor-made for one another. I’m using your approach to introduce multitenancy to a web app, where each subdomain is a separate partner site. Your pattern is perhaps the most simple way to start down that path. Thanks for the article and for sharing your code.

  • George
    May 21, 2011 - 1:08 pm | Permalink

    Any chance to have some starting point for MVC3?

    I’ve tried to do this on MVC3 but no success …

    Thanks!

    • July 12, 2011 - 6:32 am | Permalink

      Sorry for the very late reply. I’ll have a look at getting it to work on MVC 3.

  • camilo
    July 11, 2011 - 9:16 pm | Permalink

    Thanks for your post…. at the moment I think it will work for me….

    Question: Is it posible to use a maproute like this one, with this approach?:

    context.Routes.MapSubDomainRoute(
    “Admin_default”, // Name
    “{subdomain}”, // SubDomain
    “{controller}/{action}/{id}”, // Url
    new { controller = “Home”, action = “Index”, id = UrlParameter.Optional }, // Defaults
    new[] { typeof(Controllers.HomeController).Namespace }); // Namespace

    thanks in advance..

    • July 12, 2011 - 6:32 am | Permalink

      Honestly I’m not sure, at some point I’ll have to try this. Just thinking what variable will the sub domain map to?

      • camilo
        July 12, 2011 - 4:03 pm | Permalink

        yeah, I’m looking to map the subdomain, the same way the url is mapped, with variables

  • camilo
    July 12, 2011 - 10:36 pm | Permalink

    another question I have, is it necessary to modify the hosts file to test this method?

  • October 4, 2011 - 12:37 am | Permalink

    Hi Tony

    This code looks very useful – Thank you for posting it!
    Did you ever get this working with MVC 3?

    • October 4, 2011 - 6:38 am | Permalink

      Never got a chance to try it in MVC 3, cannot see why it wouldn’t work though.

  • December 6, 2011 - 4:25 pm | Permalink

    Can you tell me if there is a link where I can download your sample.
    Thanks in advance … I am actually facing the same issue and would like to solve it using your solution. I am currently workin on MVC3 and would like to see how it works in your sample first . Thanks in advance.

    • December 6, 2011 - 4:52 pm | Permalink

      There is no sample, the code above is all you need. I haven’t tried it in MVC 3 but I’ve been told by someone who has that it works fine.

      Good luck!

  • December 6, 2011 - 4:36 pm | Permalink

    just to be notified when you will reply :-)

  • March 29, 2012 - 10:59 pm | Permalink

    I am getting a 403 access denied error?

  • May 24, 2012 - 1:58 am | Permalink

    Be sure to consider this approach:

    http://www.blog.nanoworking.com/Blog/post/2012/05/19/ASPNET-MVC-SubDomain-Routing.aspx

    I found it to be better for introducing multitenancy to my app than the other answers, because MVC areas are a nice way to introduce tenant-specific controllers and views in an organized way.

  • April 9, 2013 - 10:38 am | Permalink

    its very helpful

  • September 13, 2013 - 11:12 am | Permalink

    I have managed to get this solution to work with domain names instead of subdomains however there is a problem. When I try to use a partial view in my area it looks in the wrong place for the partial view. How do I add the area views folder to the search locations for the Partial?

    • September 13, 2013 - 11:25 am | Permalink

      You can actually specify the path inside Html.Partial as long as you use the file extension: @Html.Partial("~/Areas/MyArea/Views/Form/_PartialDesign.cshtml", Model)

  • Deepak C. Nair
    January 20, 2014 - 7:20 pm | Permalink

    I would like to know how “context.Routes.MapSubDomainRoute” came into being. The object of colntext is not being seen in this example. I’m trying to implement it in MVC 4.0. If you could send over a working example in mvc 4.0, that would be great.

  • March 28, 2014 - 9:10 pm | Permalink

    I’m having trouble converting this line(s) to vb.net:

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

    Firstly, I’m just using webforms not MVC. So “MvcRouteHandler()” isn’t available for me. What should I use instead?

  • Leave a Reply