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.
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).