Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Combres 2.0 - A Library for ASP.NET Website Optimization

0.00/5 (No votes)
2 May 2010 2  
.NET library which enables combination, minification, compression, and caching of JavaScript and CSS resources for ASP.NET Web Forms and ASP.NET MVC web applications.

Introduction

A few months ago, I released the beta version of Combres 1.0, a .NET library that automates the application of many web performance optimization techniques for ASP.NET applications. I also wrote an article about that release to demonstrate the features of the library. Since that article was published, there have been a couple of minor releases until last week when a major release, version 2.0, was out. There are many changes in version 2.0 that it wouldn't make sense to update the old article, so I decide to write this article to introduce readers to Combres 2.0. This is supposed to be a self-contained article, so you don't need to refer to the old article to understand about Combres.  

Combres in a Nutshell

The development of Combres was inspired by the simple, yet highly effective, website optimization techniques described in a book by Steve Sounders and the documentation of the FireFox's addon YSlow. Specifically, Combres automates the application of the following website optimization techniques in your ASP.NET MVC or ASP.NET Web Forms applications while requiring you to do very little work.

  • Make fewer HTTP requests. Using Combres, you describe your website's resources, including JavaScript and CSS files, in an XML config file and group them into different resource sets. Combres will combine resources in the same resource set and make the combined content available in 1 single HTTP request.
  • Add Expires or Cache-Control header. Combres automatically emits Expires and Cache-Control response headers in responding to the HTTP request for each resource set based on the caching information you specify in the XML config file. In addition, Combres caches the combined content on the server so that the combination process, among other steps described below, won't be executed for every new user (or when an existing user's browser cache is invalidated).
  • Gzip components. Combres will detect Gzip and/or Deflate support in the users' browser and apply the appropriate compression algorithm on each resource set's combined content before sending it to the browser. If the browser doesn't support compression, Combres will return the raw output instead.
  • Minify JavaScript. Combres can minify the contents of both JavaScript and CSS resources. For JavaScript resources, you can configure Combres to choose among the following minification engines: YUI Compressor for .NET, Microsoft Ajax Minifier and Google Closure Compiler. For each of these engines, Combres allows you to configure all specific attributes so that you can maximize its effectiveness. Each resource set is usually assigned with a specific minification engine although if you need to, you can have resources within the same resource set minified using separate minification engines.
  • Configure ETags. Combres emits ETags for each resource set's combined content. When the browser sends back an ETag, Combres will check to see if that ETag is identifying the latest version of the resource set or not, if not it will push the new content to the browser; otherwise, it will return a Not Modified (304) response status.

In short, Combres helps combine, minify, compress, append appropriate headers and cache JavaScript and CSS resources in your application. All you need to do are creating an XML config file describing what you want Combres to do and adding a few lines of code to register and use Combres in your applications. In this article, we'll explore these core features as well as more advanced features of Combres.

Deploy Combres via NuGet (added 1/25/2011)

