Thursday, June 18, 2015

Exception Handling in ASP.NET MVC

Exception Handling in ASP.NET MVC


Learn about the HandleError filter and discuss about the different exception handling mechanisms that ill fit to an MVC application.

Index

  1. Introduction
  2. HandleErrorAttribute
  3. Limitations of HandleError
  4. HandleError vs Application_Error
  5. Extending HandleError
  6. Returning views from Application_Error
  7. ELMAH
  8. Summary

Introduction

Exception handling is a serious matter in any application, whether it's web or desktop. Implementing a proper exception handling is important in any application. In most cases once we catch the exception we have to log the exception details to database or text file and show a friendly message to the user.
In ASP.NET applications, error handling is done mostly in two ways: at local level using try-catch blocks and at global level using application events. ASP.NET MVC comes with some built-in support for exception handling through exception filters. The HandleError is the default built-in exception filter. Unfortunately, theHandleError filter not gives a complete answer to the exception handling problem and that makes us to still rely on the Application_Error event.
In this article, we will learn about the HandleError filter and discuss about the different exception handling mechanisms that will fit to an MVC application.

HandleErrorAttribute

Exception filters

The exception filters are attributes that can be applied over an action or a controller or even at a global level. When you apply the filter at the global level then it will handle the exceptions raised by all the actions of all the controllers. The exception filters not only catches the exceptions that are raised by the actions but also the ones that are raised by the action filters that are applied over that action.
All the exception filters implements the IExceptionFilter interface. The Listing 1. shows the definition of this interface. The IExceptionFilter contains a single method called OnException which will be called whenever an exception occurs. The ExceptionContext parameter which derives from ControllerContext provides access to controller, route data and HttpContext.
public interface IExceptionFilter
{
    void OnException(ExceptionContext filterContext);
}
Listing 1. IExceptionFilter definition
The HandleErrorAttribute is the default implementation of the IExceptionFilter. When you create a MVC application you will see the HandleErrorAttribute is added to the GlobalFiltersCollection in theGlobal.asax.cs.
public static void RegisterGlobalFilters(GlobalFiltersCollection filters)
{
    filters.Add(new HandleErrorAttribute());
}
Listing 2. Registering HandleErrorAttribute to GlobalFiltersCollection

What the HandleError filter does?

The HandleError filter handles the exceptions that are raised by the controller actions, filters and views, it returns a custom view named Error which is placed in the Shared folder. The HandleError filter works only if the <customErrors> section is turned on in web.config.
The HandleError filter handle exceptions only if the <customErrors> is turned on in web.config.

Error View

The Error view that is created by default contains the following HTML:
@{
   Layout = null;
}

<!DOCTYPE html>
<html>
<head>
   <meta name="viewport" content="width=device-width" />
   <title>Error</title>
</head>
<body>
   <h2>
       Sorry, an error occurred while processing your request.
   </h2>
</body>
</html>
Listing 3. Error View
It contains nothing other than a simple text message. The Layout property is set to null so that the Error view doesn't inherits the application's style.

Accessing the exception details in Error view

In some cases, we have to access the exception information from the Error view. The HandleError filter not only just returns the Error view but it also creates and passes the HandleErrorInfo model to the view. TheHandleErrorInfo model contains the details about the exception and the names of the controller and action that caused the exception.
Here is the definition of the HandleErrorInfo.
public class HandleErrorInfo
{   
    public HandleErrorInfo(Exception exception, string controllerName, 
        string actionName);

    public string ActionName { get; }

    public string ControllerName { get; }

    public Exception Exception { get; }
}
Listing 4. HandleErrorInfo model
Though it's not necessary, let's strongly type the Error view to the HandleErrorInfo model.
@model System.Web.Mvc.HandleErrorInfo
Listing 5. Strongly typing Error view to HandleErrorInfo model
We can easily show the exception and other information by accessing the model through the Model property.
<h2>Exception details</h2>
<p>
    Controller: @Model.ControllerName <br>
    Action: @Model.ActionName
    Exception: @Model.Exception
</p>
Listing 6. Displaying exception details in Error view

Returning different views for different exceptions

