Combining and optionally minifying CSS and JavaScript along with caching in ASP.Net

A while back, I ran into a blog post from “Official Google Webmaster Central Blog”  which described that site speed is now an important factor when considering website for ranking in Search Engine Result Page (SERP). Also, Scott Hanselman from Microsoft explained in a post the importance of combining and minifying CSS/Javascripts.

Now I tried a lot of libraries available on the Internet that offered combining and minifying javascripts as well as CSS files. Now each and every libraries had their advantages as well as disadvantages and I’d get lost in it’s complexity. So after a lot of research, I came up with a simple class that can be used to combine CSS and JavaScript files with caching support using multiple file cache dependency and optionally minifying them using Microsoft’s AjaxMin. What’s  more? You can provide your own minifying function or post processor (say LESS or SASS processor) too.

Without further ado, here is the code.

Namespace Utils
    Public Class Bundle
        Private _PathList As List(Of String)

        Public Delegate Function ContentProcessorDelegate(ByVal e As String) As String

        Public Shared Function Instance() As Bundle
            Return New Bundle
        End Function

        Public Sub New()
            _PathList = New List(Of String)
        End Sub
        Public Function AddItem(ByVal Path As String) As Bundle
            If Path.StartsWith("~/") OrElse Path.StartsWith("/") Then
                _PathList.Add(HttpContext.Current.Server.MapPath(Path))
            End If
            Return Me
        End Function

        Public Function Render(ByVal OutPath As String, Optional ByVal dlgProcessor As ContentProcessorDelegate = Nothing) As String
            Dim _cachekey As String = OutPath.ToSlug
            Dim _outPath As String = HttpContext.Current.Server.MapPath(OutPath)
            If HttpContext.Current.Cache(_cachekey) IsNot Nothing AndAlso IO.File.Exists(_outPath) Then
                Return OutPath & "?v=" & HttpContext.Current.Cache(_cachekey)
            End If
            Dim _builder As New StringBuilder
            For Each _file As String In _PathList
                If IO.File.Exists(_file) Then
                    _builder.AppendLine(IO.File.ReadAllText(_file))
                End If
            Next
            Dim _output As String = _builder.ToString
            If dlgProcessor IsNot Nothing Then
                _output = dlgProcessor.Invoke(_output)
            End If
            IO.File.WriteAllText(_outPath, _output)
            HttpContext.Current.Cache.Insert(_cachekey, Now.Ticks, New CacheDependency(_PathList.ToArray))
            Return Render(OutPath)
        End Function

        Public Shared Function MinifyCSS(ByVal e As String) As String
            Dim minifier As New Microsoft.Ajax.Utilities.Minifier()
            Return minifier.MinifyStyleSheet(e)
        End Function

        Public Shared Function MinifyJS(ByVal e As String) As String
            Dim minifier As New Microsoft.Ajax.Utilities.Minifier()
            Return minifier.MinifyJavaScript(e)
        End Function
    End Class
End Namespace

Now, there is only one other function named ToSlug which is actually an Extension function that converts the given filename into a usable Cache Key. You are free to choose your own version but here is the version that I use.

Imports Microsoft.VisualBasic
Imports System.Runtime.CompilerServices

Module StringHelper
    ''' <summary>
    ''' Removes all non-aplhanumeric characters except -(hyphen)
    ''' </summary>
    ''' <param name="data"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function PrepareSlug(ByVal data As String) As String
        If data Is Nothing Or data.Equals(String.Empty) Then
            Throw New ArgumentNullException("data")
        End If
        Dim result As String = ""

        'Replace special characters with it's equivalents
        result = data.Replace("&", "and").ToLower

        'Replace -(hyphen) with space
        result = result.Replace("-", " ")

        'Remove illegal characters
        result = Regex.Replace(result, "[^A-Za-z0-9\s-]", " ")

        'Replace multple white spaces with single space
        result = Regex.Replace(result, "\s+", " ").Trim()

        'Replace whitespace with -(hyphen)
        result = result.Replace(" ", "-")
        Return result
    End Function

    <Extension()> _
    Public Function ToSlug(ByVal e As String) As String
        Return PrepareSlug(e)
    End Function
End Module

How to use this code?

Since, documentation writing isn’t one of my strongest skills, forgive me if I’m unable to clearly explain how to use this code.

Let’s say you have following files in your \Themes\Standard\assets\styles folder

  • reset.css
  • grid.css
  • global.css
  • blog.css

and your Master page file is \Themes\Standard\Main.master

When trying to use code blocks(<%%>) inside HEAD tag with runat=”server” attribute, always put them inside a ContentPlaceHolder or better yet PlaceHolder tags to avoid nasty errors.

<asp:ContentPlaceHolder ID="cphStyleSheets" runat="server">
        <%
            With Utils.Bundle.Instance
            .AddItem("/Themes/Standard/assets/styles/reset.css")
            .AddItem("/Themes/Standard/assets/styles/grid.css")
            .AddItem("/Themes/Standard/assets/styles/global.css")
            .AddItem("/Themes/Standard/assets/styles/blog.css")
        %>
        <%=_StylesheetLink(.Render("/Themes/Standard/assets/styles/combined.css", AddressOf Utils.Bundle.MinifyCSS))%>
        <%End With%>
</asp:ContentPlaceHolder>

Here _StylesheetLink is just a helper function there outputs the <link/> tag for the given stylesheet.

Website Security Test