Combres can now be deployed via NuGet (http://nuget.codeplex.com/). Refer to this page (http://combres.codeplex.com/wikipage?title=5-Minute%20Quick%20Start%20for%20the%20Impatient) for more information. The content in the Using Combres section is still highly relevant for those who want to get beyond the basics.

Using Combres

Sample Web Application

I will use a very simple ASP.NET Web Forms application to demonstrate the features of Combres. Let's say we have an application with the following structure:

The page Default.aspx references the JavaScript and CSS files as follows:

<head runat="server">
    <title>Using Combres</title>
    <link type="text/css" rel="stylesheet" href="Styles/Site.css" />
    <link type="text/css" rel="stylesheet" href="Styles/jquery-ui-1.7.2.custom.css" />
    <script type="text/javascript" src="Scripts/jquery-1.3.2.js"></script>
    <script type="text/javascript" src="Scripts/jquery-ui-1.7.2.custom.min.js"></script>
</head>

If you run this page and examine the HTTP requests made by the browser, you can see that besides the HTTP request for the page itself, there are also 4 other HTTP requests corresponding to the 4 JavaScript and CSS files. The picture below shows the HTTP requests and corresponding responses traced by the FireBugs addon.

Fig2_Requests.png

There are a couple of interesting things as we look closer at any of these HTTP requests and responses.

Firstly, the missing of the Content-Encoding response header with either Gzip or Deflate value means that the content of each resource is not compressed by the server before sending to the browser, although the browser indicates in the Accept-Encoding request header that it can accept Gzip or Deflate stream.

In addition, Expires, Cache-Control and ETag headers aren't generated by the web server at all.

Finally, if you look at the actual response of a particular JavaScript or CSS file (for example, one shown in the picture below), you'll see its content is sent in raw form with full comments, tabs, and whitespaces, etc. The result of all these is that more bandwidth is consumed and the browser isn't properly instructed how to best cache resources' contents.

Fig3_Response.png

Now, let's use Combres to address these issues.

Step 1 - Creating Combres Config File

First, reference the Combres.dll in your application. (This is a merged assembly included in the binary download; Combres actually depends on some 3rd-party assemblies so if you don't use the merged assembly, you need to add reference to those 3rd-party libraries as well.) After that, add an XML file named Combres.xml and place it in the App_Data folder (you can use other file name and folder if you want). Now, to facilitate the work on the XML file, in the toolbar of Visual Studio .NET, select XML > Schemas. Click Add and choose the combres.xsd schema file, which is bundled in the Combres' download. Doing that will help bring Intellisense support when editing Combres' config file.

Fig4_Schema.png

Add the following content into Combres.xml:

<?xml version="1.0" encoding="utf-8" ?>
<combres xmlns='urn:combres'>
  <resourceSets url="~/combres.axd" defaultDuration="30" 
                                defaultVersion="auto" 
                                defaultDebugEnabled="auto" >
    <resourceSet name="siteCss" type="css">
      <resource path="~/styles/site.css" />
      <resource path="~/styles/jquery-ui-1.7.2.custom.css" />
    </resourceSet>
    <resourceSet name="siteJs" type="js">
      <resource path="~/scripts/jquery-1.3.2.js" />
      <resource path="~/scripts/jquery-ui-1.7.2.custom.min.js" />
    </resourceSet>
  </resourceSets>
</combres>	

The above XML file defines 2 resource sets, each with 2 resources. You can add as many resource sets and resources as you want in your application. The attributes of the resourceSets element need explanation.

The url attribute specifies the starting URL of the Combres content generator. The defaultDuration attribute defines the number of days which Combres uses to instruct the browser and Combres itself to cache the content. In order to invalidate the caches at client side and server side, Combres will need the requested URL of the resource set to be changed. Here, we instruct Combres to detect changes to resource sets (e.g. resource contents, configuration settings etc.) and generate a new URL automatically by specifying auto for the defaultVersion attribute. Finally, we want Combres to reuse the debug mode specified in web.config, so we use auto as the value for defaultDebugEnabled (more on debugging later). These default values (including the values of all other attributes whose name start with 'default' mentioned in this article) will be used for all resource sets although any resource set can override these values as needed.

Advanced Usages

External and Dynamically Generated Resources

If a JavaScript file or CSS file is external from your application, you need to reference it as follows:

<resource path="http://sub.domain.com/jquery-1.3.2.js" mode="dynamic" />

Note that you have to specify the full URL and change the mode to dynamic. This tells Combres to open an HTTP request to download the required resource, instead of attempting to read it from the file system. Likewise, if you have a local resource which is dynamically generated at runtime, say, by some HTTP handler, then you need to use the dynamic mode as well. For example:

<resource path="~/generator.aspx?file=jquery-1.3.2.js" mode="dynamic" /> 
Sharing Resources in a Resource Set

If you have a large site in which there are resource sets sharing some common resources, instead of repeating these resource declarations in multiple resource sets, you can define a common resource set and have it referenced by other resource sets. For example:

<resourceSet name="commonJs" type="js">
    <!-- some resources -->
</resourceSet>
<resourceSet name="anotherSet" type="js">
    <!-- some resources -->
    <resource reference="commonJs"/>
    <!-- some resources -->
</resourceSet>
<resourceSet name="yetAnotherSet" type="js">
    <!-- some resources -->
    <resource reference="commonJs"/>
    <!-- some resources -->
</resourceSet>
Disable Compression

If you don't want Combres to compress the content for you (usually because you rely on another compression mechanism such as hardware-based compression), then you can just set defaultCompressionEnabled to false.

Step 2 - Modify web.config

The next step is modifying web.config to do 2 things:

  1. Add a config section for Combres and in that section specify the location of the Combres config file.
     <configSections>
          <section name="combres" type="Combres.ConfigSectionSetting, Combres, 
    	Version=2.0.0.0, Culture=neutral, PublicKeyToken=49212d24adfbe4b4"/>
     </configSections>
     <combres definitionUrl="~/App_Data/combres.xml"/>
  2. Register the UrlRoutingModule, which is an HTTP module that Combres depends on for registering its route.
    <httpModules>
            <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, 
    	System.Web.Extensions, Version=3.5.0.0, Culture=neutral, 
    	PublicKeyToken=31BF3856AD364E35"/>
            <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, 
    	System.Web.Routing, Version=3.5.0.0, Culture=neutral, 
    	PublicKeyToken=31BF3856AD364E35"/>
    </httpModules>

Step 3 - Register Combres Route

Add the global application file (global.ascx) and make sure its code-behind file has the following contents. (Note that AddCombresRoute is an extension method defined in the Combres namespace.)

using System;
using System.Web.Routing;
using Combres;
	
namespace UsingCombres
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.AddCombresRoute("Combres Route");
        }
    }
}

Step 4 - Generate Combres Links

In this final step, we need to change Default.aspx to use Combres API to generate the links to resource sets. Note that the parameters we pass to the method CombresLink() are the names of the resource sets as we defined in Combres.xml.

<%@ Import Namespace="Combres" %>
<head runat="server">
    <title>Using Combres</title>
    <%= WebExtensions.CombresLink("siteCss") %>
    <%= WebExtensions.CombresLink("siteJs") %>
</head>

That's it. Run your application and watch the HTTP requests under FireBugs. You'll see the following capture:

Fig5_Combres.png

Let's discuss the effects of applying Combres. First, instead of having 4 HTTP requests corresponding to 4 different resources as previously, we now only have 2 HTTP requests corresponding to the 2 resource sets we define. Second, the content of each resource set is gzipped before sending to the browser, as indicated via the value of the Content-Encoding response header. Also, proper Cache-Control and Expires headers are generated based on the value you specify in defaultDuration in the Combres.xml file. An ETag is also generated by Combres so that the browser can use to check for newer content.

If you look at the actual response, you'll see the content of each resource set is minified, i.e. having comments and unnecessary whitespaces removed. In fact, the JavaScript minifiers used by Combres go as far shortening variable names, inlining function calls and removing unused code. A quick maths will show that the new page only has to download 80.4 KB of resources, which is less than one fourth of the original of 344.2KB.

Fig6_CombresResponse.png

Advanced Usages

ASP.NET MVC applications can integrate with Combres following the above steps. However, if you add reference to the assembly Combres.Mvc.dll, then you can call CombresLink() extension method (and its overloads) from either UrlHelper or HtmlHelper instead of directly calling into WebExtensions class. If you do this, the code to generate the Combres links will look as follows:

<%= Url.CombresLink("siteCss") %>
<%= Url.CombresLink("siteJs") %>

If you have to deploy your ASPX pages in environments where no inline code is allowed (e.g. CompilationMode set to true, CMS etc.), you can use the Include custom server control to generate the Combres links instead. For example:

<%@ Register Assembly="Combres" Namespace="Combres" TagPrefix="cb" %>
<cb:Include ID="siteCssCtrl" runat="server" SetName="siteCss"/>
<cb:Include ID="siteJsCtrl" runat="server" SetName="siteJs"/>

Advanced Topics

Version Generator

