The localization story in ASP.NET is an interesting one. While there is a decent amount of information on how to use it, information on how it works and how you can modify it to your own needs is fairly weak. We’ll quickly review how localization works today in ASP.NET, how it is internally wired and then look at how we can tweak.
For the purpose of this post, I will refer to a fictional application called MyApp.
Review of Accessing Localized Resources in Asp.Net pages today
- Site-wide resources are stored under a root level directory in “App_GlobalResources.”
- Page-specific resources are stored in the local folder under a folder called “App_LocalResources”. The resources for a file named 'view.aspx' reside inside of the App_LocalResources folder under the name 'view.aspx.resx'
- Global Resources are accessed by providing the resource path in the asp.net parsed files in the form of <%$ Resources: [class], [member] %>
- Local Resources are accessed by providing the resource path in the asp.net parsed files in the form of <%$ Resources: [member] %>
How the Localization Scheme is implemented
- Visual Studio munges all resource table implementations in App_GlobalResources by forcing them into the absolute namespace of ‘Resources.’.
- Visual Studio compiles resources located under the individual App_LocalResources into their own resource assembly that gets embedded into the project as ‘[root namspace].[virtualpath].[pagename].resources
- Handoff of values across methods in ExpressionBuilder relies on casting via System.Object.
- The Resources: binding expression mentioned above is wired up in the system web.config with the following wireup:
<expressionBuilders>
<add expressionPrefix="Resources" type="System.Web.Compilation.ResourceExpressionBuilder"/>
</expressionBuilders>
Considerations for Change
A couple of brief thoughts that form our solution
- When you have more than one tier in your application, having shared resources is preferred. When sending your resources out for localization, duplicate definitions across resource files gives the opportunity for translation inconsistency.
- The solution needs to factor out the casting to/from System.Object for the purpose of clarity.
- If you avoid code wizards, the enlistment of visual studio in this scheme probably leaves you with high blood pressure.
I’m not going to beleaguer the third point – I’m guessing that there is a use-case for this, but I have not personally seen it. After digging through the handlers for the global and local resources, my resolve for rewriting this grew even more.
Creating Our Custom Handlers
When creating my own custom handler for resources, I started by looking at ResourceExpressionBuilder which derives from System.Web.Compilation.ExpressionBuilder for code to reuse. What I found was nothing worth reusing, but plenty of ideas. The overloaded methods provided a view of discrete responsibilities that I isolated in the final implementation. Because the base expression builder is expected to throw System.Web.HttpException, this also called for factoring responsibilities in a manner that allows me to move the core mechanics to another assembly without requiring a reference to System.Web.
My intent is to wireup a keyword in the web application to a single resource table. In this case, I will create a MyAppResources keyword that will resolve the member residing in a separate resource assembly at MyApp.Resources.MyAppResources.resx. The solution below is can be wired up however you like, this is just my choice in this instance.
Our first step is therefore creating an interface that isolates the responsibilities independent of the implementation called IResourceValueProvider
1: /// <summary>
2: /// Interface responsible for breaking out the discrete responsibilities in providing
3: /// resource strings to the application based on free text validation
4: /// </summary>
5: public interface IResourceValueProvider
6: {7: /// <summary>
8: /// Responsible for taking the expression and parsing it into a meaningful path to
9: /// a resource value
10: /// </summary>
11: /// <param name="expression">string passed from the pages</param>
12: void ParseExpression(string expression);
13: 14: /// <summary>
15: /// Responsible for validating that the expression resolves to the target resource table
16: /// </summary>
17: /// <returns>true if found</returns>
18: bool ValidateParsedValue();
19: 20: /// <summary>
21: /// Responsible for returning the string value from the resource table
22: /// </summary>
23: /// <returns>resolved resource value</returns>
24: string GetResourceValue();
25: 26: /// <summary>
27: /// Responsible for describing the resource table name
28: /// </summary>
29: string TargetResourceTable { get; }
30: }The code comments do a fair job of explaining their responsibilities, so I’ll focus on a couple of key points here not explained. In our implementation of expecting a single value, the incoming expression in ParseExpression will not need parsing. If we decided to support [class], [member], then we would obviously require a String.Split implementation. Also, the TargetResourceTable member is responsible for describing the target resource table floated up in error conditions.
Implementing our CustomHandler
The following implementation is done as close to our Resource table as we can. There are many reasons for this and while it could be implemented in our web application due to the use of reflection, the corresponding explicit reference in Web.Config (shown later) provides a level of immediate recognition as to where these resources live.
1: /// <summary>
2: /// Responsible for providing access to the MyAppStringTable resources. As this
3: /// approach is used more, most of this functionality is likely to be
4: ///
5: /// </summary>
6: public class MyAppResourceValueProvider : IResourceValueProvider
7: {8: protected string Expression { get; private set; }
9: protected PropertyInfo PropertyInfo { get; private set; }
10: #region IResourceValueProvider Members
11: 12: /// <summary>
13: /// Parse expression is where the passed in string must be interrogated according to requirements.
14: /// As this is a single string that maps directly to the object, there is no need to perform
15: /// any parsing aside from capture.
16: /// </summary>
17: /// <param name="expression"></param>
18: public void ParseExpression(string expression)
19: {20: // the expression is not null checked as this is a closed system where null or empty expressions
21: // are explicitly handled conditions by the caller
22: Expression = expression; 23: } 24: 25: /// <summary>
26: /// This method is responsible to make sure that the passed along expression maps to something meaningful.
27: /// </summary>
28: /// <returns>true if the expression is valid</returns>
29: public bool ValidateParsedValue()
30: {31: PropertyInfo = (typeof (MyAppStringTable)).GetProperty(Expression,
32: BindingFlags.NonPublic | BindingFlags.Static | 33: BindingFlags.IgnoreCase); 34: 35: return PropertyInfo != null;
36: } 37: 38: /// <summary>
39: /// Retrieves the string value from the resource table
40: /// </summary>
41: /// <returns></returns>
42: public string GetResourceValue()
43: {44: return PropertyInfo.GetValue(null, null) as string;
45: } 46: 47: /// <summary>
48: /// Retreives the name of the resource table targeted by this handler
49: /// </summary>
50: public string TargetResourceTable
51: {52: get { return "MyAppStringTable"; }
53: } 54: 55: #endregion
56: }Extending ExpressionBuilder to Parse and Evaluate
Our last job is to extend the ExpressionBuilder to wire this up with error handling.
1: /// <summary>
2: /// This generic mapping permits a resource file to be mapped in the web.config file with a clear
3: /// declaration in the web.config expressionBuilder set.
4: /// </summary>
5: /// <typeparam name="TResourceValueProvider">a resource value provider that maps string names to resource properties</typeparam>
6: internal class GenericResourceExpressionBuilder<TResourceValueProvider> : ExpressionBuilder
7: where TResourceValueProvider : IResourceValueProvider, new()
8: {9: /// <summary>
10: /// Returns a realizable CodeExpression that represents the parsed data passed in. By this time, the
11: /// existance of the CodeExpression has been determined
12: /// </summary>
13: /// <param name="entry">context of the bound property</param>
14: /// <param name="parsedData">parsed data as defined by the ParseExpression method</param>
15: /// <param name="context">builder context</param>
16: /// <returns>CodeExpression used for evaluating the returned value</returns>
17: public override CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
18: {19: var provider = parsedData as IResourceValueProvider;
20: 21: // the following situation should never happen as ParseExpression does not permit a null value through
22: if (provider == null)
23: throw new HttpException("Critical Error: the declared property identification process has failed");
24: 25: return new CodePrimitiveExpression(provider.GetResourceValue());
26: } 27: 28: /// <summary>
29: /// Parses the expression and tests that it maps into the resource file
30: /// </summary>
31: /// <param name="expression">text expression from the web site</param>
32: /// <param name="propertyType">expression type</param>
33: /// <param name="context">builder context inside the site</param>
34: /// <returns></returns>
35: public override object ParseExpression(string expression, Type propertyType, ExpressionBuilderContext context)
36: {37: if (String.IsNullOrEmpty(expression) || String.IsNullOrEmpty(expression.Trim()))
38: throw new HttpException("The globalization field cannot be null or empty");
39: 40: var provider = new TResourceValueProvider();
41: 42: try
43: { 44: provider.ParseExpression(expression); 45: }46: catch( Exception e)
47: {48: throw new HttpException(e.Message, e);
49: } 50: 51: if (!provider.ValidateParsedValue())
52: throw new HttpException(string.Format(CultureInfo.InvariantCulture,
53: "The field {0} is not declared on {1}",
54: expression, 55: provider.TargetResourceTable)); 56: 57: return provider;
58: } 59: 60: /// <summary>
61: /// Declares that this process does not require evaluation
62: /// </summary>
63: public override bool SupportsEvaluate
64: { 65: get 66: {67: return false;
68: } 69: } 70: }Pulling it All Together in ASP.NET
Our final step is pulling this into the local web.config with the following snippet.
1: <expressionBuilders> 2: <add 3: expressionPrefix="MyAppResources"
4: type="MyApp.GenericResourceExpressionBuilder<MyApp.Resources.MyAppResourcesValueProvider,
5: MyApp.Resources>, MyApp"/> 6: </expressionBuilders> Final Wrap
So there you have it – a view of how resources are managed in Visual Studio and the tools to change it to match differing requirements. In my server-side files, I can now point to my resources quite simply. If I want to grab the value named 'ApplicationName' in my resource file, the accompanying markup looks like this:
<%$ MyAppResources: ApplicationName %>