ASP.NET Bundling/Minification and Embedded Resources
Introduction:
If you want to share your application resources(like css, javascript, images, etc) between different projects then embedded resource is a great choice. Embedded resource is also good for component/control writers because it allows component/control writers to distribute all the application resources with just a single assembly. A lot of vendors are already using this approach. It will great for component/control writers if they can leverage the ASP.NET bundling and minification for improving the performance. So, in this article I will show you how to write a very basic component(helper) which will use the ASP.NET bundling and minification feature on embedded javascript/css files.
Description:
First of all create a new Class Library project and install the Microsoft.AspNet.Mvc, WebActivator and Microsoft ASP.NET Web Optimization Framework 1.1.0-alpha1(make sure to include the -Pre parameter in Package Manager Console) nuget packages. Next, add a reference of System.Web assembly. Then, create your control/component/helper. For demonstration purpose, I will use this sample helper,
public static class HtmlHelpers { public static MvcHtmlString NewTextBox(this HtmlHelper html, string name) { var js = Scripts.Render("~/ImranB/Embedded/Js").ToString(); var css = Styles.Render("~/ImranB/Embedded/Css").ToString(); var textbox = html.TextBox(name).ToString(); return MvcHtmlString.Create(textbox + js + css); } }
In this helper, I am just using a textbox with a style and script bundle. Style bundle include 2 css files and script bundle include 2 js files. So, just create 2 css files(NewTextBox1.css and NewTextBox2.css) and 2 javascript files(NewTextBox1.js and NewTextBox2.js) and then mark these files as embedded resource. Next, add a AppStart.cs file and add the following lines inside this file,
[assembly: WebActivator.PostApplicationStartMethod(typeof(AppStart), "Start")] namespace ImranB { public static class AppStart { public static void Start() { ConfigureRoutes(); ConfigureBundles(); } private static void ConfigureBundles() { BundleTable.VirtualPathProvider = new EmbeddedVirtualPathProvider(HostingEnvironment.VirtualPathProvider); BundleTable.Bundles.Add(new ScriptBundle("~/ImranB/Embedded/Js") .Include("~/ImranB/Embedded/NewTextBox1.js") .Include("~/ImranB/Embedded/NewTextBox2.js") ); BundleTable.Bundles.Add(new StyleBundle("~/ImranB/Embedded/Css") .Include("~/ImranB/Embedded/NewTextBox1.css") .Include("~/ImranB/Embedded/NewTextBox2.css") ); } private static void ConfigureRoutes() { RouteTable.Routes.Insert(0, new Route("ImranB/Embedded/{file}.{extension}", new RouteValueDictionary(new { }), new RouteValueDictionary(new { extension = "css|js" }), new EmbeddedResourceRouteHandler() )); } } }
.
The above class using WebActivator's PostApplicationStartMethod, which allows your assembly to run some code after the Application_Start method of global.asax. The Start method simply register a custom virtual path provider and two bundles which are used in our NewText helper class. But ASP.NET optimization framework will only emit a bundle url when debug="false" or when BundleTable.EnableOptimizations = true. Therefore, we also need to handle the normal javascript and css requests. For this case, the above method has also register a specific route for handling embedded resource requests using EmbeddedResourceRouteHandler route handler. Here is the definition of this handler,
public class EmbeddedResourceRouteHandler : IRouteHandler { IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) { return new EmbeddedResourceHttpHandler(requestContext.RouteData); } } public class EmbeddedResourceHttpHandler : IHttpHandler { private RouteData _routeData; public EmbeddedResourceHttpHandler(RouteData routeData) { _routeData = routeData; } public bool IsReusable { get { return false; } } public void ProcessRequest(HttpContext context) { var routeDataValues = _routeData.Values; var fileName = routeDataValues["file"].ToString(); var fileExtension = routeDataValues["extension"].ToString(); string nameSpace = typeof(EmbeddedResourceHttpHandler) .Assembly .GetName() .Name;// Mostly the default namespace and assembly name are same string manifestResourceName = string.Format("{0}.{1}.{2}", nameSpace, fileName, fileExtension); var stream = typeof(EmbeddedResourceHttpHandler).Assembly.GetManifestResourceStream(manifestResourceName); context.Response.Clear(); context.Response.ContentType = "text/css";// default if (fileExtension == "js") context.Response.ContentType = "text/javascript"; stream.CopyTo(context.Response.OutputStream); } }
EmbeddedResourceRouteHandler returns EmbeddedResourceHttpHandler http handler which will be used to extract embedded resource file from the assembly and then write the file to the response body. Now, the only missing thing is EmbeddedVirtualPathProvider,
public class EmbeddedVirtualPathProvider : VirtualPathProvider { private VirtualPathProvider _previous; public EmbeddedVirtualPathProvider(VirtualPathProvider previous) { _previous = previous; } public override bool FileExists(string virtualPath) { if (IsEmbeddedPath(virtualPath)) return true; else return _previous.FileExists(virtualPath); } public override CacheDependency GetCacheDependency(string virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart) { if (IsEmbeddedPath(virtualPath)) { return null; } else { return _previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart); } } public override VirtualDirectory GetDirectory(string virtualDir) { return _previous.GetDirectory(virtualDir); } public override bool DirectoryExists(string virtualDir) { return _previous.DirectoryExists(virtualDir); } public override VirtualFile GetFile(string virtualPath) { if (IsEmbeddedPath(virtualPath)) { string fileNameWithExtension = virtualPath.Substring(virtualPath.LastIndexOf("/") + 1); string nameSpace = typeof(EmbeddedResourceHttpHandler) .Assembly .GetName() .Name;// Mostly the default namespace and assembly name are same string manifestResourceName = string.Format("{0}.{1}", nameSpace, fileNameWithExtension); var stream = typeof(EmbeddedVirtualPathProvider).Assembly.GetManifestResourceStream(manifestResourceName); return new EmbeddedVirtualFile(virtualPath, stream); } else return _previous.GetFile(virtualPath); } private bool IsEmbeddedPath(string path) { return path.Contains("~/ImranB/Embedded"); } } public class EmbeddedVirtualFile : VirtualFile { private Stream _stream; public EmbeddedVirtualFile(string virtualPath, Stream stream) : base(virtualPath) { _stream = stream; } public override Stream Open() { return _stream; } }
EmbeddedVirtualPathProvider class is self explanatory. It simply maps a bundling url(used above) and return the embedded request as stream. Note, this class will be invoked by ASP.NET optimization framework during bundling and minifying process. Now, just build your component/control/helper assembly. Then, create a sample ASP.NET(MVC) application and then use this component/control/helper in your page. For example, like,
@using ImranB.Helpers @Html.NewTextBox("New")
Summary:
In this article, I showed you how to create a component/control/helper that leverage the ASP.NET Optimization framework. A sample application is available at github for download. Hopefully you will enjoy my this article too.