If you look at the URL Combres generated for each resource set, at the end there's a bunch of random numbers appended to it. This is a 32-bit integer-based version string Combres automatically generates by hashing different elements of a resource set such as resources' contents, applied filters and minification settings, etc. (More on filters and minification settings later.) This is to make sure any change to any resource file or any relevant setting will force a cache invalidation at both the client and server. If the possibility of a hash collision scares you, you can employ a more "spacy" built-in version generator, Sha512VersionGenerator, which generates 512-bit version strings, as follows:

 <resourceSets url="~/combres.axd" defaultDuration="30" 
                                    defaultVersion="auto" 
                                    defaultDebugEnabled="auto"
                                    defaultVersionGenerator=
				"Combres.VersionGenerators.Sha512VersionGenerator" >

If you are not happy with the 2 built-in version generators, you can always plug-in your own implementation here by creating a class implementing Combres.IVersionGenerator and then replace the value of the defaultVersionGenerator attribute with the fully-qualified name of your class. If you totally don't want Combres to manage the version automatically, you can always perform manual versioning by changing the defaultVersion to anything but auto. Doing this, Combres won't try to detect changes and adjust the version anymore, you are to control this process yourself.

The change detection mechanism in Combres works by using FileSystemWatcher to monitor changes in Combres config file, JavaScript and CSS files. This is a pretty lightweight change detection mechanism. However, when coming to external and dynamically generated resources, this mechanism won't work. Instead, Combres needs to continuously retrieve the resources' contents and compare with existing contents in Combres' internal content cache. This is an expensive operation and should be optimized per application. Therefore, Combres exposes 2 attributes, localChangeMonitorInterval and remoteChangeMonitorInterval, which indicate the number of seconds Combres will wait before performing a lookup-and-compare operation for dynamically generated resources and external resources respectively.

Minifiers

By default, Combres uses the YUI .NET Compressor with some default settings for both CSS and JavaScript minification. You can create minifier instances using different minification engine and/or settings by declaring them in Combres.xml. After declaring those, you can apply each of these minifier instances for all resource sets, individual resource set, or even individual resource in a set.

Go back to our example, let's say we want to turn off minification for jquery-ui-1.7.2.custom.min.js and use Microsoft Ajax Minifier to minify jquery-1.3.2.js. In addition, we want to use some custom settings for the YUI CSS Compressor. We can change Combres.xml to become:

<?xml version="1.0" encoding="utf-8" ?>
<combres xmlns='urn:combres'>
  <cssMinifiers>
    <minifier name="yui" type="Combres.Minifiers.YuiCssMinifier, Combres">
      <param name="CssCompressionType" type="string" value="StockYuiCompressor" />
      <param name="ColumnWidth" type="int" value="-1" />
    </minifier>
  </cssMinifiers>
  <jsMinifiers>
    <minifier name="msajax" type="Combres.Minifiers.MSAjaxJSMinifier, Combres" 
	binderType="Combres.Binders.SimpleObjectBinder, Combres">
      <param name="CollapseToLiteral" type="bool" value="true" />
      <param name="EvalsAreSafe" type="bool" value="true" />
      <param name="MacSafariQuirks" type="bool" value="true" />
      <param name="CatchAsLocal" type="bool" value="true" />
      <param name="LocalRenaming" type="string" value="CrunchAll" />
      <param name="OutputMode" type="string" value="SingleLine" />
      <param name="RemoveUnneededCode" type="bool" value="true" />
      <param name="StripDebugStatements" type="bool" value="true" />
    </minifier>
  </jsMinifiers>
  <resourceSets url="~/combres.axd" defaultDuration="30" 
                                defaultVersion="auto" 
                                defaultDebugEnabled="auto" >
    <resourceSet name="siteCss" type="css" minifierRef="yui">
      <resource path="~/styles/site.css" />
      <resource path="~/styles/jquery-ui-1.7.2.custom.css" />
    </resourceSet>
    <resourceSet name="siteJs" type="js">
      <resource path="~/scripts/jquery-1.3.2.js" minifierRef="msajax"  />
      <resource path="~/scripts/jquery-ui-1.7.2.custom.min.js" minifierRef="off" />
    </resourceSet>
  </resourceSets>
</combres>

