ASP.NET & MVC 4: Cookieless domain for bundling and static resources
Last week, one reader of this blog asked, how to setup cookieless domain to serve bundle response and other static resources in ASP.NET application. Yes it is very much necessary and one of the web performance guideline that static resource should be served from cookieless domain for better performance. So here in this post we will see how to setup ASP.NET application and IIS for cookieless domain with minimal code changes and deployment task. You can directly jump to IIS setup section if you don't want to read detailed information.
Cookieless domain and IIS
Logically we can’t configure IIS or any other web server so that it does not accept or set cookie for domain. This is because cookie is stored in client side and more importantly it is component of client and not of server. Hence we can’t configure IIS or any other web server to disable cookie.
So what’s the solution for that? As a developer, we need to take care that we do not set cookie for static or cookieless domain by server side scripting or client side scripting. We need to also take care that any third party JavaScript also do not set cookie for static domain. Because once cookie is set, it will be used for all subsequent requests until it is expired or removed by client.
ASP.NET & Cookie
By default ASP.NET session id is stored in cookie and even if we use cookieless session then also there are chances that cookie might be set by other code or sometime by HTTP handler or HTTP module also. Conclusion is that we can’t use our main domain (for e.g. www.domain.com) to serve static content and we need to configure another domain or website (for e.g. static.domain.com) to serve static content and we need to move static resource to static.domain.com and refer that cookieless domain from within www.domain.com pages.
That’s sound pretty much cool and easy too but ASP.NET 4.5 & MVC 4, introduced bundling which bundle all JavaScript and CSS files dynamically at runtime by HTTP module. As behind the scene bundling is using HTTP module, again we need to setup ASP.NET application for static.domain.com and the same time we also need to take care that static.domain.com must not serve dynamic response other than bundle response. So here are the step by step tutorial starting with IIS setup to achieve the same.
IIS Setup
Create two websites in IIS. One for our main domain i.e. www.domain.com and another for cookieless domain i.e. static.domain.com. Now point these both website to same physical directory i.e. C:\inetpub\www.domain.com. Yeah we don’t want to deploy at two places and still static.domain.com will serve only static resource including bundle response and still there will be not any additional request filtering overhead for www.domain.com. Continue reading to know more ;)
Redirect domain.com to www.domain.com
Once you setup domain in IIS now its time to configure www.domain.com so that any domain.com request must be redirected to www.domain.com because cookie set for domain.com will also be shared by all sub domain including static.domain.com hence it is much important steps. You can read this post on how to redirect domain.com to www.domain.com.
Code changes
Now add following three app settings in web.config. StaticSiteName is the name of website in IIS for static.domain.com.
<appSettings>
<add key="StaticSiteName" value="static.domain.com"/>
<add key="StaticDomain" value="http://static.domain.com"/>
<add key="MainDomain" value="http://www.domain.com"/>
</appSettings>
We will use this StaticSiteName to dynamically register HTTP module only for static.domain.com so static resource filtering will be enabled only for static.domain.com and not for www.domain.com even if we are sharing same deployment.
We will use PreApplicationStartMethod and Microsoft.Web.Infrastructure to dynamically register HTTP module in pre-application startup stage.
Following is the code snippet for the same.
public class PreApplicationStart
{
public static void Start()
{
string strStaticSiteName = ConfigurationManager.AppSettings["StaticSiteName"];
string strCurrentSiteName = HostingEnvironment.SiteName;
if (strCurrentSiteName.ToLower() == strStaticSiteName.ToLower())
{
DynamicModuleUtility.RegisterModule(typeof(StaticResource));
}
}
}
public class StaticResource : IHttpModule
{
public void Init(HttpApplication context)
{
context.BeginRequest += new EventHandler(context_BeginRequest);
}
void context_BeginRequest(object sender, EventArgs e)
{
HttpContext context = HttpContext.Current;
string strUrl = context.Request.Url.OriginalString.ToLower();
//HERE WE CAN CHECK IF REQUESTED URL IS FOR STATIC RESOURCE OR NOT
if (strUrl.Contains("Path/To/Static-Bundle/Resource") == false)
{
string strMainDomain = ConfigurationManager.AppSettings["MainDomain"];
context.Response.Redirect(strMainDomain);
}
}
public void Dispose()
{
}
}
In above code snippet we are registering StaticResource HTTP module only if pre application start method is fired for static.domain.com and then after if current request is not for static or bundle response then we are redirecting user to our main domain i.e. www.domain.com. This is also very much important because if we do not filter request then it is likely possible that our main domain content could be available via static.domain.com. One more important thing is that we are registering HTTP module only for static.domain.com application context so request to www.domain.com will not be intercepted by StaticResource module so it could save fraction of second in overall response time :)
Now add following extension method for UrlHelper class.
public static class Extensions
{
public static string StaticContent(this UrlHelper url, string contentPath)
{
string strStaticDomain = ConfigurationManager.AppSettings["StaticDomain"];
return contentPath.Replace("~", strStaticDomain);
}
}
Now we can use @Url.StaticContent() from view so that it will render static resource url with static.domain.com whether it is image, script, CSS, or bundles or wherever we want to refer cookieless domain. for e.g.
<link href="@Url.StaticContent("~/Content/Site.css")" rel="Stylesheet" />
<script src="@Url.StaticContent("~/Scripts/jquery-1.7.1.js")" type="text/javascript"></script>
<script src="@Url.StaticContent("~/bundles/jquery")" type="text/javascript"></script>
<img src="@Url.StaticContent("~/Images/heroAccent.png")" alt="" />
We can also override Styles.Render or Scripts.Render to serve script resource and style resource from cookieless domain.
Third party JavaScript and cookie
Often we are using third party widget in our web application so it may be possible that third party script can set cookie for domain.com and any cookie set for domain.com will be shared by all sub domain including static.domain.com so we need to configure third party JavaScript such a way that it set cookie for www.domain.com and not for domain.com. Following is the code snippet for configuring Google Analytics script to set cookie for www.domain.com. You can get more information here.
_gaq.push(['_setDomainName', 'www.domain.com']);
By default Google Analytics set cookie for top level domain i.e. domain.com and that could be shared by all sub domain. We need to identify such third party JavaScript and accordingly we need to change it for truly cookieless domain.
See below images to see all above setup in action!
Reference
- Redirect domain.com to www.domain.com (must step)
- ASP.NET: PreApplicationStartMethod example
- ASP.NET: Register HttpModule at runtime
- [Updated] ASP.NET Web Optimization Framework & Cookieless Domain
Hope this would be helpful. You can also follow me on @NandipMakwana to get latest tips.