We can return different views from the HandleError filter. For ex. if you want to return one view for database exceptions and another view for application exceptions, you could easily do that by specifying the View andException properties as shown in the below listing.
[HandleError(Exception = typeof(DbException), View = "DatabaseError")]
[HandleError(Exception = typeof(AppException), View = "ApplicationError")]
public class ProductController
{
    
}
Listing 7. Setting View and Exception properties
The HandleErrorAttribute can be applied multiple times over a controller or action or at global level.

Limitations of HandleError

The HandleError filter has some limitations by the way.
  1. Not support to log the exceptions
  2. Doesn't catch HTTP exceptions other than 500
  3. Doesn't catch exceptions that are raised outside controllers
  4. Return error view even for exceptions occurred in AJAX calls
Let us see one by one!

1. Not support to log the exceptions

Logging is very important in error handling and even simple applications get much benefit by logging the errors to a text file or database. The HandleError filter suppresses the exceptions once they are handled and all it does is showing a custom view to the user.

2. Doesn't catch HTTP exceptions other than 500.

The HandleError filter captures only the HTTP exceptions having status code 500 and by-passes the others. Let's assume we have an action that returns all the posts published for a particular category. If the category not exists then we are throwing a 404 HTTP exception.
public ViewResult Posts(string category)
{
    if(_blogRepository.Category(category) == null)
        throw new HttpException(404, "Category not found");

    return _blogRepository.Posts(category);
}
Listing 8. Throwing HttpException from action
If any user passes an invalid category a 404 exception will be thrown from the action and the HandleError don't catch this error. Usually in this case, programmers like to show a custom view with a message "The requested category is not found or invalid". Not only the 404 exceptions, the HandleError doesn't catch any HTTP exception other than 500.
Handling the HTTP exceptions in filters is right or wrong is a thing for debate. In some cases we may have to bypass the HTTP exceptions to the framework to take proper action against it. So handling HTTP exceptions in filters depends upon the application and it's not mandatory!

3. Doesn't catch exceptions that are raised outside controllers

The HandleError is an exception filter and exception filters are called only if any exception happens inside the action or in the action filters that are applied over the action. So if the exception happens some other place the filter will be silent.
For example, say we have set up a route constraint for a specific route as shown in the below listing.
routes.MapRoute(
     "Default", 
     "Strange~Action",
     new { controller = "NonIE", action = "Action"  },
     new { browserConstraint = new UserAgentConstraint("IE") } 
); 
Listing 9. Adding constraints to routes
Route Constraints restricts the set of URLs that a route will match against.
The UserAgentConstraint that we set up in the above route restricts the route to handle requests from only Internet Explorer browsers. In the implementation of UserAgentConstraint I'm purposely throwing an exception.
public class UserAgentConstraint : IRouteConstraint
{
    private readonly string _restrictAgent;

    public UserAgentConstraint(string restrictAgent)
    {
        _restrictAgent = restrictAgent;
    }

    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
            // TODO: return true if the browser is not restricted one.
        throw new NotImplementedException("You forget to implement me");
    }
} 
Listing 9. UserAgentConstraint
This exception is thrown at very early stage and HandleError filter won't be there to catch this. When we access the route where the above constraint is applied we will see the yellow screen instead of the custom error page.

4. Return error view even in exceptions occurred in AJAX calls.

In case of AJAX calls, if some exception happens the HandleError filter returns the custom error view, which is not useful in the client-side. It would be great to return a piece of JSON in AJAX exceptions and for that we have to extend the HandleError filter or have to create a custom exception filter.

HandleError vs. Application_Error

Exception filters are not global error handlers and this is an important reason that forces us to still rely onApplication_Error event. Some programmers don't even use the HandleError filter in their application at all and use only the Application_Error event for doing all the error handling and logging work. The important problem we face in the Application_Error event is, once the program execution reaches this point then we are out of MVC and we can't access the context objects and other useful stuff of the framework.
Another important feature that exception filters brings to us is we can handle the exceptions in different ways at different scopes, this is important in some cases, for ex. when exceptions are raised from one controller we have to return a custom error view and for other controllers we have to return a different error view, this could be easily accomplished through exception filters but not easily through the Application_Error event.
The bottom-line is, we need to use the Application_Error event at most of the cases in applications unless we are using a framework like ELMAH which magically handles all the exceptions. But whether we need to use the HandleError filter or not is totally depend upon the application. When we need a controller or action level exception handling then we can use the HandleError filter along with the Application_Error event, else we can simply ignore the HandleError filter.

