Click here to Skip to main content
Click here to Skip to main content

Under the Hood of BuildManager and Resource Handling

, 26 Aug 2010
Rate this:
Please Sign up or sign in to vote.
An automatic resource translator for ASP.NET applications

Introduction

ASP.NET application internationalization uses ResX file to handle translation of labels. This article describes a mocking technique that enables "automatic" translation for any resource contained in resource folders without the need to create and maintain a set of automatically translated resx files. The resource is presented embraced with a culture specific tag if current culture is different from default culture.

French version:

french.png

English version:

english.png

ASP.NET Resource Folders

ASP.NET Framework handles two kinds of resources:

  • Global resources can be used at any level of the web application. The resx file has to be stored inside the App_GlobalResources at the root of the application.
  • Local resources only do work at the page level. Each web page can define an associated resx file with string resources. The resx file must be stored in App_LocalResources at the same hierarchical level as the page and its name has to match web page name with a .resx extension of course.

For more information, here is a link on the MSDN dedicated page.

Building a Custom ResourceProviderFactory

Resource handling is made through a resource provider that enables reading the specific resource. Resource providers are created by a ResourceProviderFactory which propose two methods:

We will implement our own factory in order to create our custom resource providers.

using System.Web.Compilation;

namespace MyProject.Resources {

public class CulturePrefixedResourceProviderFactory : ResourceProviderFactory {

   public override IResourceProvider CreateGlobalResourceProvider(string classKey){
      return new CulturePrefixedGlobalResXResourceProvider(classKey);
   }
   public override IResourceProvider CreateLocalResourceProvider(string virtualPath){
      return new CulturePrefixedLocalResXResourceProvider(virtualPath);
   }
}

IResourceProvider Overview

Resource providers need to implement the IResourceProvider interface which defines:

  • ResourceReader property, returns a reader that enables resource reading
  • GetObject method, returns a resource value from its key and culture

Our local and global resource providers will override class AbstractResXResourceProvider that provide a bit abstraction with the implementation of IResourceProvider.GetObject method that creates the translation mocking aspect.

using System.Globalization;
using System.Resources;
using System.Web.Compilation;

namespace MyProject.Resources {

public abstract class AbstractResXResourceProvider : IResourceProvider {

   protected AbstractResXResourceProvider() { }

   protected abstract ResourceManager ResourceManager {
      get;
   }

   public virtual object GetObject(string resourceKey, CultureInfo culture) {
      if (ResourceManager == null) {
         return null;
      }
      if (culture == null) {
         culture = CultureInfo.CurrentUICulture;
      }
      object item = ResourceManager.GetObject(resourceKey, culture);
      string strItem = item as string;
      if (strItem != null && culture.TwoLetterISOLanguageName != "fr" && 
			culture != CultureInfo.InvariantCulture) {
         return string.Format(CultureInfo.InvariantCulture, 
			"[{0}]{1}[/{0}]", culture.Name, strItem);
      }
      return item;
   }
}

Build a Global Resource Provider

The global resource provider extends the AbstractResXResourceProvider and implements access to the resource manager located in the dedicated assembly for global resources. This one can be retrieved with the BuildManager.AppResourcesAssembly property.

My solution works with reflection as I didn't find any existing public entry point in the .NET API to do this.

//Invokes BuildManager.AppResourcesAssembly
protected virtual Assembly AppResourcesAssembly {
   get {
      Type buildManagerType = typeof(BuildManager);
      PropertyInfo property = buildManagerType.GetProperty("AppResourcesAssembly", 
	BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.GetField);
      return (Assembly)property.GetValue(null, null);
   }
}  

For global resources, every resx file is embedded as a resource with the AppResourcesAssembly with a "Resources." prefix. The access to the ResourceManager is very easy once you have access to the AppResourcesAssembly.

protected override ResourceManager ResourceManager {
   get {
      string baseName = "Resources." + this._classKey;
      ResourceManager manager = new ResourceManager(baseName, AppResourcesAssembly);
      manager.IgnoreCase = true;
      return manager;
   }
} 

Build a Local Resource Provider

The solution I implemented also uses a lot of reflection because of the actual lack of entry point in the .NET API to do this operation.

//Invokes VirtualPath.FileName property
protected string VirtualPathFileName {
   get {
      PropertyInfo fileNameProperty = _virtualPathType.GetProperty("FileName");
      return (string)fileNameProperty.GetValue(_virtualPathInstance, null);
   }
}

//Invokes VirtualPath.Parent property
protected object VirtualPathParent {
   get {
      PropertyInfo parentProperty = _virtualPathType.GetProperty("Parent");
      return parentProperty.GetValue(_virtualPathInstance, null);
   }
}

protected override ResourceManager ResourceManager {
   get {
      Assembly localResourceAssembly = LocalResourceAssembly;
      if (localResourceAssembly == null) {
         throw new InvalidOperationException
		("Unable to find the matching resource file.");
      }
      ResourceManager manager = new ResourceManager
		(VirtualPathFileName, localResourceAssembly);
      manager.IgnoreCase = true;
      return manager;
  }

//Invokes :
// string localAsmName = BuildManager.GetLocalResourcesAssemblyName(VirtualPath.Parent);
// object result = BuildManager.GetBuildResultFromCache(localAsmName);
// if (result != null) {
//    return ((BuildResultCompiledAssembly)result).ResultAssembly;
// }
protected virtual Assembly LocalResourceAssembly {
   get {
      string localResourceAssemblyName = 
	(string)_methodGetLocalResourcesAssemblyName.Invoke(null, new object[] 
		{ VirtualPathParent });
      object buildResultFromCache = _methodGetBuildResultFromCache.Invoke
		(null, new object[] { localResourceAssemblyName });
      if (buildResultFromCache != null) {
         return (Assembly)_propertyResultAssembly.GetValue(buildResultFromCache, null);
      }
      return null;
   }
}

Web Application Configuration

As usual, in ASP.NET, the web.config file allows you to modify the ResourceProviderFactoryType used for globalization.

<configuration>
    <system.web>
        <globalization resourceProviderFactoryType=
		"MyProject.Resources.CulturePrefixedResourceProviderFactory"
                        culture="fr-FR"
                        uiCulture="fr-FR" />
    </system.web>
</configuration> 

What's Next?

As you can see, creating custom resource providers for ASP.NET applications requires a little work. I use this tool during the testing of an internationalized application in order to ensure that every label is correctly filled within a resx file.

Another very classical resource provider implementation allows the resources to be retrieved from a database.

History

  • 27th August, 2010: Initial post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Olivier Gaudefroy
Technical Lead KLEE Group
France France
No Biography provided

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web03 | 2.8.140826.1 | Last Updated 27 Aug 2010
Article Copyright 2010 by Olivier Gaudefroy
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid