Working with the ResourceManager [Kim Hamilton]

The essentials of resource fallback and how to debug failures

Resource loading failures can be tricky to debug. However, once you have a basic overview of how the ResourceManager works, as well as a few debugging tools, you’ll be able to easily get to the bottom of most problems.

This post will cover the key elements of resource lookup and debugging with a simple code sample, addressing the following:

  • Basics of resource generation and file structure
  • How to construct a ResourceManager to find your resources
  • How resource fallback works
  • How to debug resource failures

Code Sample

The code sample is posted in the BCL code gallery downloads as ResourceManagerSample.zip. The zip file contains:

  • .restext files in different languages: these are text files containing key, value pairs from which we’ll generate .resources files.  Note that the extension restext isn’t important, it can be anything you want.
  • FallbackTest.cs: sample code that looks up resources in different cultures and prints the results
  • make.bat: builds main exe and satellite assemblies

Instructions

  1. Extract the zip file and run make.bat, which does the following:
    1. Converts the .restext files to .resources files (the binary format understood by the ResourceManager) using resgen.
    2. Compiles the main exe and embeds the invariant resources, or the resources that are always expected to exist if other cultures are missing.
    3. Generates satellite assemblies for the other cultures, placing them in culture-specific subfolders

After Step 1, the directory structure looks like the following:

 FallbackTest.exe                 (main executable, contains FallbackStrings.resources)
de\FallbackTest.resources.dll    (de satellite assembly, contains FallbackStrings.de.resources)
fr\FallbackTest.resources.dll    (fr satellite assembly, contains FallbackStrings.fr.resources)
fr-CA\FallbackTest.resources.dll (fr-CA satellite assembly, contains FallbackStrings.fr-CA.resources)
  1. Run FallbackTest.exe to see the results, discussed below

Before diving into the results, let’s look at the source of FallbackTest.cs to see what it’s doing.

Creating a ResourceManager

The ResourceManager can perform two types of resource lookup, depending on how you construct it.

  • File-based: resource files are directly on disk in the binary .resources files. Create this type of ResourceManager via ResourceManager.CreateFileBasedResourceManager.
  • Assembly-based: resources are embedded in an assembly or a satellite assembly. Create this type of ResourceManager with the standard constructors.

Assembly-based is by far the most commonly used; a satellite assembly has the same advantages as a regular assembly such as versioning and the ability to sign. That’s why we’re focusing on assembly-based lookup in this example.

The code sample creates a ResourceManager as follows:

 ResourceManager rm = new ResourceManager("FallbackStrings",
                            Assembly.GetExecutingAssembly());

The first argument is baseName — the name of the resources files we’re searching without the culture and “.resources” extension.  In step 1a above, resgen created files named, for example, FallbackStrings.fr.resources and FallbackStrings.fr-CA.resources. So baseName in this example would be FallbackStrings.

The second argument is the main assembly for the resources. Since we’re calling from that assembly, we can just say Assembly.GetExecutingAssembly(). Note that this is the common case.

Resource Fallback Model

Let’s move to the resource lookups and sample code output. First, notice these lines:

 CultureInfo ci = new CultureInfo(culture);
Thread.CurrentThread.CurrentUICulture = ci;

This is done because the ResourceManager uses the current thread’s CurrentUICulture to search for resources by default. ResourceManager.GetString also has overloads that accept a CultureInfo. If provided, that culture will be searched instead.

After setting the UI Culture, fallback test searches for a couple of localized strings and prints them to the screen.

 Current UI culture: fr-CA
Greeting: bonjour
End of week: la fin de semaine
------------------------------
Current UI culture: fr-FR
Greeting: bonjour
End of week: le week-end
------------------------------
Current UI culture: de-DE
Greeting: Guten Tag
End of week: weekend
------------------------------
Current UI culture: ja-JP
Greeting: hello
End of week: weekend
------------------------------

Let’s focus on fr-CA and fr-FR. fr-CA is the French Canadian locale, which has some different words than fr-FR, the French spoken in France.

In this code sample, we’ve included resources for fr-CA, but none for fr-FR. Instead, we’ve provided resources for fr, which is the neutral French culture. A neutral culture is not region-specific and is the parent of its corresponding region-specific cultures. For example, fr is the parent culture of both fr-CA and fr-FR.