Extending HandleError

Most of the cases we have to extend the built-in HandleError filter or have to create a custom exception filter to do some useful job like logging. Here is an example that shows how to extend the built-in filter to log the exceptions using log4net and return a JSON object for AJAX calls.
public class CustomHandleErrorAttribute : HandleErrorAttribute
{
    private readonly ILog _logger;

    public CustomHandleErrorAttribute()
    {
        _logger = LogManager.GetLogger("MyLogger");
    }

    public override void OnException(ExceptionContext filterContext)
    {
        if (filterContext.ExceptionHandled || !filterContext.HttpContext.IsCustomErrorEnabled)
        {
            return;
        }

        if (new HttpException(null, filterContext.Exception).GetHttpCode() != 500)
        {
            return;
        }

        if (!ExceptionType.IsInstanceOfType(filterContext.Exception))
        {
            return;
        }

        // if the request is AJAX return JSON else view.
        if (filterContext.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
        {
            filterContext.Result = new JsonResult 
            { 
                JsonRequestBehavior = JsonRequestBehavior.AllowGet, 
                Data = new 
                { 
                    error = true,
                    message = filterContext.Exception.Message
                } 
            };
        }
        else
        {
            var controllerName = (string)filterContext.RouteData.Values["controller"];
            var actionName = (string)filterContext.RouteData.Values["action"];
            var model = new HandleErrorInfo(filterContext.Exception, controllerName, actionName);
            
            filterContext.Result = new ViewResult
            {
                ViewName = View,
                MasterName = Master,
                ViewData = new ViewDataDictionary<HandleErrorInfo>(model),
                TempData = filterContext.Controller.TempData
            };
        }

        // log the error using log4net.
        _logger.Error(filterContext.Exception.Message, filterContext.Exception);

        filterContext.ExceptionHandled = true;
        filterContext.HttpContext.Response.Clear();
        filterContext.HttpContext.Response.StatusCode = 500;

        filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;      
    }
}
Listing 10. Extending HandleError
Most of the code are same as in the built-in filter. Notice we have ignored the HTTP exceptions so anyway we need to wire-up the Application_Error event to catch and log the missed exceptions. If the request is an AJAX call then we are returning a JSON object that contains a boolean and the exception message else we are returning the error view. We are setting the response status code as 500 and the HandleError filter also does the same, this is important in terms of REST and HTTP standards.

Returning views from Application_Error

In some applications we have to depend upon the Application_Error event for handling all the exceptions or the ones that are missed by the exception filters. Mostly programmers like to return an MVC view instead of a static page. Though we are out of the MVC context still we can return a view using a controller (thanks to StackOverflow).
Let's create an Error controller that return different views for different errors as shown in the below listing.
public class ErrorController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    public ActionResult NotFound()
    {
        return View();
    }
}
Listing 11. Error Controller
We have to invoke this Error controller from the Application_Error to return a view after the exception is logged.
protected void Application_Error(object sender, EventArgs e)
{
    var httpContext = ((MvcApplication)sender).Context;
    
    var currentRouteData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext));    
    var currentController = " ";
    var currentAction = " ";
    
    if(currentRouteData != null)
    {
        if(currentRouteData.Values["controller"] != null && !String.IsNullOrEmpty(currentRouteData.Values["controller"].ToString()))
        {
            currentController = currentRouteData.Values["controller"].ToString();
        }
        
        if(currentRouteData.Values["action"] != null && !String.IsNullOrEmpty(currentRouteData.Values["action"].ToString()))
        {
            currentAction = currentRouteData.Values["action"].ToString();
        }
    }
    
    var ex = Server.GetLastError();

    var controller = new ErrorController();
    var routeData = new RouteData();
    var action = "Index";

    if (ex is HttpException)
    {
        var httpEx = ex as HttpException;

        switch (httpEx.GetHttpCode())
        {
            case 404:
                action = "NotFound";
                break;

            // others if any

            default:
                action = "Index";
                break;
        }
    }

    httpContext.ClearError();
    httpContext.Response.Clear();
    httpContext.Response.StatusCode = ex is HttpException ? ((HttpException)ex).GetHttpCode() : 500;
    httpContext.Response.TrySkipIisCustomErrors = true;
    routeData.Values["controller"] = "Error";
    routeData.Values["action"] = action;

    controller.ViewData.Model = new HandleErrorInfo(ex, currentController, currentAction);    
    ((IController)controller).Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData));
} 
Listing 12. Returning views from Application_Error
We are doing many things in the above code, mainly we are instantiating the Error controller and invoking it by calling the Execute() method passing the HandleErrorInfo model and this is the model used by theHandleError filter as well.
We are using a switch statement just to demonstrate how we can return different error views for different HTTP exceptions. If you want to take care of AJAX calls you have to change the implementation little as we did in the custom HandleError filter but to keep things simple I've ignored that part.

ELMAH

ELMAH (Error Logging Modules and Handlers) is an application-wide error logging facility that is completely pluggable. You can easily configure ELMAH for an ASP.NET MVC application without much code. The other benefits brought up by ELMAH is, it provides a custom page where the admin can view the errors including the original yellow screen, also it provides options to send emails, generate RSS etc.
ELMAH logs only unhandled exceptions so we have to signal ELMAH to log the exceptions that are handled. When we use the HandleError filter and ELMAH in an application we will confused seeing no exceptions are logged by ELMAH, it's because once the exceptions are handled by the HandleError filter it sets theExceptionHandled property of the ExceptionContext object to true and that hides the errors from logged by ELMAH. A better way to overcome this problem is extend the HandleError filter and signal to ELMAH as shown in the below listing.
public class ElmahHandleErrorAttribute : HandleErrorAttribute
{
    public override void OnException(ExceptionContext filterContext)
    {
        base.OnException(filterContext);

        // signal ELMAH to log the exception
        if (filterContext.ExceptionHandled)
            ErrorSignal.FromCurrentContext().Raise(filterContext.Exception);
    }
}
Listing 13. Signaling to Elmah

Summary

In this article we saw about the built-in HandleError filter available with ASP.NET MVC to handle the exceptions. The HandleError filter has some limitations and most importantly it doesn't handle all the exceptions that are raised by the application.
Although HandleError filter bring some benefits in customizing error handling at a controller or action level still we have to rely on the Application_Error event.
ELMAH is an error handling module that is easily pluggable to an ASP.NET application. Unlike HandleError, ELMAH catches all the exceptions raised by the application.

http://www.codeproject.com/Articles/422572/Exception-Handling-in-ASP-NET-MVC


ASP.NET MVC Bundling and minification

Bundling and Minification

By |
Bundling and minification are two techniques you can use in ASP.NET 4.5 to improve request load time.  Bundling and minification improves load time by reducing the number of requests to the server and reducing the size of requested assets (such as CSS and JavaScript.)
Most of the current major browsers limit the number of simultaneous connections per each hostname to six. That means that while six requests are being processed, additional requests for assets on a host will be queued by the browser. In the image below, the IE F12 developer tools network tabs shows the timing for assets required by the About view of a sample application.
B/M
The gray bars show the time the request is queued by the browser waiting on the six connection limit. The yellow bar is the request time to first byte, that is, the time taken to send the request and receive the first response from the server. The blue bars show the time taken to receive the response data from the server. You can double-click on an asset to get detailed timing information. For example, the following image shows the timing details for loading the/Scripts/MyScripts/JavaScript6.js file.
The preceding image shows the Start event, which gives the time the request was queued because of the browser limit the number of simultaneous connections. In this case, the request was queued for 46 milliseconds waiting for another request to complete.

Bundling

Bundling is a new feature in ASP.NET 4.5 that makes it easy to combine or bundle multiple files into a single file. You can create CSS, JavaScript and other bundles. Fewer files means fewer HTTP requests and that can improve first page load  performance.
The following image shows the same timing view of the About view shown previously, but this time with bundling and minification enabled.

Minification

Minification performs a variety of different code optimizations to scripts or css, such as removing unnecessary white space and comments and shortening variable names to one character. Consider the following JavaScript function.
AddAltToImg = function (imageTagAndImageID, imageContext) {
    ///<signature>
    ///<summary> Adds an alt tab to the image
    // </summary>
    //<param name="imgElement" type="String">The image selector.</param>
    //<param name="ContextForImage" type="String">The image context.</param>
    ///</signature>
    var imageElement = $(imageTagAndImageID, imageContext);
    imageElement.attr('alt', imageElement.attr('id').replace(/ID/, ''));
}
After minification,  the function is reduced to the following:
AddAltToImg = function (n, t) { var i = $(n, t); i.attr("alt", i.attr("id").replace(/ID/, "")) }
In addition to removing the comments and unnecessary whitespace, the following parameters and variable names were renamed (shortened) as follows:
OriginalRenamed
imageTagAndImageIDn
imageContextt
imageElementi

Impact of Bundling and Minification

The following table shows several important differences between listing all the assets individually and using bundling and minification (B/M) in the sample program.
 Using B/MWithout B/MChange
File Requests934256%
KB Sent3.2611.92266%
KB Received388.5153036%
Load Time510 MS780 MS53%
The bytes sent had a significant reduction with bundling as browsers are fairly verbose with the HTTP headers they apply on requests. The received bytes reduction is not as large because the largest files (Scripts\jquery-ui-1.8.11.min.js and Scripts\jquery-1.7.1.min.js) are already minified. Note: The timings on the sample program used theFiddler tool to simulate a slow network. (From the Fiddler Rules menu, select Performance then Simulate Modem Speeds.)

Debugging Bundled and Minified JavaScript

It's easy to debug your JavaScript in a development environment (where the compilation Element in theWeb.config file is set to debug="true" ) because the JavaScript files are not bundled or minified. You can also debug a release build where your JavaScript files are bundled and minified. Using the IE F12 developer tools, you debug  a JavaScript function included in a minified bundle using the following approach:
  1. Select the Script tab and then select the Start debugging button.
  2. Select the bundle containing the JavaScript function you want to debug using the assets button.
  3. Format the minified JavaScript by selecting the Configuration button, and then selecting Format JavaScript.
  4. In the Search Script input box, select the name of the function you want to debug. In the following image,AddAltToImg was entered in the Search Script input box.
For more information on debugging with the F12 developer tools, see the MSDN article Using the F12 Developer Tools to Debug JavaScript Errors.

Controlling Bundling and Minification

Bundling and minification is enabled or disabled by setting the value of the debug attribute in the compilation Element  in the Web.config file. In the following XML, debug is set to true so bundling and minification is disabled.
<system.web>
    <compilation debug="true" />
    <!-- Lines removed for clarity. -->
</system.web>
To enable bundling and minification, set the debug value to "false". You can override the Web.config setting with theEnableOptimizations property on the BundleTable class. The following code enables bundling and minification and overrides any setting in the Web.config file.
public static void RegisterBundles(BundleCollection bundles)
{
    bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                 "~/Scripts/jquery-{version}.js"));

    // Code removed for clarity.
    BundleTable.EnableOptimizations = true;
}
Note: Unless EnableOptimizations is true or the debug attribute in the compilation Element  in the Web.config file is set to false, files will not be bundled or minified. Additionally, the .min version of files will not be used,  the full debug versions will be selected.EnableOptimizations  overrides the debug attribute in the compilation Element  in theWeb.config file

Using Bundling and Minification  with ASP.NET Web Forms and Web Pages

Using Bundling and Minification  with ASP.NET MVC

In this section we will create an ASP.NET MVC project to examine bundling and minification. First, create a new ASP.NET MVC internet project named MvcBM without changing any of the defaults.
Open the App_Start\BundleConfig.cs file and examine the RegisterBundles method which is used to create, register and configure bundles. The following code shows a portion of  the RegisterBundles method.
 public static void RegisterBundles(BundleCollection bundles)
{
     bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                 "~/Scripts/jquery-{version}.js"));
         // Code removed for clarity.
}
The preceding code creates a new JavaScript bundle named ~/bundles/jquery that includes all the appropriate (that is debug or minified but not .vsdoc) files in the Scripts folder that match the wild card string "~/Scripts/jquery-{version}.js". For ASP.NET MVC 4, this means with a  debug configuration, the file jquery-1.7.1.js will be added to the bundle. In a release configuration,  jquery-1.7.1.min.js will be added. The bundling framework follows several common conventions such as:
  • Selecting “.min” file for release when “FileX.min.js” and “FileX.js” exist.
  • Selecting the non “.min” version for debug.
  • Ignoring “-vsdoc” files (such as jquery-1.7.1-vsdoc.js), which are used only by IntelliSense.
The {version} wild card matching shown above is used to automatically create a jQuery bundle with the appropriate  version of jQuery in your Scripts folder.  In this example, using a wild card provides the following benefits:
  • Allows you to use NuGet to update to a newer jQuery version without changing the preceding bundling code or jQuery references in your view pages.
  • Automatically selects the full version for debug configurations and the ".min" version for release builds.

Using a CDN

The follow code replaces the local jQuery bundle with a CDN jQuery bundle.
public static void RegisterBundles(BundleCollection bundles)
{
    //bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
    //            "~/Scripts/jquery-{version}.js"));

    bundles.UseCdn = true;   //enable CDN support

    //add link to jquery on the CDN
    var jqueryCdnPath = "http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.1.min.js";

    bundles.Add(new ScriptBundle("~/bundles/jquery",
                jqueryCdnPath).Include(
                "~/Scripts/jquery-{version}.js"));

    // Code removed for clarity.
}
In the code above, jQuery will be requested from the CDN while in release mode  and  the debug version of jQuery will be fetched locally in debug mode. When using a CDN, you should have a fallback mechanism in case the CDN request fails. The following markup fragment from the end of the layout file shows script added to request jQuery should the CDN fail.
        </footer>

        @Scripts.Render("~/bundles/jquery")

        <script type="text/javascript">
            if (typeof jQuery == 'undefined') {
                var e = document.createElement('script');
                e.src = '@Url.Content("~/Scripts/jquery-1.7.1.js")';
                e.type = 'text/javascript';
                document.getElementsByTagName("head")[0].appendChild(e);

            }
        </script> 

        @RenderSection("scripts", required: false)
    </body>
</html>

Creating a Bundle

The Bundle class Include method takes an array of strings, where each string is a virtual path to resource. The following code from the RegisterBundles method in the App_Start\BundleConfig.cs file shows how multiple files are added to a bundle:
bundles.Add(new StyleBundle("~/Content/themes/base/css").Include(
          "~/Content/themes/base/jquery.ui.core.css",
          "~/Content/themes/base/jquery.ui.resizable.css",
          "~/Content/themes/base/jquery.ui.selectable.css",
          "~/Content/themes/base/jquery.ui.accordion.css",
          "~/Content/themes/base/jquery.ui.autocomplete.css",
          "~/Content/themes/base/jquery.ui.button.css",
          "~/Content/themes/base/jquery.ui.dialog.css",
          "~/Content/themes/base/jquery.ui.slider.css",
          "~/Content/themes/base/jquery.ui.tabs.css",
          "~/Content/themes/base/jquery.ui.datepicker.css",
          "~/Content/themes/base/jquery.ui.progressbar.css",
          "~/Content/themes/base/jquery.ui.theme.css"));
The Bundle class IncludeDirectory method is provided to add all the files in a directory (and optionally all subdirectories) which match a search pattern.  The Bundle class IncludeDirectory API is shown below:
 public Bundle IncludeDirectory(
     string directoryVirtualPath,  // The Virtual Path for the directory.
     string searchPattern)         // The search pattern.
 
 public Bundle IncludeDirectory(
     string directoryVirtualPath,  // The Virtual Path for the directory.
     string searchPattern,         // The search pattern.
     bool searchSubdirectories)    // true to search subdirectories.