To understand exactly what each attribute of a minification engine does, please refer to the documentation for each particular minifier type (e.g. MSAjaxJSMinifier). You can also add your own custom minifier by creating a class implementing the Combres.IResourceMinifier interface.

Debugging Support

While the processing workflow performed by Combres is useful for web applications in production environment, it may cause a lot of inconveniences in development environment because it is almost impossible to understand (or debug) combined and minified JavaScript and CSS resource sets. To facilitate debugging, you can turn on defaultDebugEnabled setting for all resource sets or debugEnabled setting for individual resource set. As mentioned earlier, if this is set to auto, then Combres will use the current debug setting in web.config. For each resource set with debugEnabled being true, resources are combined but not minified or cached at all. Combres also appends a comment before each resource's content in the combined content to help identify the location of that resource.

While the above makes debugging somewhat easier, it's still not as easy as when you debug your JavaScript code or CSS content without using Combres. Fortunately, in Combres 2.0 you can set the value of a new attribute defaultIgnorePipelineWhenDebug to true, which will force Combres to generate direct links to the resources so that the whole Combres processing pipeline is bypassed. The HTML generated would then look exactly as the one generated if you were not using Combres at all. (The implication of this is that no Combres filter is executed for these resources.)

Logging Support

A useful utility to monitor Combres' internal processing is the Combres log. Logging mechanism in Combres is implemented in a way that a custom log provider can be plugged in easily. Currently there is only one built-in log provider, Log4Net. All you need to do to enable this log provider is modifying the relevant sections in web.config so that it looks like this:

    <configSections>
      <section name="combres" type="Combres.ConfigSectionSetting, 
	Combres, Version=2.0.0.0, 
	Culture=neutral, PublicKeyToken=49212d24adfbe4b4"/>
      <section name="log4net" 
	type="log4net.Config.Log4NetConfigurationSectionHandler, Combres, 
	Version=2.0.0.0, Culture=neutral, PublicKeyToken=49212d24adfbe4b4"/>
    </configSections>
    <combres definitionUrl="~/App_Data/combres.xml" 
	logProvider="Combres.Loggers.Log4NetLogger" />
    <log4net>
      <root>
        <level value="ALL"/>
        <appender-ref ref="RollingFile"/>
      </root>
      <logger name="Combres">
        <level value="DEBUG"/>
      </logger>
      <appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
        <file value="log.txt"/>
        <appendToFile value="true"/>
        <maximumFileSize value="100KB"/>
        <maxSizeRollBackups value="2"/>
        <layout type="log4net.Layout.PatternLayout">
          <conversionPattern value="%d [%t] %-5p %c - %m%n"/>
        </layout>
      </appender>
    </log4net>

Now, if you run your application, you should see the file log.txt created and populated with many logging statements by Combres.

Filters

Of all the hooks exposed by Combres, nothing is as powerful as filter. Combres provides 4 filter types, each of them is invoked at a specific time during the Combres processing pipeline. Filter classes must implement one or more of the following interfaces (except from the base interface IContentFilter):

Fig7_Filters.png

CanApplyTo(), defined in IContentFilter, receives a ResourceType enum (which has only 2 values, JS and CSS) and returns true if the filter implementation supports this resource type and returns false otherwise. The TransformContent() method of each filter interface has a unique signature but essentially does the same thing: accepting some content, performing some processing and returning a transformed content.

To understand how these filter types work, we need to understand the Combres processing workflow. For each resource set that is not in debug mode, Combres starts by partitioning its resources into multiple minification groups, each associated with the same minifier instance. (If there's only one minifier instance configured for all resources in the set, then there's only one minification group.) For each minification group, the below steps will be performed by Combres. (The output at each step will be used as input for the next step.)

  • Read the content of every resource in the set, invoking filters implementing ISingleContentFilter for each read, passing in the resource's content and adding the returned content into an array.
  • Combine the strings in the array of the previous step, invoking filters implementing ICombinedContentFilter with the combined content and retrieving the transformed combined content.
  • Minify the combined content, invoking filters implementing IMinifiedContentFilter with the minified content and retrieving the transformed minified content.

