Click here to Skip to main content
Email Password   helpLost your password?

Introduction

Many web developers probably know about various website optimization techniques described in the YSlow documentation and Steve Souders' book. Most of these techniques are very simple, yet bring about huge difference to the downloading time of most web pages. As simple as they are, applying some of these rules again and again in all .NET web applications can easily become a tedious task. As I couldn't find an existing solution that meets my needs from the Internet, I went about implementing Combres (formerly known as Client-side Resource Combine Library), a very easy-to-use library which can be used to automate many steps that you would have to do yourself when applying certain optimization techniques in your MVC and Web Form ASP.NET applications.

Key features of Combres

Steps to use Combres

Step 1. Modify your web.config file to register the path of the Combres configuration file

Add the following elements to your web.config file:

You can use any path for the definitionUrl, as long as it is a partial ASP.NET path (i.e., starting with "~/"). You don't have to put it in the App_Data folder if you don't want to, but it's a good convention to put application data files there.

In this example, the file combres.xml in the App_Data folder will be the configuration file for the Combres library. You might wonder why I chose to put configuration settings in yet another file instead of leveraging the existing web.config file. The reason is that any change to web.config forces an application restart, and I don't think anybody would want to have such a time-consuming process take place just because they make simple changes to their CSS and JavaScript files, like renaming a file, or moving it to another folder.

Step 2. Create the Combres configuration file in the location you specified in the previous step

Let's look at a sample Combres configuration file, and I'll explain what each element and attribute means.

The above file (optionally) defines the list of filters to be used to extend the standard behaviors of Combres. I'll explain what filters are later; for now, just forget about them for a minute.

The most important thing in the Combres configuration file is the resource sets definition. As I mentioned earlier, you can organize your JavaScript and CSS files into separate resource sets. This sample configuration file defines three resource sets: a CSS set and two JavaScript sets (notice the value of the type attribute of the resourceSet element). There is, of course, no limit to the number of resource sets you can define.

Each resource set contains one or more resources of the same type (CSS or JavaScript). All these files are to be combined, minified, gzipped, and cached together, as well as requested in one single HTTP request.

Let's look at the attributes of the resourceSet element:

Now, let's look at the attributes of the resource element.

Step 3. Register the route

Notice the attribute URL in the element resourceSets. Whatever value you specify here will be used by the ASP.NET routing engine to map with the Combres processing pipeline. In the example, I use combres.axd although you can use any name and extension (as long as you configure IIS to route that extension to ASP.NET; the axd extension is already configured out of the box). The next thing you need to do is register that route with the ASP.NET routing engine. Depending on whether you're working on an ASP.NET MVC or an ASP.NET WebForms application, the steps are a little bit different.

ASP.NET MVC

The routing module is registered by default for an ASP.NET MVC application, so there are only a few steps you need to do. In the routing registration routine in global.asax, invoke routes.AddCombresRoute("Combres"). (AddCombresRoute() is an extension method defined in a type in the Combres namespace, so you need to import the Combres namespace into your global.asax, i.e., "using Combes".) Note that since I use the axd extension, I need to put the call before routes.IgnoreRoute("{resource}.axd/{*pathInfo}"), which is added by default by the ASP.NET MVC project template; otherwise, the routing engine will ignore our Combres route.

mvc.asax.png

Combres is now ready to serve combined contents via the 'combres.axd' path. Next, in views that we need to use JS and CSS files, add the links to the resource sets. The following examples show an MVC view which uses two resource sets we defined in the configuration file:

mvc.view.png

CombresUrl and CombresLink are extension methods defined in the Combres namespace, therefore import this namespace to your page first. CombresUrl generates only the URL, while CombresLink generates the full script or link tag, depending on the type of the resource set. This is the generated code:

mvc.view.generated.png

Note the nice URLs generated: combres.axd is followed by the resource set name and its current version. Including version as part of the URL is one of the things Combres does in order to control the caching behavior of the client browser. That should be it. If you run your application and use tools like FireBugs to disassemble the HTTP request and response, say the request to the siteCss resource set, you'll see the following:

browser.headers.png

browser.response.png

There are a couple of things in the above screenshots that are worth noticing:

ASP.NET WebForm

As of ASP.NET 3.5 SP1, the routing module is not registered by default in the ASP.NET project template. Therefore, there are a number of steps you need to follow to register that module. There is a great step-by-step tutorial that shows you how to do this, so I won't repeat it here. Once the routing module is integrated into the ASP.NET pipeline, register the Combres route as follows:

form.asax.png

To use Combres in a ASPX web page, do as follows:

form.aspx.png

WebExtensions is a type defined in the Combres namespace, therefore import this namespace to your page first. Things will work exactly the same with the ASP.MVC example above.

Understanding filters

Filter is a mechanism enabled by Combes to help developers add custom logic to the standard Combres' processing pipeline via means of intercepting key transformation phases. In order to implement a filter, you implement the ICombresFilter interface by overriding its three methods:

filter.png

The diagram below shows how Combres interacts with filters during its processing pipeline, which is simplified in the diagram to show only the relevant parts. At each of the interception points, all the filters registered with Combres will be instantiated and invoked. Each of these filters has a chance to modify the output of the previous phase, and the modified content will be fed into the next phase.

filter.pipeline.png

At each interception point, an instance of a filter type is instantiated on the fly and one of its methods is invoked, depending on the current phase. This invocation idiom is to make it easy to develop custom filters: you can have instance variables if you want to, and you don't have to consider thread-safety when implementing your filters. You also don't have to worry about performance (for using a lot of Reflection here), because Fasterflect, another library I developed to turn Reflection invocations into close-to-native invocations, is used internally by Combres.

After developing a filter, you need to register it with Combres via the configuration file. (Now, you can refer back to the sample configuration file to look at the filters element.) Each filter needs to appear in a filter element with its type specified.

Combres ships with two built-in filters: HandleCssVariablesFilter and FixUrlsInCssFilter.

Let's look at what they do and how they are implemented.

HandleCssVariablesFilter

The idea of this filter comes from a blog post by Rory Neopoleon. Basically, it allows you to define variables in CSS files. For example, you can have a CSS with the following @define block:

@define
{
    boxColor: #345131;
    boxWidth: 150px;
}
p
{
    color: @boxColor;
    width: @boxWidth;
}

The filter will turn the content into the following:

p
{
    color: #345131;
    width: 150px;
}

This simple technique is a great way to refactor your CSS files. Let's look at the implementation of this filter.

public class HandleCssVariablesFilter : ICombresFilter
{
    public string TransformSingleContent(Settings settings, 
                  Resource resource, string content)
    {
        if (resource.ParentSet.Type != ResourceType.CSS)
            return content;

        // Remove comments because it may mess up the result
        content = Regex.Replace(content, @"/\*.+?\*/", "", 
                                RegexOptions.Singleline);
        var regex = new Regex(@"@define\s*{(?<define>.*?)}", 
                              RegexOptions.Singleline);
        var match = regex.Match(content);
        if (!match.Success)
            return content;

        var value = match.Groups["define"].Value;
        var variables = value.Split(';');
        var sb = new StringBuilder(content);
        variables.ToList().ForEach(var =>
                      {
                         if (string.Empty == var.Trim())
                             return;
                         var pair = var.Split(':');
                         sb.Replace("@" + pair[0].Trim(), pair[1].Trim());
                      });

        // Remove the variables declaration,
        // it's not needed in the final output
        sb.Replace(match.ToString(), string.Empty);
        return sb.ToString();
    }

    public string TransformCombinedContent(Settings settings, 
                  ResourceSet set, string content)
    {
        return content;
    }

    public string TransformMinifiedContent(Settings settings, 
                  ResourceSet set, string content)
    {
        return content;
    }
}