Bundles are referenced in views using the Render method ,  ( Styles.Render for CSS and Scripts.Render for JavaScript). The following markup from the Views\Shared\_Layout.cshtml file shows how the default ASP.NET internet project views reference CSS and JavaScript bundles.
<!DOCTYPE html>
<html lang="en">
<head>
    @* Markup removed for clarity.*@    
    @Styles.Render("~/Content/themes/base/css", "~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
</head>
<body>
    @* Markup removed for clarity.*@
   
   @Scripts.Render("~/bundles/jquery")
   @RenderSection("scripts", required: false)
</body>
</html>
Notice the Render methods takes an array of strings, so you can add multiple bundles in one line of code. You will generally want to use the Render methods which create the necessary HTML to reference the asset. You can use theUrl method to generate the URL to the asset without the markup needed to reference the asset. Suppose you wanted to use the new HTML5 async attribute. The following code shows how to reference modernizr using the Urlmethod.
<head>
    @*Markup removed for clarity*@
    <meta charset="utf-8" />
    <title>@ViewBag.Title - MVC 4 B/M</title>
    <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
    <meta name="viewport" content="width=device-width" />
    @Styles.Render("~/Content/css")

   @* @Scripts.Render("~/bundles/modernizr")*@

    <script src='@Scripts.Url("~/bundles/modernizr")' async> </script>
</head>

Using the "*" Wildcard Character to Select Files

The virtual path specified in the Include method  and  the search pattern in the IncludeDirectory method can accept one "*" wildcard character as a  prefix or suffix to in the last path segment. The search string is case insensitive. The IncludeDirectory method has the option of searching subdirectories.
 Consider a project with the following JavaScript files:
  • Scripts\Common\AddAltToImg.js
  • Scripts\Common\ToggleDiv.js
  • Scripts\Common\ToggleImg.js
  • Scripts\Common\Sub1\ToggleLinks.js
dir imag
The following table shows the files added to a bundle using the wildcard as shown:
CallFiles Added or Exception Raised
Include("~/Scripts/Common/*.js")AddAltToImg.js, ToggleDiv.js, ToggleImg.js
Include("~/Scripts/Common/T*.js")Invalid pattern exception. The wildcard character is only allowed on the prefix or suffix.
Include("~/Scripts/Common/*og.*")Invalid pattern exception. Only one wildcard character is allowed.
"Include("~/Scripts/Common/T*")ToggleDiv.js, ToggleImg.js
"Include("~/Scripts/Common/*")Invalid pattern exception. A pure wildcard segment is not valid.
IncludeDirectory("~/Scripts/Common", "T*")ToggleDiv.js, ToggleImg.js
IncludeDirectory("~/Scripts/Common", "T*",true)ToggleDiv.js, ToggleImg.js, ToggleLinks.js
Explicitly adding each file to a bundle  is generally the preferred over wildcard loading of files for the following reasons:
  • Adding scripts by wildcard defaults to loading them in alphabetical order, which is typically not what you want. CSS and JavaScript files frequently need to be added in a specific (non-alphabetic) order. You can mitigate this risk by adding a custom IBundleOrderer implementation, but explicitly adding each file is less error prone. For example, you might add new assets to a folder in the future which might require you to modify yourIBundleOrderer implementation.
  • View specific files added to a directory using wild card loading can be included in all views referencing that bundle. If the view specific script is added to a bundle, you may get a JavaScript error on other views that reference the bundle.
  • CSS files that import other files result in the imported files loaded twice. For example, the following code creates a bundle with most of the jQuery UI theme CSS files loaded twice.
    bundles.Add(new StyleBundle("~/jQueryUI/themes/baseAll")
        .IncludeDirectory("~/Content/themes/base", "*.css"));
    The wild card selector "*.css" brings in each CSS file in the folder, including  theContent\themes\base\jquery.ui.all.css file.  The jquery.ui.all.css file imports other CSS files.

Bundle Caching

Bundles set the HTTP Expires Header one year from when the bundle is  created. If you navigate to a previously viewed page, Fiddler shows IE does not make a conditional request for the bundle, that is, there are no HTTP GET requests from IE for the bundles and no HTTP 304 responses from the server. You can force IE to make a conditional request for each bundle with the F5 key (resulting in a HTTP 304 response for each bundle). You can  force a full refresh by using ^F5 (resulting in a HTTP 200 response for each bundle.)
The following image shows the Caching tab of the Fiddler response pane:
fiddler caching image
The request
http://localhost/MvcBM_time/bundles/AllMyScripts?v=r0sLDicvP58AIXN_mc3QdyVvVj5euZNzdsa2N1PKvb81
is for the bundle AllMyScripts and contains a query string pairv=r0sLDicvP58AIXN_mc3QdyVvVj5euZNzdsa2N1PKvb81. The query string v has a value token that is a unique identifier used for caching. As long as the bundle doesn't change, the ASP.NET application will request theAllMyScripts  bundle using this token. If any file in the bundle changes, the ASP.NET optimization framework will generate a new token, guaranteeing that browser requests for the bundle will get the latest bundle.
If you run the IE9 F12 developer tools and navigate to a previously loaded page, IE incorrectly shows conditional GET requests made to each bundle and the server returning HTTP 304. You can read why IE9 has problems determining if a conditional request was made  in the blog entry Using CDNs and Expires to Improve Web Site Performance.

LESS, CoffeeScript, SCSS, Sass Bundling.

The bundling and minification framework provides a mechanism to process intermediate languages such as SCSS,SassLESS or Coffeescript, and apply transforms such as minification to the resulting bundle. For example, to add.less files to your MVC 4 project:
  1. Create a folder for your LESS content. The following example uses the Content\MyLess folder.
  2. Add the .less NuGet package dotless to your project.
    NuGet dotless install
  3. Add a class that implements the IBundleTransform interface. For the .less transform, add the following code to your project.
    using System.Web.Optimization;
    
    public class LessTransform : IBundleTransform
    {
        public void Process(BundleContext context, BundleResponse response)
        {
            response.Content = dotless.Core.Less.Parse(response.Content);
            response.ContentType = "text/css";
        }
    }
  4. Create a bundle of LESS files with the LessTransform and the CssMinify transform. Add the following code to the RegisterBundles method in the App_Start\BundleConfig.cs file.
    var lessBundle = new Bundle("~/My/Less").IncludeDirectory("~/My", "*.less");
    lessBundle.Transforms.Add(new LessTransform());
    lessBundle.Transforms.Add(new CssMinify());
    bundles.Add(lessBundle);
  5. Add the following code to any views which references the LESS bundle.
     @Styles.Render("~/My/Less");

Bundle Considerations

A good convention to follow when creating bundles is to include "bundle" as a prefix in the bundle name. This will prevent a possible routing conflict.
Once you update one file in a bundle, a new token is generated for the bundle query string parameter and the full bundle must be downloaded the next time a client requests a page containing the bundle. In traditional markup where each asset is listed individually, only the changed file would be downloaded. Assets that change frequently may not be good candidates for bundling.
Bundling and minification primarily improve the first page request load time. Once a webpage has been requested, the browser caches the assets (JavaScript, CSS and images) so bundling and minification won’t provide any performance boost when requesting the same page, or pages on the same site requesting the same assets. If you don’t set the expires header correctly on your assets, and you don’t use bundling and minification, the browsers freshness heuristics will mark the assets stale after a few days and the browser will require a validation request for each asset. In this case, bundling and minification provide a performance increase after the first page request. For details, see the blog Using CDNs and Expires to Improve Web Site Performance.
The browser limitation of six simultaneous connections per each hostname can be mitigated by using a CDN. Because the CDN will have a different hostname than your hosting site, asset requests from the CDN will not count against the six simultaneous connections limit to your hosting environment. A CDN can also provide common package caching and edge caching advantages.
Bundles should be partitioned by pages that need them. For example, the default ASP.NET MVC template for an internet application creates a jQuery Validation bundle separate from jQuery. Because the default views created have no input and do not post values, they don't include the validation bundle.
The System.Web.Optimization namespace is implemented in System.Web.Optimization.DLL. It leverages the WebGrease library (WebGrease.dll) for minification capabilities, which in turn uses Antlr3.Runtime.dll.
I use Twitter to make quick posts and share links. My Twitter handle is@RickAndMSFT

Additional Resources

Contributors

This article was originally created on August 23, 2012

http://www.asp.net/mvc/overview/performance/bundling-and-minification