Apr 21st 2008 06:43 pm

YSlow and ASP.NET – Expires Header – Expression Builders (Part 2 of 3)

Continuing our task of caching indefinitely static content by versioning it, we want to build the framework so that instead of code like this:

<asp:image imageurl="~/images/rose.jpg" runat="server" id="image1">

we have this:

<asp:Image id="image1" imageurl="<%$ Image: rose.jpg %>" runat="server"/>

The key difference is that we can dynamically assign where the image path for rose.jpg resolves in the latter case. Specifically, we can resolve to applicationPath/versionX.X.X/Images/rose.jpg (or whatever url scheme meets your fancy).

We need two things. First we create an expressionBuilder object and then tie the custom expressionBuilder into the build. I’m using the VS “WebSite” project, but this should work for “Web Application Projects”.

The following code draws heavily from this post on Dave Reed’s excellent blog infinitiesloop.

Imports System.CodeDom
Imports System.Web         

Namespace StaticContent         

	Public NotInheritable Class PathBuilder         

		Private Shared CssPath As String = String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}/StaticContent/{1}/Css/{{0}}", System.Web.HttpContext.Current.Request.ApplicationPath, System.Configuration.ConfigurationManager.AppSettings("versionNumber")))
		Private Shared ImagePath As String = String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}/StaticContent/{1}/Images/{{0}}", System.Web.HttpContext.Current.Request.ApplicationPath, System.Configuration.ConfigurationManager.AppSettings("versionNumber")))
		Private Shared JavascriptPath As String = String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}/StaticContent/{1}/Javascripts/{{0}}", System.Web.HttpContext.Current.Request.ApplicationPath, System.Configuration.ConfigurationManager.AppSettings("versionNumber")))         

		Public Shared Function ConvertCssUrl(ByVal path As String) As String
			Return String.Format(System.Globalization.CultureInfo.InvariantCulture, CssPath, path)
		End Function         

		Public Shared Function ConvertImageUrl(ByVal path As String) As String
			Return String.Format(System.Globalization.CultureInfo.InvariantCulture, ImagePath, path)
		End Function         

		Public Shared Function ConvertJavascriptUrl(ByVal path As String) As String
			Return String.Format(System.Globalization.CultureInfo.InvariantCulture, JavascriptPath, path)
		End Function         

		Private Sub New()
		End Sub         

	End Class         

	MustInherit Class StaticContentExpressionBuilder
		Inherits Compilation.ExpressionBuilder         

		Protected Shared evaluationExpression As String         

		Protected MustOverride ReadOnly Property StaticContentExpression() As String         

		Public Overrides Function ParseExpression(ByVal expression As String, ByVal propertyType As System.Type, ByVal context As Compilation.ExpressionBuilderContext) As Object
			Return String.Format(System.Globalization.CultureInfo.InvariantCulture, StaticContentExpression, expression)
		End Function         

		Public Overloads Overrides Function GetCodeExpression(ByVal entry As System.Web.UI.BoundPropertyEntry, ByVal parsedData As Object, ByVal context As System.Web.Compilation.ExpressionBuilderContext) As System.CodeDom.CodeExpression
			Return New CodeSnippetExpression(parsedData.ToString())
		End Function         

	End Class         

	<Compilation.ExpressionPrefix("Js")> _
 Class JavascriptExpressionBuilder
		Inherits StaticContentExpressionBuilder         

		Protected Overrides ReadOnly Property StaticContentExpression() As String
			Get
				Return "StaticContent.PathBuilder.ConvertJavascriptUrl( ""{0}"" )"
			End Get
		End Property         

	End Class         

	<Compilation.ExpressionPrefix("Image")> _
	Class ImageExpressionBuilder
		Inherits StaticContentExpressionBuilder         

		Protected Overrides ReadOnly Property StaticContentExpression() As String
			Get
				Return "StaticContent.PathBuilder.ConvertImageUrl( ""{0}"" )"
			End Get
		End Property         

	End Class         

	<Compilation.ExpressionPrefix("Css")> _
	Class CssExpressionBuilder
		Inherits StaticContentExpressionBuilder         

		Protected Overrides ReadOnly Property StaticContentExpression() As String
			Get
				Return "StaticContent.PathBuilder.ConvertCssUrl( ""{0}"" )"
			End Get
		End Property         

	End Class         

End Namespace

I’ve chosen to implement several types of static content (css, images, and js) but that’s just a matter of convenience. Clearly this could all be done with a single expression. Note the reason I have placed the content type specific methods on PathBuilder is that I need to call these methods in part three (code-behind) where accessing the expressionBuilder assemblies would be pretty awkward.

To tie our new expression builders into the build we need to add some entries to the web.config.

<compilation debug="true" strict="true" explicit="true">
	<expressionBuilders>
		<add expressionPrefix="Css" type="StaticContent.CssExpressionBuilder"/>
		<add expressionPrefix="Image" type="StaticContent.ImageExpressionBuilder"/>
		<add expressionPrefix="Js" type="StaticContent.JavascriptExpressionBuilder"/>
	</expressionBuilders>
</compilation>

That’s it. Now we can do things in our markup like:

<asp:image imageurl="Image: rose.jpg" runat="server" id="image1">
<link rel="stylesheet" href="Css: Fonts.css %>" type="text/css" id="FontsCss" runat="server">

the image “rose.jpg” and css file “Fonts.css” will be rendered to the page as urls:
“applicationPath/StaticContent/1.0.0/Images/rose.jpg” and “applicationPath/StaticContent/1.0.0/Css/Fonts.css” respectively.

1 Comment »

One Response to “YSlow and ASP.NET – Expires Header – Expression Builders (Part 2 of 3)”

  1. Rob on 13 Aug 2008 at 4:44 pm #

    Interesting post. We came across a similar problem but used a slightly different method to solve it. We’re still using IIS6 so miss out on a few of the luxuries of IIS7, namely URL rewriting. Instead we’ve added Ionic’s ISAPI Rewrite to IIS, and manage our CSS and JavaScript (haven’t had time to redo all the image url’s).

    During the build (CruiseControl.Net) we get the last svn revision number. Part of the build process involves “tokenising” our .config files (ie Web.config) with environment variables. We pass in the revision number into the config file.

    Then when generating the url for the CSS and JavaScript we include the AppSettings key as part of the path, something like “/webresources/javascript/merged.7567.js”. We then have a rewrite rule to point “/webresources/javascript/merged.7567.js” > “/webresources/javascript/merged.js”. So to the browser it requests a url, but IIS points that request to a different file.

    We chose not to use a querystring (ie merged.js?version=7567) as this doesn’t follow HTTP guidelines. IE and Firefox will work regardless but I believe Safari and Opera may be stricter.

    It’s then just a matter of setting an Expires Header for the JavaScript and CSS, we currently have ours at 20 days.

    http://my.huddle.net is the url. Doing some interesting stuff there. RESTful JSON API, Inversion Of Control, Distributed NHibernate Caching, Domain Driven Design etc…

Trackback URI | Comments RSS

Leave a Reply

« | »