Subdomains for a single application with ASP.NET MVC
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 instead of a List for the static cache variable).