Finally, once having the minified contents of all minification groups available, Combres merges them together, compressing the merged content and invoking filters implementing ICompressedContentFilter with the compressed content and using the transformed compressed content to send to the browser.

For resource set in debug mode, the workflow is a bit different. The steps to be performed for each resource set include:

  • Read the content of every resource in the set, invoking filters implementing ISingleContentFilter for each read, passing in the resource's content and adding the returned content into an array
  • Combine the strings in the array of the previous step, invoking filters implementing ICombinedContentFilter with the combined content and retrieving the transformed combined content
  • Compress the combined content, invoking filters implementing ICompressedContentFilter with the compressed content and using the transformed compress content to send to browser.

Out of the box, Combres has 3 built-in filters: FixUrlsInCssFilter, HandleCssVariablesFilter and DotLessCssFilter. Let's look at each of them.

FixUrlsInCssFilter

This filter implements ISingleContentFilter and addresses the problem when relative URLs in your CSS files are no longer valid when Combres is used. That is because these URLs are now relative to the Combres generator, not the CSS URLs anymore and unless these CSS files happen to locate in the same path as the Combres generator, the browser wouldn't be able to resolve the referenced URLs. To address this, the filter modifies the all the referenced relative URLs in CSS resources to make them absolute to the application root path. Basically a no-brainer, just apply the filter and your CSS path problem disappears. Even more than that, the filter allows you to user the standard ASP.NET partial path syntax, ~/ so that the URLs work seamlessly with virtual applications.

The code for this filter is as follows:

