Subdomains for a single application with ASP.NET MVC

  1. June 7, 2009

    Update: Complete source code demonstrating this approach is available on MSDN Code Gallery for MVC1 and MVC2.

    I’ve wanted to use subdomains for sub-sites within a single application for a while now, the way you see the Rails guys doing all the time (e.g. 37 signals, Shopify and LessEverything). Basically, instead of setting up multiple sub-sites like this…

    http://example.com/shop1/
    http://example.com/shop2/

    …I wanted to do this…

    http://shop1.example.com/
    http://shop2.example.com/

    …and still be able to create new sites dynamically without having to reconfigure IIS.

    Why would you want to do that?

    Using a subdomain rather than a path makes it much clearer (to users and search engines) that the sites are separate and distinct. Having separate domains also gives the customer a feeling of ownership - they’re not sharing the domain with anyone else.

    Setting up DNS for the development environment

    First I tried to add wildcard entries to my HOSTS file, but I quickly found out that doesn’t work. Then I started looking for some sort of DNS proxy that would allow me to define a wildcard DNS entry like *.local, so that shop1.local and shop2.local would automatically point to localhost.

    I couldn’t find anything like that, so I settled for manually updating my HOSTS file each time I added a new site. I know in production I can add wildcard DNS entries so I wasn’t too worried about finding a set-and-forget solution here.

    Getting it working in the Visual Studio 2008 ASP.NET Web Server

    The next problem I ran into was the built-in web server in VS2008 always returns “localhost” when you look at the HttpRequestBase.Url.Host property. The workaround I used was to instead look at the Host header from HttpRequestBase.Headers. This will usually come in with a port attached when debugging locally (e.g. “localhost:3308”) so you need to extract it like this:

    string host = requestContext.HttpContext.Request.Headers["Host"].Split(':')[0];

    Quick and testable design

    After playing around with the idea of defining a new Route handler that would look at the host passed in the URL, I eventually went with the idea of a base Controller that is aware of the Site it’s being accessed for. It looks like this:

    public abstract class SiteController : Controller {
        ISiteProvider _siteProvider;
    
        public SiteController() {
            _siteProvider = new SiteProvider();
        }
    
        public SiteController(ISiteProvider siteProvider) {
            _siteProvider = siteProvider;
        }
    
        protected override void Initialize(RequestContext requestContext) {
            string[] host = requestContext.HttpContext.Request.Headers["Host"].Split(':');
    
            _siteProvider.Initialise(host[0]);
    
            base.Initialize(requestContext);
        }
    
        protected override void OnActionExecuting(ActionExecutingContext filterContext) {
            ViewData["Site"] = Site;
    
            base.OnActionExecuting(filterContext);
        }
    
        public Site Site {
            get {
                return _siteProvider.GetCurrentSite();
            }
        }
    
    }

    ISiteProvider is a simple interface:

    public interface ISiteProvider {
        void Initialise(string host);
        Site GetCurrentSite();
    }

    This also allows for customers who want to bring their own domain - the sites don’t have to be subdomains of a default domain.

    Updates (March 5, 2010)

    Ben points out below that you need to do a bit of extra work when it comes to output caching so that output isn’t cached across all subdomains. My preferred method is to use the VaryByHeader=”Host” like this:

    [OutputCache(Duration=10,VaryByHeader="Host",VaryByParam="None")]
    public ActionResult Index() {
        // your code here
    }

    Ben shows how to do it with a VaryByCustom parameter below too.

    Also, here’s a simple example implementation of ISiteProvider, where MyDataContext is a LINQ to SQL data context:

    public class SiteProvider : ISiteProvider {
        MyDataContext _db;
        Site _site;
    
        public SiteProvider(MyDataContext db) {
            _db = db;
        }
    
        public void Initialise(string host) {
            _site = _db.Sites.SingleOrDefault(s => s.Host == host);
        }
    
        public Site GetCurrentSite() {
            return _site;
        }		
    }
    

    Ben gives an example below of how to do it with an in-memory cache of Sites. This will improve performance because it doesn’t have to load the Site from the DB for each request (I’d probably just tweak Ben’s solution to use a Dictionary<string,Site> instead of a List<Site> for the static cache variable).

    Comments
blog comments powered by Disqus