MVC Portable Areas – Static Files as Embedded Resources
This is the third post in a series related to build and deployment considerations as I've been exploring MVC Portable Areas:
In the last post, I walked through a convention for managing static files. In this post I'll discuss another approach to manage static files (e.g., images, css, js, etc.). With this approach, you also compile the static files as embedded resources into the assembly similar to the *.aspx pages. Once again, you can set this to happen automatically by simply modifying your *.csproj file to include the desired extensions so you don't have to remember every time you add a file:
<Target Name="BeforeBuild"> <ItemGroup> <EmbeddedResource Include="***.aspx;***.ascx;***.gif;***.css;***.js" /> </ItemGroup> </Target>
We now need a reliable way to serve up these static files that are embedded in the assembly. There are a couple of ways to do this but one way is to simply create a Resource controller whose job is dedicated to doing this.
public class ResourceController : Controller { public ActionResult Index(string resourceName) { var contentType = GetContentType(resourceName); var resourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); return this.File(resourceStream, contentType); } private static string GetContentType(string resourceName) { var extention = resourceName.Substring(resourceName.LastIndexOf('.')).ToLower(); switch (extention) { case ".gif": return "image/gif"; case ".js": return "text/javascript"; case ".css": return "text/css"; default: return "text/html"; } } }
In order to use this controller, we need to make sure we've registered the route in our portable area registration (shown in lines 5-6):
public class WidgetAreaRegistration : PortableAreaRegistration { public override void RegisterArea(System.Web.Mvc.AreaRegistrationContext context, IApplicationBus bus) { context.MapRoute("ResourceRoute", "widget1/resource/{resourceName}", new { controller = "Resource", action = "Index" }); context.MapRoute("Widget1", "widget1/{controller}/{action}", new { controller = "Home", action = "Index" }); RegisterTheViewsInTheEmbeddedViewEngine(GetType()); } public override string AreaName { get { return "Widget1"; } } }
In my previous post, we relied on a custom Url helper method to find the actual physical path to the static file like this:
<img src="<%: Url.AreaContent("/images/arrow.gif") %>" /> Hello World!
However, since we are now embedding the files inside the assembly, we no longer have to worry about the physical path. We can change this line of code to this:
<img src="<%: Url.Resource("Widget1.images.arrow.gif") %>" /> Hello World!
Note that I had to fully quality the resource name (with namespace and physical location) since that is how .NET assemblies store embedded resources. I also created my own Url helper method called Resource which looks like this:
public static string Resource(this UrlHelper urlHelper, string resourceName) { var areaName = (string)urlHelper.RequestContext.RouteData.DataTokens["area"]; return urlHelper.Action("Index", "Resource", new { resourceName = resourceName, area = areaName }); }
This method gives us the convenience of not having to know how to construct the URL – but just allowing us to refer to the resource name. The resulting html for the image tag is:
<img src="/widget1/resource/Widget1.images.arrow.gif" />
so we can always request any image from the browser directly. This is almost analogous to the WebResource.axd file but for MVC. What is interesting though is that we can encapsulate each one of these so that each area can have it's own set of resources and they are easily distinguished because the area name is the first segment of the route. This makes me wonder if something like this ResourceController should be baked into portable areas itself.
I'm definitely interested in anyone has any opinions on it or have taken alternative approaches.