A quick aside on this decision:

Resource divisions like this are fairly common in practice; the most common words for a language (across regions) are placed in the neutral culture’s resource file, and any deviations from that are placed in the specific culture’s resource file. This minimizes duplication in resource files.  So in this example, the choice to put greeting in fr allowed us to share resources across French-speaking regions, but adding explicit resources for fr-CA allows French Canadian to differ from the most commonly-used French.

Now notice in the program output that fr-CA and fr-FR have the same greeting but different words for the end of the week. That’s because for greeting, fr-CA resources didn’t have a value, so it fell back to fr (same as fr-FR).

In general, a resource fallback chain looks like this.

Culture passed to GetString(, x) or ThreadUICulture (fr-CA) -> parent culture (fr) -> invariant culture (embedded in main assembly)

Compare the output for the other cultures above to see why the output makes sense.

The fallback chain can also vary as follows:

  • In .NET 4 (and Silverlight 2), on Windows Vista and above, we merge in user and system preferences according to the API GetThreadPreferredUILanguages (with flags MUI_MERGE_USER_FALLBACK | MUI_MERGE_SYSTEM_FALLBACK). These cultures are searched after the specified culture and its parents, but before the invariant culture
  • NeutralResourcesLanguageAttribute on the assembly: this allows performance improvements and allows ultimate fallback resources to be located in a satellite assembly (and in a different culture), instead of the main assembly. About the NeutralResourcesLanguageAttribute
  • Specify in the app’s config that only certain assembly locales should be probed. This also allows performance improvements. Note that this won’t add cultures to the fallback chain, it will only remove them. Performance tips for using resources

There are 3 common tools I use for debugging resource-related issues. These are:

  • Fusion: shows attempts to load satellite assemblies, which can help uncover assembly-loading problems
  • Reflector: view embedded .resources file
  • Resview: same as above but also has diagnostics to tell if there are problems

Let’s look at these in-depth by seeing what can go wrong. Comment out the call to GoodTest(), then uncomment the call to BadTest(), and rerun make.bat. This code has a bug, which is that the baseName is incorrect (“Fallback_Strings” instead of “FallbackStrings”). Running the code, we get the following exception:

System.Resources.MissingManifestResourceException: Could not find any resources appropriate for the specified culture or the neutral culture.  Make sure "Fallback_Strings.resources" was correctly embedded or linked into assembly "FallbackTest" at compile time, or that all the satellite assemblies required are loadable and fully signed.

Let’s investigate this failure showing how the different tools are used:

Fusion logs

To enable fusion logs, launch the fusion log viewer, fuslogvw.exe, which is provided as a Framework sdk tool (you can find it under Framework\tools). Choose the option “Log all binds to disk”. I also select the “Enable custom log path” checkbox and specify a path such as C:\fuslog. (Since fusion logs are written as html files, I prefer to browse the files directly on disk than view the output in the fusion log viewer.)

Then run your program and look in C:\fuslog for the results.

In C:\fuslog\Default\FallbackTest.exe\, I see there were binding attempts for fr-CA and fr, because there are files named, e.g.:

“FallbackTest.resources, Version=0.0.0.0, Culture=fr-CA, PublicKeyToken=null.HTM”

Opening those files, you can see that the assembly did load correctly, so the problem isn’t a missing satellite assembly.

If the fusion log file did indicate a problem with the satellite assembly loading, then you know that either the satellite assembly is missing or has some other problem (version or signing problem), in which case you most likely have a build or install problem to correct.

Reflector/Resview

Both of these allow you to see the contents of a .resources file embedded in an assembly, but resview (part of the Framework sdk) has additional diagnostic tools. However, Reflector provides a nice interface for browsing the resource file, and I most often use that.

Open up the assembly of interest (either satellite or main) in Reflector. Expand the “Resources” section of the assembly to verify that the baseName we specified is actually present. As expected, we have a resource file named FallbackStrings.X.resources, but not Fallback_Strings.X.resources, and the solution is to remove the underscore from the baseName.

While this file name problem seems basic, it turns out to be a very common problem, so keep it in mind.

Another simple common mistake is if the key isn’t actually in the file. You can double-click on the .resources file in reflector to see the contents.