Recently in a post on Bundling and Minification in MVC4 by Rick Anderson, Rick explains how to use the new ASP.NET Web Optimization Framework features (note: this feature can be used with a Web Pages Project, MVC4 Project or Web Forms Project).
If you just want to browse the code and not read, here you go:
Download MVC4 Bootstrap .Less Demo Project
Before ASP.NET Web Optimization Framework
There have always been ways to minify your Css and JavaScript for optimizing ASP.net sites in the past but developers are lazy and sometimes extra steps need to be automated or easier to implement.
There are tools like:
All of the above work fine and you can still use them instead of the ASP.NET Web Optimization Framework.
Using Web Optimization Framework with .Less and Twitter Bootstrap 2.1
Out of the box, the Web Optimization Framework has CssMinify and JsMinify bundle transformers but CssMinify does not support .less files. This is where the IBundleTransform interface comes in. You can implement this to provide transformations to .less files. Rick's tutorial explains the basics of implementing dotLess and IBundleTransform for less files but the problem is that this solution does not work with Twitter's Bootstrap .less Source. The .less source uses @import rules to import multiple files and the dotLess parser has no way of finding the location of the files. THe following code will demostrate how to overcome this problem:
ImportedFilePathResolver.cs - this class helps find the @import file:
using System.IO;
using System.Web;
using System.Web.Hosting;
using dotless.Core.Input;
public class ImportedFilePathResolver : IPathResolver
{
private string _currentFileDirectory;
private string _currentFilePath;
public ImportedFilePathResolver(string currentFilePath)
{
CurrentFilePath = currentFilePath;
}
///
/// Gets or sets the path to the currently processed file.
///
public string CurrentFilePath
{
get { return _currentFilePath; }
set
{
_currentFilePath = value;
_currentFileDirectory = Path.GetDirectoryName(value);
}
}
///
/// Returns the absolute path for the specified improted file path.
///
/// The imported file path.
public string GetFullPath(string filePath)
{
filePath = filePath.Replace('\\', '/').Trim();
if (filePath.StartsWith("~"))
{
filePath = VirtualPathUtility.ToAbsolute(filePath);
}
if (filePath.StartsWith("/"))
{
filePath = HostingEnvironment.MapPath(filePath);
}
else if (!Path.IsPathRooted(filePath))
{
filePath = Path.Combine(_currentFileDirectory, filePath);
}
return filePath;
}
}
LessMinify.cs - class to parse .less files and transform them:
using System;
using System.IO;
using System.Text;
using System.Web;
using System.Web.Optimization;
using dotless.Core;
using dotless.Core.Abstractions;
using dotless.Core.Importers;
using dotless.Core.Input;
using dotless.Core.Loggers;
using dotless.Core.Parser;
public class LessMinify : IBundleTransform
{
public void Process(BundleContext context, BundleResponse bundle)
{
if (bundle == null)
{
throw new ArgumentNullException("bundle");
}
context.HttpContext.Response.Cache.SetLastModifiedFromFileDependencies();
var lessParser = new Parser();
ILessEngine lessEngine = CreateLessEngine(lessParser);
var content = new StringBuilder(bundle.Content.Length);
foreach (FileInfo file in bundle.Files)
{
SetCurrentFilePath(lessParser, file.FullName);
string source = File.ReadAllText(file.FullName);
content.Append(lessEngine.TransformToCss(source, file.FullName));
content.AppendLine();
AddFileDependencies(lessParser);
}
bundle.ContentType = "text/css";
bundle.Content = content.ToString();
}
///
/// Creates an instance of LESS engine.
///
/// The LESS parser.
private ILessEngine CreateLessEngine(Parser lessParser)
{
var logger = new AspNetTraceLogger(LogLevel.Debug, new Http());
return new LessEngine(lessParser, logger, true, false);
}
///
/// Adds imported files to the collection of files on which the current response is dependent.
///
/// The LESS parser.
private void AddFileDependencies(Parser lessParser)
{
IPathResolver pathResolver = GetPathResolver(lessParser);
foreach (string importedFilePath in lessParser.Importer.Imports)
{
string fullPath = pathResolver.GetFullPath(importedFilePath);
HttpContext.Current.Response.AddFileDependency(fullPath);
}
lessParser.Importer.Imports.Clear();
}
///
/// Returns an instance used by the specified LESS lessParser.
///
/// The LESS prser.
private IPathResolver GetPathResolver(Parser lessParser)
{
var importer = lessParser.Importer as Importer;
if (importer != null)
{
var fileReader = importer.FileReader as FileReader;
if (fileReader != null)
{
return fileReader.PathResolver;
}
}
return null;
}
///
/// Informs the LESS parser about the path to the currently processed file.
/// This is done by using custom implementation.
///
/// The LESS parser.
/// The path to the currently processed file.
private void SetCurrentFilePath(Parser lessParser, string currentFilePath)
{
var importer = lessParser.Importer as Importer;
if (importer != null)
{
var fileReader = importer.FileReader as FileReader;
if (fileReader == null)
{
importer.FileReader = fileReader = new FileReader();
}
var pathResolver = fileReader.PathResolver as ImportedFilePathResolver;
if (pathResolver != null)
{
pathResolver.CurrentFilePath = currentFilePath;
}
else
{
fileReader.PathResolver = new ImportedFilePathResolver(currentFilePath);
}
}
else
{
throw new InvalidOperationException("Unexpected importer type on dotless parser");
}
}
}
Using LessMinify class in App_Start/BundleConfig.cs
using System.Web.Optimization;
public class BundleConfig
{
public static void RegisterBundles(BundleCollection bundles)
{
// remove other code for brevity
var defaultCss = new StyleBundle("~/bundles/css").Include(
"~/Content/less/bootstrap.less",
"~/Content/less/responsive.less",
"~/Content/Prettify/prettify.css",
"~/Content/docs.css");
defaultCss.Transforms.Add(new LessMinify());
//defaultCss.Transforms.Add(new CssMinify());
bundles.Add(defaultCss);
var defaultJs = new ScriptBundle("~/bundles/js").Include(
"~/Scripts/bootstrap.js",
"~/Scripts/Prettify/prettify.js",
"~/Scripts/application.js");
defaultJs.Transforms.Add(new JsMinify());
bundles.Add(defaultJs);
BundleTable.EnableOptimizations = true;
}
}
GitHub Project
I went ahead and created a GitHub project with to showcase the code listed above. The project includes all of the demo pages from twitter bootstrap but MVC'ied. Also includes the OAuth login page, Ninject (even though it isn't being utilized), T4MVC and WebActivator. Make sure after you download the project and open in VS2012 to go to "Manage Nuget Packages" and "Restore Nuget packages" to download all referenced packages. To use the OAuth logins, open App_Start/OAuthConfig.cs and edit the settings. If you have any issues or features, submit them here.

