Files
Please download the example solution for this post from the following url.
http://lab.andrewrea.co.uk/ResourcesExampleMvc.rar
Summary
Ok, I have been writing this one as I went, whilst thinking and deving so my opinion on this has changed from when I started writing this to what I ended up with and I have to say I am quite chuffed with the outcome as it yields some other possibilities which I want to now blog about, but to cut a long story/blog post short I have made a Http Handler which accepts some parameters so it can locate a resource file, enumerate through its properties and output them as JavaScript variables to the Response Output Stream. This allows me to expose resources the application is using to client script so I am not duplicating any of them and making maintenance and additions much easier.
I have also wrapped in a bit of token based security, although I do kind of ensure that what is attempted to be enumerated is a resource file by passing the type into a ResourceManager I have added the token based security regardless. Another good option would be to encrypt the url like the common .axd resource handler does, but either way I have used a token based approach using HMAC and SHA1.
Example usage:
<%= Html.ClientResourceLink<ResourcesExampleMvc.Core.Resources.Global>("jsloc.axd","fr") %>
Example output:
<script type="text/javascript" src="http://weblogs.asp.net/jsloc.axd?typeName=ResourcesExampleMvc.Core.Resources.Web.Controllers.Home.Home&culture=fr&token=2E935FA9EF23865A9A57B437EC9A7CCE62A7712B×tamp=1260744495"></script>
The handler at work:
var cache_date = "13 December 2009 23:57";
var GlobalString1 = "French Global String 1";
Finally I have made a HtmlHelper which will generate the relevant link / links I need. So the blog that I started writing, dev’d and changed my mind…
here goes…
I have been looking at different examples on the web relating to client side localisation. The two main points I see are:
- Add secondary resources inside JavaScript files (This leads to duplication of resources)
- Make a call to the Resource Manager inside delimiters inside the mark-up. (This is fine as long as you are not using separate JavaScript Files)
I might be wrong, but from what I can see on the Resource Manager, there is only a set way of getting access to a resource, and that is by its name, so if I want to get several related items, I need to know the keys of each. As I am writing this I am now starting to think if what I originally wanted to do is as efficient as I first thought.
Basically I thought that inside each Resource file that is defined, each key could be prefixed with some kind of grouping information, e.g. HomeController , so I could have the following keys:
- HomeController_WelcomeText
- HomeController_Button1
- HomeController_Button2
With this I then thought of making a way to query the resources, so I could apply a prefix and then get in return all matched keys. I would do this by reflection and looping over each property, storing its name and value.
So to summarise I was stuck in the way of thinking one resource file and differentiating based on the string key. DOH!, thinking about it now, I think the best and most clean solution is to use a separate resource file per each localisable entity i.e. a Form, or Controller etc… If you think about it also, this will make things much more organised when you get to a point where you have thousands of resources. Having them logically separated in line with the entity which will be localised makes good sense. Not only that it also make the task I was thinking about much easier.
The Idea
If you think about ASP.NET MVC for example, and the following. You will always have resources which are common or global and then you will have resources which are specific to a certain part of the project, e.g. Home Controller.
I want to be able to output any localisation that I need so that I can consume with JavaScript without any unnecessary AJAX calls. More so I only want to output the required keys from the global resources and the resources specific to the area which it is currently being executed, i.e. Home Controller.
The format which I am thinking for the output of the client side localisation is simply a included JavaScript file with the contents simply declared variables which match the name of those inside the resource files i.e.
var Global_Resource_Key_1 = "Hello World";
var Global_Resource_Key_2 = "Hello Galaxy";
var Local_Resource_Key_1 = "Local Number 1";
var Local_Resource_Key_2 = "Local Number 2";
Going back to what I said above, these will be output by a method using reflection to iterate through each of its properties to then generate the required output. Once the iteration has complete it would be wise to store the resulting collection in Cache or Application object. I am thinking that the generation of the script will be using a HttpHandler, allowing for the variables to be dynamic inside the mark-up and script declaration.
Thinking about it more, this is exactly why we are given the special .NET folders of :
- App_GlobalResources
- App_LocalResources
These are great, but, I need to have the resource files inside another assembly so that they can be referenced both from the web and also internally from the calling assembly. So inside a test project I have done the following to setup:
- Create a MVC Application
- Delete the Account View and Controllers
- Move the Controllers, Global.asax.cs and Models to the Class Library project
- Create a separate Class Library Project
- Create a folder for resources
- Create a Global Resources and also a Resource file per Controller with the relevant folder structure
- Created a Configuration folder and class to handle the secret key for hmac’ing and cache timeout
- Created an Extension Method folder and HtmlHelper class which will be a shortcut for the deveoper to use which will generate the link
- Created a HttpHandlers folder and the actual handler which I will use to generate and cache the relevant properties
Also, I have omitted any DI/IOC for the purposes of this example. I will go through each of the Class Library project sections separately.
Create a folder for resources & Create a Global Resources and also a Resource file per Controller with the relevant folder structure
Having a separate folder in the resource means I can consume these from the web application but also from any models which are inside the assembly or business data for example where I may state the resource for validation attributes like those used in the DataAnnotations or the MVAB (Microsoft Validation Application Block).
I have but the global resource file directly inside this folder and then created sub folders to reflect different parts of the application which the one being in this instance, Web and then even further by controller. I think grouping the resources by Controller for the web is a logical step, and along with a global resource file, you are pretty much covered.
When you build the solution, .NET will logically group your assemblies by culture, so have many resource files still means they will compile down into one assembly, which is great.
Created a Configuration folder and class to handle the secret key for hmac’ing and cache timeout
I could quite as easily have used the AppSettings but I thought that it would be good to give this attempt its own configuration section, which I could extend and keep encapsulated in the future. The two things which I am using this section for at the moment is to store the key I will use to generate the HMAC hashes and also the sliding timeout for the cache of the resources. The secret key can be anything you want, but I needed it in a centralised place so i can ensure the same one is used to generate and also compare. There is not much to the Configuration Section accept a couple of required attributes and the syntax for retrieving the value declared in the config file.
<clientResourceConfiguration
resourceSlidingTimeout="20"
resourceHmacKey="470F0BE4675941baBEFBC1134CC1FEAF28C47C15D71543b8A9F57360CFFCD33B"/>
The secret key / resourceHmacKey here is simply two GUIDs stripped of the curlies and dashes and concatenated together. To reference this configuration inside the code, I have placed a static property on the MvcApplication class inside the Global.asax.cs file. Seemed like a logical place to put it, and of course made it a singleton.
public class MvcApplication : System.Web.HttpApplication
{
private static ClientResourceConfiguration _clientResourceConfiguration = null;
public static ClientResourceConfiguration ClientResourceConfiguration
{
get
{
if (_clientResourceConfiguration == null)
{
_clientResourceConfiguration = (ClientResourceConfiguration)ConfigurationManager.GetSection("clientResourceConfiguration");
}
return _clientResourceConfiguration;
}
}
...
Created an Extension Method folder and HtmlHelper class which will be a shortcut for the developer to use which will generate the link
This is simply to make it easier for the developer to create the link. You can simply add the type you want to parse, the name of the handler which is mapped in the config file and the culture. In this implementation I have designed it to expect the two letter culture name and then go on to resolve the specific culture. I know it would have been easier to just specify the specific culture but I dev’d this with a work related problem I had and wanted to simulate the environment in which I have to work with.
I have created an overloaded method so that if I need to I can force clear the cache for a specific handler. As I will show you further down I also output the date it was cached, again simply for diagnostic purposes.
The token here is simply so i can be sure that the resource file which is being requested has been authorized by the server, as it is the server which is the only entity that has the secret key and can create such links. I have included a timestamp which makes the HMAC hash different each time. The validation of this token will only occur if the requested resource is not in the cache, as I make the assumption that is if it is in the cache, then it has to have been generated for a valid reason by the server.
Oh and there is a small method in there I found on Brad Abrams site which simply gives me back the number of seconds since 1970, which acts as a timestamp.
public static class HtmlHelpers
{
public static string ClientResourceLink<T>(this HtmlHelper helper, string handler, string twoLetterCultureName)
{
return ClientResourceLink<T>(helper, handler, twoLetterCultureName, true);
}
public static string ClientResourceLink<T>(this HtmlHelper helper, string handler, string twoLetterCultureName, bool cache)
{
var typeName = typeof(T).FullName;
var timeStamp = GetTimeStamp().ToString();
var valueToHash = String.Concat(typeName, twoLetterCultureName, timeStamp);
var token = CryptoHelper.Hmac(valueToHash, MvcApplication.ClientResourceConfiguration.ResourceHmacKey, HashType.SHA1);
var url = String.Format("~/{0}?typeName={1}&culture={2}&token={3}×tamp={4}",
handler,
typeName,
twoLetterCultureName,
token,
timeStamp);
if (!cache)
{
url += "&cache=ncache";
}
return String.Format("<script type=\"text/javascript\" src=\"{0}\"></script>",
new UrlHelper(helper.ViewContext.RequestContext).Content(url));
}
/// <summary>
/// From Brad Abrams : http://blogs.msdn.com/brada/archive/2004/03/20/93332.aspx
/// </summary>
/// <returns></returns>
private static int GetTimeStamp()
{
TimeSpan t = (DateTime.UtcNow - new DateTime(1970, 1, 1));
int timestamp = (int)t.TotalSeconds;
return timestamp;
}
}
The CryptoHelper class is one which “I made earlier,” and which I blogged about here http://www.andrewrea.co.uk/2009/10/05/ACryptographyHelperClassForHashingAndForKHMACKeyedHashMessageAuthenticationCode.aspx. It is simply a helper method wrapping around some types and methods inside the System.Security.Cryptography namespace. You will see some example usages in the summary above.
Created a HttpHandlers folder and the actual handler which I will use to generate and cache the relevant properties
This is basically the crooks of the solution and it is the handler. I have used the .axd extension simply because it is already recognised and is ignored my the MVC route handler.
Below is the entry I have used to configure the Http Handler for GET only and an example path to map it to
<add verb="GET" path="/jsloc.axd" validate="false" type="ResourcesExampleMvc.Core.HttpHandlers.ClientSideResourceHttpHandler,ResourcesExampleMvc.Core" />
It is in this class where I handle:
- The parameters passed in
- The validation of the token
- The cache of the resources for the client
- The generation of the resources
I simply set the Response.ContentType to text/javascript and then write out the information through a StreamWriter. The first variable I add is the CacheDate and then followed by the resources themselves, as they appear inside the resource file. A point to mention here is if you have defined any of the keys in the resource files with spaces in they will be replaced with underscores.
Major Point : You must set the scope of your Resource File, which ever one you want the Handler to parse as Public, this is due to me put Binding Flags on the reflection as Public and Static. I suppose I could have added Internal, but not sure, so I will leave for now as Public.
public class ClientSideResourceHttpHandler : IHttpHandler
{
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
string typeName = context.Request.QueryString["typeName"];
string twoLetterCultureName = context.Request.QueryString["culture"];
string token = context.Request.QueryString["token"];
string timestamp = context.Request.QueryString["timestamp"];
string nocache = context.Request.QueryString["nocache"];
var timeout = MvcApplication.ClientResourceConfiguration.ResourceSlidingTimeout;
string key = GetKey(typeName, twoLetterCultureName);
if (context.Cache[key] == null || !String.IsNullOrEmpty(nocache))
{
var type = Type.GetType(typeName);
var resourceManager = new ResourceManager(type);
if (!ValidateToken(typeName, twoLetterCultureName, timestamp, token))
throw new SecurityException("Invalid token submitted for client resource");
var list = new List<KeyValuePair<string, string>>();
var properties = type.GetProperties(
System.Reflection.BindingFlags.Public |
System.Reflection.BindingFlags.Static);
foreach (var property in properties)
{
if (property.PropertyType == typeof(string))
{
list.Add(
new KeyValuePair<string, string>(property.Name,
resourceManager.GetString(property.Name.Replace("_", " "),
GetCulture(twoLetterCultureName))
)
);
}
}
context.Cache.Insert(key, list, null, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(timeout));
}
context.Response.ContentType = "text/javascript";
WriteOutClientResources(context.Response.OutputStream, (List<KeyValuePair<string, string>>)context.Cache[key]);
context.Response.End();
}
#endregion
private void WriteOutClientResources(Stream outputStream, List<KeyValuePair<string, string>> values)
{
using (var sw = new StreamWriter(outputStream))
{
sw.WriteLine(String.Format("var cache_date = \"{0}\";", DateTime.Now.ToString("f")));
foreach (var item in values)
{
sw.WriteLine(String.Format("var {0} = \"{1}\";", item.Key, item.Value));
}
sw.Flush();
}
}
private bool ValidateToken(string typeName, string twoLetterCultureName, string timestamp, string token)
{
var valueToHmac = String.Concat(typeName, twoLetterCultureName, timestamp);
var valueToCompare = CryptoHelper.Hmac(valueToHmac, MvcApplication.ClientResourceConfiguration.ResourceHmacKey, HashType.SHA1);
return valueToCompare.Equals(token);
}
private string GetKey(string controllerName, string twoLetterCultureName)
{
return String.Format("{0}!{1}", controllerName, twoLetterCultureName);
}
private CultureInfo GetCulture(string twoLetterCultureName)
{
switch (twoLetterCultureName)
{
case "fr":
return CultureInfo.CreateSpecificCulture("fr-FR");
default:
return CultureInfo.CreateSpecificCulture("en-GB");
}
}
}
If you did not want the overhead of a Handler, or you do not want the actual dynamic script reference, then there is nothing stopping you in cutting this write down, and making a HtmlHelper which simply parsing a type and outputs the JavaScript variables directly to the requesting resource, so instead of an include script tag, it would output a script block directly in the dom. Personally I just like the script tag and the visual reduction in code in the view source, I am unsure of any performance gain if any.
So that is basically it, this is something I am definitely going to test drive and among other things, use it for other purposes. One idea I had was to use this to generate Client Side objects based on say the models. I will do this for the next post I hope.