Embedded Resources with MonoRail Applications
November 12, 2007 – 11:36
I have a number of JavaScript files that I like to share amongst my web projects. Rather than keeping a separate copy of these script files in each application, I prefer to embed them in to an assembly that the various web projects can reference. But how do you reference these JavaScript files in your HTML?
Using ASP.NET WebForms this is easy – you simply add a [assembly: WebResource] attribute to your AssemblyInfo.cs file and then you can call Page.ClientScript.GetWebResourceUrl to generate a URL to an HTTP Handler that will extract the resource from the assembly and output its contents (see this article on 4guys for more information)
I wanted to do a similar thing using MonoRail, so I wrote a Controller and a Helper that perform a similar task.
The first thing I do is create a helper that will look up the names of all the embedded resources in the assembly. To do this, I use a static constructor to ensure that this only happens the first time that the class is used.
public class ScriptHelper : AbstractHelper
{
internal const string ROOT_NAMESPACE = "Cristal.Common.Resources";
private static readonly List<string> _resources = new List<string>();
private static readonly string _version;
static ScriptHelper()
{
_version = Assembly.GetExecutingAssembly().GetName().Version.ToString().Replace(".", "");
foreach (string resource in typeof(ScriptHelper).Assembly.GetManifestResourceNames())
{
_resources.Add(resource);
}
}
}
The static constructor finds all of the assembly-level resources and adds their names to a cache. I also look up the version of the currently executing assembly – more on this later. The constant ROOT_NAMESPACE variable simply stores the namespace where the embedded resources are stored.
Finally, I have a public method called “Install” that can be called from my Views to reference one of these embedded scripts:
public string Install(string name)
{
NameValueCollection querystring = new NameValueCollection(2);
querystring.Add("n", name); //name of script
querystring.Add("v", _version); //version
string url = Controller.UrlBuilder.BuildUrl(Controller.Context.UrlInfo,
string.Empty,
"Resources",
"Scripts",
querystring);
//cut off the final ampersand which monorail irritatingly appends.
url = url.Substring(0, url.Length - 1);
url = HttpUtility.HtmlEncode(url);
return string.Format("<script type=\"text/javascript\" src=\"{0}\"></script>", url);
}
This method generates a URL to a pretend controller with the name of the script (and the assembly version) as querystring parameters. I can then declare this helper on my controller, and reference it in the view:
Controller:
[Helper(typeof(ScriptHelper), "Script")]
public class MyController : Controller
{
public void Index() {}
}
And the view:
${Script.Install("SomeScriptName")}
Note that the name of the script is not the full web resource name (eg Cristal.Common.Resources.SomeScriptName.js)
In the HTML, this will generate some thing like:
<script type="text/javascript" src="/Resources/Scripts.rails?n=SomeScriptName&v=1000"></script>
The “n” querystring parameter is the name of the script and the “v” is the assembly version.
Of course, this won’t actually work at the moment as there isn’t a Resources controller to handle the request. So let’s create one:
public class ResourcesController : Controller
{
//n stands for "name", v stands for "version"
[Cache(HttpCacheability.Public, Duration = 86400, VaryByParams = "v,n")]
public void Scripts()
{
if (Request.QueryString["n"] == null)
throw new Exception("Invalid resource.");
Response.ContentType = "text/javascript";
RenderText(ScriptHelper.GetResource(HttpUtility.HtmlEncode(Request.QueryString["n"])));
}
}
The “Scripts” action is very simple – it ensures that a script name is present in the query string, then passes it to the GetResource method on our scripthelper (below). Also notice the [Cache] attribute at the top of the action – once the script has been loaded from the assembly, the response is cached. However, to ensure that each script is cached separately, we specify the “n” query string parameter (the script name) in the VaryByParams option.
“v” is also in VaryByParams so that I can force the cache to expire by incrementing the version number.
Here’s the GetResource method on ScriptHelper:
internal static string GetResource(string name)
{
name = BuildResourceName(name);
string toReturn = string.Empty;
if (name != null)
{
Assembly asm = typeof(ScriptHelper).Assembly;
using (StreamReader reader = new StreamReader(asm.GetManifestResourceStream(name)))
{
toReturn = reader.ReadToEnd();
}
}
return toReturn;
}
private static string BuildResourceName(string name)
{
name = string.Format("{0}.{1}.js", ROOT_NAMESPACE, name);
bool hasResource = false;
foreach (string resource in _resources)
{
if (name.Equals(resource, StringComparison.InvariantCultureIgnoreCase))
{
hasResource = true;
name = resource;
break;
}
}
if (!hasResource)
return null;
return name;
}
Firstly, BuildResourceName is called to convert the script name (eg “SomeScriptName”) in to the fully qualified Web-resource name (eg “Cristal.Common.Resources.SomeScriptName.js”). Then, we use a streamreader to extract the contents of the JavaScript file from the embedded resource, convert it to a string and return it to the controller.
I’ve implemented this in the next version of CristalWeb (version 7).