Note that TransformSingleContent() is overridden because we want this filter to work on a per CSS file basis. The other methods simply return the original input. Some might be scared by the amount of text manipulation done and its implication to performance. In reality, this is hardly a problem, thanks to the caching mechanism supported by Combres. Chances are that the processing is done once and the content is served from either the browser's or the server's cache for days or even months, depending on your configuration settings and how frequently you update your contents.

FixUrlsInCssFilter

This is a neat filter that addresses a problem many users of Combres reported. The problem is that URLs referenced in CSS files are interpreted by browsers as relative to the location of the CSS file. Because of that, when Combres is used to serve CSS content, unless the Combres' registered route starts in the same folder as the CSS file (unlikely, given that you might have a lot of CSS files in different folders in your application), the browser might not be able to resolve the correct paths to the referenced URLs (which result in issues like images not being shown). The FixUrlsInCssFilter is built to fix this problem. Even more than that, it allows you to use the standard ASP.NET partial part, e.g., URLs starting with '~/'.

For example, assume the path of the CSS file is ~/content/site.css, then the following transformations are done by the filter to each URL reference in the CSS file:

Let's look at how the filter is implemented:

public class FixUrlsInCssFilter : ICombresFilterreadonly {
    ILog Log = LogManager.GetLogger(
                   System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

    public string TransformSingleContent(Settings settings, 
                  Resource resource, string content)
    {
        if (resource.ParentSet.Type == ResourceType.CSS)
            return Regex.Replace(content, @"url\((?<url>.*?)\)", 
                match => FixUrl(resource.Path, match),
                RegexOptions.IgnoreCase | RegexOptions.Singleline | 
                RegexOptions.ExplicitCapture);
        return content;
    }

    private static string FixUrl(string cssPath, Match match)
    {
        try
        {
            const string template = "url(\"{0}\")";
            var url = match.Groups["url"].Value.Trim('\"', '\'');
            if (url.StartsWith("/"))
                return string.Format(template, url);
            if (url.StartsWith("~"))
                return string.Format(template, url.ResolveUrl());

            var cssFolder = cssPath.Substring(0, cssPath.LastIndexOf("/"));
            var backFolderCount = Regex.Matches(url, @"\.\./").Count;
            for (int i = 0; i < backFolderCount; i++)
            {
                url = url.Substring(3); // skip a '../'
                cssFolder = cssFolder.Substring(0, cssFolder.LastIndexOf("/"));
                // move back 1 folder
            }
            return string.Format(template, (cssFolder + "/" + url).ResolveUrl());
        }
        catch (Exception ex)
        {
            // Be lenient here, only log. After all,
            // this is just an image in the CSS file
            // and it should't be the reason to stop loading that CSS file.
            if (Log.IsWarnEnabled) 
                Log.Warn("Cannot fix url " + match.Value, ex);
            return match.Value;
        }
    }

    public string TransformCombinedContent(Settings settings, 
                  ResourceSet set, string content)
    {
        return content;
    }

    public string TransformMinifiedContent(Settings settings, 
                  ResourceSet set, string content)
    {
        return content;
    }
}

Conclusion

In this article, I have shown how easy it is to use Combres to employ many website performance optimization techniques in your ASP.NET and ASP.NET MVC applications. I have also demonstrated the extensibility of Combres via the filtering mechanism. With this mechanism in place, you can easily extend Combres to fit your own needs.

The source code of Combres comes with two sample applications, an ASP.NET MVC and an ASP.NET WebForm. You can take the configuration and code in these samples to quickly get your web application up and running with Combres.

I love to hear from you. Either post your comments here or in the project's discussion board at CodePlex.

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralI guest it does not work in Azure
Thoại Nguyễn
18:14 2 Nov '09  
Hi, Combres seems to be what I need now. However, I think it might not work in Azure since it's use an external XML file to store the configuration. Thanks anyway Big Grin
GeneralRe: I guest it does not work in Azure
Buu Nguyen
19:16 2 Nov '09  
I haven't got a chance to look at Azure, so I don't know whether it works there or not and if not, how much work it would take to get there. But support for Azure is definitely something I want to do.



Last Updated 2 Nov 2009 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010