public sealed class FixUrlsInCssFilter : ISingleContentFilter
{
    private static readonly ILogger Log = LoggerFactory.CreateLogger(
                   System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
    /// <inheritdoc cref="IContentFilter.CanApplyTo" />
    public bool CanApplyTo(ResourceType resourceType)
    {
        return resourceType == ResourceType.CSS;
    }

    /// <inheritdoc cref="ISingleContentFilter.TransformContent" />
    public string TransformContent(ResourceSet resourceSet, 
	Resource resource, string content)
    {
        return Regex.Replace(content, @"url\((?<url>.*?)\)",
            match => FixUrl(resource, match),
            RegexOptions.IgnoreCase | RegexOptions.Singleline | 
			RegexOptions.ExplicitCapture);
    }

    private static string FixUrl(Resource resource, Match match)
    {
        try
        {
            const string template = "url(\"{0}\")";
            bool isInSameApp = resource.IsInSameApplication;
            var url = match.Groups["url"].Value.Trim('\"', '\'');

            // Absolute URL, return as-is
            if (url.StartsWith("http", true, CultureInfo.InvariantCulture))
                return string.Format(CultureInfo.InvariantCulture, template, url);

            if (url.StartsWith("~", StringComparison.Ordinal))
            {
                // The CSS is in the same application 
                // resolve partial URLs found in the CSS to full relative paths
                if (isInSameApp)
                    return string.Format(CultureInfo.InvariantCulture, 
			template, url.ResolveUrl());
                
                /* Otherwise, attempt to treat ~ as /
                 * 
                 * @NOTE: This won't work if the remote app is in a virtual directory
                 * See my comment dated 11:00PM Monday Nov 23 in this discussion
                 * http://combres.codeplex.com/Thread/View.aspx?ThreadId=64366
                 */
                url = "/" + url.Substring(2); // 2 for the "~/"
            }

            var cssPath = resource.Path;
            if (url.StartsWith("/", StringComparison.Ordinal))
            {
                // The CSS is in the same application, keep root-based URLs as-is
                if (isInSameApp)
                    return string.Format(CultureInfo.InvariantCulture, template, url);

                // Otherwise, append root URL of the remote server/app to this url object
                var uri = new Uri(cssPath);
                return string.Format(CultureInfo.InvariantCulture, 
			template, uri.GetBase() + url);
            }

            // Relative URL in CSS mean relative to the CSS location
            // Because CSS location must either be ~/ or absolute, the ResolveUrl() 
            // at the end of this code block will do 
            var cssFolder = cssPath.Substring(0, cssPath.LastIndexOf
		("/", StringComparison.Ordinal)); // e.g. ~/content/css
            while (url.StartsWith("../", StringComparison.Ordinal))
            {
                url = url.Substring(3); // skip one '../'
                cssFolder = cssFolder.Substring(0, cssFolder.LastIndexOf
		("/", StringComparison.Ordinal)); // move back one folder
            }

            return string.Format(CultureInfo.InvariantCulture, 
		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;
        }
    }
}

HandleCssVariablesFilter

This is an ISingleContentFilter filter which allows you to define variables in a particular CSS file and reuse them throughout the file. 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 above content into the following:

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

This simple technique is a great way to refactor your CSS files. The implementation of this filter is as follows:

public sealed class HandleCssVariablesFilter : ISingleContentFilter
{
    /// <inheritdoc cref="IContentFilter.CanApplyTo" />
    public bool CanApplyTo(ResourceType resourceType)
    {
        return resourceType == ResourceType.CSS;
    }

    /// <inheritdoc cref="ISingleContentFilter.TransformContent" />
    public string TransformContent(ResourceSet resourceSet, 
	Resource resource, string content)
    {
        // Remove comments because they may mess up the result
        content = Regex.Replace(content, @"/\*.*?\*/", 
		string.Empty, 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(variable =>
                      {
                         if (string.IsNullOrEmpty(variable.Trim()))
                             return;
                         var pair = variable.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();
    }
}

DotLessCssFilter

This ISingleContentFilter filter is added in 2.0. It makes CSS resources go through the dotLessCSS processor which offers many useful features including variable declaration, expressions, and mixins, etc. Refer to the documentation of dotLessCSS for more information about these features. (Although these features cover what is provided by HandleCssVariablesFilter, HandleCssVariablesFilter is more lightweight than DotLessCssFilter so the former can still be useful at times.)

The code for DotLessCssFilter is as follows:

public sealed class DotLessCssFilter : ISingleContentFilter
{
    private static readonly DotlessConfiguration Config = new DotlessConfiguration
                                                            {
                                                                CacheEnabled = false,
                                                                MinifyOutput = false
                                                            };

    private static readonly EngineFactory EngineFactory = new EngineFactory();

    /// <inheritdoc cref="IContentFilter.CanApplyTo" />
    public bool CanApplyTo(ResourceType resourceType)
    {
        return resourceType == ResourceType.CSS;
    }

    /// <inheritdoc cref="ISingleContentFilter.TransformContent" />
    public string TransformContent(ResourceSet resourceSet, 
	Resource resource, string content)
    {
        var tmpFile = Path.GetTempFileName();
        File.WriteAllText(tmpFile, content);
        try
        {
            return EngineFactory.GetEngine(Config).TransformToCss(tmpFile);
        } 
        finally
        {
            File.Delete(tmpFile);
        }
    }
}

Using Filters

In order to apply a filter to your resource sets, you need to modify your Combres config file. For example, you can change Combres.xml to look like:

<combres xmlns='urn:combres'>
  <filters>
    <filter type="Combres.Filters.FixUrlsInCssFilter, Combres" />
    <filter type="Combres.Filters.DotLessCssFilter, Combres" 
	acceptedResourceSets="dotLessCss" />
  </filters>
	...

By default, Combres will invoke the method CanApplyTo() of each filter implementation to determine whether a resource set should be processed or not. However, you can have finer control over that process by specifying a list of resource set names in the acceptedResourceSets attribute of a filter, as can be seen above, and Combres will make sure only those resource sets will go through that filter.

Want to Know More?

There are some other minor features of Combres that I don't discuss in this article. If you want to learn about those features, you can refer to the annotated Combres config file (combres_full_with_annotation.xml) bundled in this download. This file describes all the elements and attributes you can add into a Combres config file and their effects. On the other hand, the CHM API documentation should come in handy for those who want to explore the public API of Combres and/or extend it with new minifiers, filters, and loggers.

Conclusion

I hope that this article has provided enough information for you to get started using Combres. If you have any feedback or suggestion, feel free to leave a comment here or in the discussion forum of the project CodePlex page. Thanks and enjoy your ride!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here