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,
01 |
public
static
class
HtmlHelpers
|
02 |
{
|
03 |
public
static
MvcHtmlString NewTextBox(this
HtmlHelper html, string
name)
|
04 |
{
|
05 |
var js = Scripts.Render("~/ImranB/Embedded/Js").ToString();
|
06 |
var css = Styles.Render("~/ImranB/Embedded/Css").ToString();
|
07 |
var textbox =
html.TextBox(name).ToString();
|
08 |
return
MvcHtmlString.Create(textbox + js +
css);
|
09 |
}
|
10 |
}
|
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,
01 |
[assembly:
WebActivator.PostApplicationStartMethod(typeof(AppStart), "Start")]
|
02 |
namespace
ImranB
|
03 |
{
|
04 |
public
static
class
AppStart
|
05 |
{
|
06 |
public
static
void
Start()
|
07 |
{
|
08 |
ConfigureRoutes();
|
09 |
ConfigureBundles();
|
10 |
}
|
11 |
private
static
void
ConfigureBundles()
|
12 |
{
|
13 |
BundleTable.VirtualPathProvider = new
EmbeddedVirtualPathProvider(HostingEnvironment.VirtualPathProvider);
|
14 |
BundleTable.Bundles.Add(new
ScriptBundle("~/ImranB/Embedded/Js")
|
15 |
.Include("~/ImranB/Embedded/NewTextBox1.js")
|
16 |
.Include("~/ImranB/Embedded/NewTextBox2.js")
|
17 |
);
|
18 |
19 |
BundleTable.Bundles.Add(new
StyleBundle("~/ImranB/Embedded/Css")
|
20 |
.Include("~/ImranB/Embedded/NewTextBox1.css")
|
21 |
.Include("~/ImranB/Embedded/NewTextBox2.css")
|
22 |
);
|
23 |
}
|
24 |
private
static
void
ConfigureRoutes()
|
25 |
{
|
26 |
RouteTable.Routes.Insert(0,
|
27 |
new
Route("ImranB/Embedded/{file}.{extension}",
|
28 |
new
RouteValueDictionary(new
{ }),
|
29 |
new
RouteValueDictionary(new
{ extension = "css|js"
}),
|
30 |
new
EmbeddedResourceRouteHandler()
|
31 |
));
|
32 |
}
|
33 |
}
|
34 |
}
|
.
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,
01 |
public
class
EmbeddedResourceRouteHandler :
IRouteHandler
|
02 |
{
|
03 |
IHttpHandler
IRouteHandler.GetHttpHandler(RequestContext
requestContext)
|
04 |
{
|
05 |
return
new
EmbeddedResourceHttpHandler(requestContext.RouteData);
|
06 |
}
|
07 |
}
|
08 |
public
class
EmbeddedResourceHttpHandler :
IHttpHandler
|
09 |
{
|
10 |
private
RouteData _routeData;
|
11 |
public
EmbeddedResourceHttpHandler(RouteData
routeData)
|
12 |
{
|
13 |
_routeData = routeData;
|
14 |
}
|
15 |
public
bool
IsReusable
|
16 |
{
|
17 |
get
{ return
false; }
|
18 |
}
|
19 |
public
void
ProcessRequest(HttpContext context)
|
20 |
{
|
21 |
var routeDataValues =
_routeData.Values;
|
22 |
var fileName = routeDataValues["file"].ToString();
|
23 |
var fileExtension = routeDataValues["extension"].ToString();
|
24 |
string
nameSpace = typeof(EmbeddedResourceHttpHandler)
|
25 |
.Assembly
|
26 |
.GetName()
|
27 |
.Name;// Mostly the default namespace and assembly
name are same
|
28 |
string
manifestResourceName = string.Format("{0}.{1}.{2}", nameSpace, fileName, fileExtension);
|
29 |
var stream = typeof(EmbeddedResourceHttpHandler).Assembly.GetManifestResourceStream(manifestResourceName);
|
30 |
context.Response.Clear();
|
31 |
context.Response.ContentType = "text/css";// default
|
32 |
if
(fileExtension == "js")
|
33 |
context.Response.ContentType = "text/javascript";
|
34 |
stream.CopyTo(context.Response.OutputStream);
|
35 |
}
|
36 |
}
|
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,
01 |
public
class
EmbeddedVirtualPathProvider :
VirtualPathProvider
|
02 |
{
|
03 |
private
VirtualPathProvider _previous;
|
04 |
public
EmbeddedVirtualPathProvider(VirtualPathProvider
previous)
|
05 |
{
|
06 |
_previous = previous;
|
07 |
}
|
08 |
public
override
bool
FileExists(string
virtualPath)
|
09 |
{
|
10 |
if
(IsEmbeddedPath(virtualPath))
|
11 |
return
true;
|
12 |
else
|
13 |
return
_previous.FileExists(virtualPath);
|
14 |
}
|
15 |
public
override
CacheDependency GetCacheDependency(string
virtualPath, IEnumerable
virtualPathDependencies, DateTime
utcStart)
|
16 |
{
|
17 |
if
(IsEmbeddedPath(virtualPath))
|
18 |
{
|
19 |
return
null;
|
20 |
}
|
21 |
else
|
22 |
{
|
23 |
return
_previous.GetCacheDependency(virtualPath,
virtualPathDependencies, utcStart);
|
24 |
}
|
25 |
}
|
26 |
public
override
VirtualDirectory GetDirectory(string
virtualDir)
|
27 |
{
|
28 |
return
_previous.GetDirectory(virtualDir);
|
29 |
}
|
30 |
public
override
bool
DirectoryExists(string
virtualDir)
|
31 |
{
|
32 |
return
_previous.DirectoryExists(virtualDir);
|
33 |
}
|
34 |
public
override
VirtualFile GetFile(string
virtualPath)
|
35 |
{
|
36 |
if
(IsEmbeddedPath(virtualPath))
|
37 |
{
|
38 |
string
fileNameWithExtension =
virtualPath.Substring(virtualPath.LastIndexOf("/") + 1);
|
39 |
string
nameSpace = typeof(EmbeddedResourceHttpHandler)
|
40 |
.Assembly
|
41 |
.GetName()
|
42 |
.Name;// Mostly the default namespace and assembly
name are same
|
43 |
string
manifestResourceName = string.Format("{0}.{1}", nameSpace, fileNameWithExtension);
|
44 |
var stream = typeof(EmbeddedVirtualPathProvider).Assembly.GetManifestResourceStream(manifestResourceName);
|
45 |
return
new
EmbeddedVirtualFile(virtualPath,
stream);
|
46 |
}
|
47 |
else
|
48 |
return
_previous.GetFile(virtualPath);
|
49 |
}
|
50 |
private
bool
IsEmbeddedPath(string
path)
|
51 |
{
|
52 |
return
path.Contains("~/ImranB/Embedded");
|
53 |
}
|
54 |
}
|
55 |
public
class
EmbeddedVirtualFile : VirtualFile
|
56 |
{
|
57 |
private
Stream _stream;
|
58 |
public
EmbeddedVirtualFile(string
virtualPath, Stream stream)
|
59 |
: base(virtualPath)
|
60 |
{
|
61 |
_stream = stream;
|
62 |
}
|
63 |
public
override
Stream Open()
|
64 |
{
|
65 |
return
_stream;
|
66 |
}
|
67 |
}
|
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,
1 |
@using
ImranB.Helpers
|
2 |
3 |
@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.