.NET Fusion
Home About Workshops Articles Writing Talks Books Contact

9. Resources and Satellite Assemblies

You saw in Example 6.1 that assemblies can contain resources, and sometimes these resources are in external files. Resources are used to contain extra data that are used by your code, but significantly, they can be localized. This means that you can have resources in multiple languages. At runtime, when you know the locale where the code is running, your code can load and use the appropriate resource. In this section you will see how resources are stored and how they are loaded.

9.1 Embedded and Linked Resources

Resources can be embedded or linked, they can be raw or compiled, and they can be localized or neutral. On this page we will explain all of these.  Start by creating a text file called strings.txt that contains this text:

This is some string data

Next create a process assembly app.cs that has this code:

using System;
using System.IO;
using System.Reflection;

class App
{
   static void Main()
   {
      Assembly a = Assembly.GetExecutingAssembly();
      Stream stm = a.GetManifestResourceStream("strings");
      using (StreamReader sr = new StreamReader(stm))
      {
         Console.WriteLine(sr.ReadToEnd());
      }
   }
}

Compile this with the following command line:

csc app.cs /res:strings.txt,strings

Run the process and confirm that you see the text printed on the command line. The /res command line switch indicates that the resource should be embedded into the assembly. The format of the switch is that the first part is the resource filename and the part after the comma is the name that the resource will have in the file; this is the name that you pass to GetManifestResourceStream. If you omit to name of the resource then the name of the file will be used.

To see the effect of this switch take a look at the manifest with

ildasm /text /item=app app.exe

Within the results you'll see this:

.mresource public strings
{
}

The .mresource indicates that the resource is a manifest resource and since there is no other directives, the resource is embedded. Rename the resource file (rename strings.txt strings.old), run the process again to convince you that the strings are being picked up from the assembly. Now restore the file (rename strings.old strings.txt).

Next, compile the process again, but use /linkres instead of /res. Run the app to prove that the resources are being printed. Now edit the file strings.txt and run the process again to confirm that the data in the file is being shown each time. Now rename the resource file and run the process again. You'll find that you'll get a ArgumentNullException from the StreamReader constructor because GetManifestResourceStream could not find the linked resource and returned a null. This is a great example of how not to handle errors. The actual error is that the file does not exist, the ArgumentNullException is thrown because the return value from GetManifestResourceStream was not handled correctly. The only consequence is that ArgumentNullException will confuse the user.

.NET Version 3.0
The exception you'll see with version 3.0/2.0 of the framework is FileNotFoundException, which makes far more sense. The description of the exception even tells you the name of the file that could not be found. Finally, someone at Microsoft is paying attention.

Now restore the resource file to its original name. Use ildasm to look at the manifest of this assembly, you will find something like this:

.file nometadata strings.txt
.hash = (B3 8B 6F 0F A8 02 5D 50 DE C5 EA 81 CB 2D 36 77
11 D6 74 D3 )
.mresource public strings
{
   .file strings.txt at 0x00000000
}

The first thing to point out is that the .mresource entry has a .file entry indicating that the resource is linked. The .file entry above indicates that it is a resource file (nometadata means that the file has its own format) and after that is a hash of the external file. On an earlier page you edited the resource file before you ran the process, clearly in this case the hash will be different. In fact the hash is not checked unless the assembly is signed, however, it will only occur if the resource is part of a library, not if it is part of the process assembly. (This is covered in the Security Workshop.)

Embedding a resource file means that the data is always available, at the expense of increasing the module file size; linking a resource file means that the resource file can be edited at a later stage, but since it is a separate file you must take steps to deploy it along with the module. The linked resource may look as if you can change resources on the fly, this is not the case. Edit the process to include a while loop:

Assembly a = Assembly.GetExecutingAssembly();
while (true)
{
   Console.ReadLine();
   Stream stm = a.GetManifestResourceStream("strings");
   using (StreamReader sr = new StreamReader(stm))
   {
      Console.WriteLine(sr.ReadToEnd());
   }
}

The idea is that there should be a pause before the resource is obtained to give you an opportunity to edit the file. After the resource is used it is disposed (this is the reason for the using statement, it will call Dispose on the StreamReader which in turn will call Dispose on the Stream).

Load the resource file with notepad. Now run the process and when it pauses, change the resource file and save the result. Press Enter at the command line to confirm that the resource file has been read. Repeat the test. This second time round you'll get an error from notepad. (Use Ctrl-C to close the process.) This indicates that notepad cannot write to the specified file. What has happened here is that the first time GetManifestResourceStream is called it puts a lock on the resource file and this lock remains until the process is shut down. If you want to edit a linked resource file you should do so only when the assembly is not loaded. However, you should resist editing a linked resource file because this will upset security as will be explained in the security workshop.

9.2 Compiled Resources

Resources can be raw or compiled, the last example used raw resources but the problem is that all you get is a stream and then it is up to you what you do with the stream. Note that many of the UI items that you would store as a Win32 resource have a .NET class that can be constructed from a stream. For example, Bitmap, Cursor, Icon and Metafile. However, if you have many resources then you'll find that you will have many .mresource entries and you'll have a problem giving each one a meaningful name. This is where compiled resources come in. A compiled resource combines several resources together into one .mresource entry. The entry can be named after the form or class that the resources are associated with, so there is a useful level of categorization. The Assembly class has a method GetManifestResourceNames that lists all of the .mresources in an assembly, and a method called GetManifestResourceInfo that returns information about a specific resource like whether it is embedded or linked and if it is linked it gives the name of the external file.

Edit the strings.txt file so that it looks like this:

one=first string
two=second string

This can be compiled with the resgen utility:

resgen strings.txt

this will generate a file called strings.resources. You can add this to an assembly as a linked or embedded resource. You can use GetManifestResourceStream to obtain this resource, but the problem is that the stream returned will be the entire compiled resource. The framework comes with a class called ResourceReader that will decompile the data in the resource. Add a using statement to your file:

using System.Resources;
using System.Collections;

Next, change the Main method to load the resources through the ResourceReader and then access the items in the resources:

Assembly a = Assembly.GetExecutingAssembly();
using(Stream stm = a.GetManifestResourceStream("strings.resources"))
{
   ResourceReader reader = new ResourceReader(stm);
   foreach (DictionaryEntry de in reader)
   {
      Console.WriteLine("{0}={1}", de.Key, de.Value);
   }
}

Compile this linking to the resource file either as an embedded, or a linked resource. You'll find that the decompiled resources will be printed on the console.

This is a pain to use if you want to load a specific resource rather than having to iterate through all resources as in this example. To help here, the framework provides the ResourceSet class that simply iterates through all resources and stores them in a collection so that each resource can be accessed individually:

using(Stream stm = a.GetManifestResourceStream("strings.resources"))
{
   ResourceReader reader = new ResourceReader(stm);
   ResourceSet resourceSet = new ResourceSet(reader);
   Console.WriteLine(resourceSet.GetString("one"));
}

This works fine for cultural neutral resources in the current assembly. However, culture specific resources can be in assemblies other than the current assembly so you'll have to load the right assembly and determine the name of the right manifest resource. To do this you'll use the ResourceManager class.

Next, change the Main method so that it creates an instance of the ResourceManager and uses this to load a resource:

Assembly a = Assembly.GetExecutingAssembly();
ResourceManager resources = new ResourceManager("strings", a);
Console.WriteLine(resources.GetString("one"));

The constructor is overloaded, this version takes the name of the resource (without the .resources extension) and a reference to the assembly that contains the resource. Technically, the assembly is described as the main assembly for the resources, because the ResourceManager will use this as the place to start its search for the resource.

Note that you give the name of the resource without the .resources extension because the ResourceManager will add this for you. If you provide an extension the ResourceManager will throw an ArgumentException. In fact the ResourceManager does more than just adding the extension when it constructs the name of the resource as you'll see later.

Compile the process and add the resource file as an embedded resource:

csc app.cs /res:strings.resources

Again, note that the manifest resource should have a name with an extension of .resources. Run this application and confirm that it does pick up the first string as identified in the strings.txt file.

9.3 XML Compiled Resources

This is fine if you want to use string resources in an application, for example, error messages, captions on dialogs etc. However, you'll also want to add binary resources like icons or bitmaps and there is no obvious way to add a binary resource with this mechanism that obviously takes strings. In fact the solution is quite easy. Text encoding is used to convert binary values into printable text values and the Convert class has two methods, ToBase64String and FromBase64String, to do this (note that I have written a stream class that converts between binary data and base64 more efficiently than the framework classes).

Managed XML resources are commonly known as .resx files and you can write them with the ResXResourceWriter class, similarly you can read the data from an XML .resx file using the ResXResourceReader class. However, you're unlikely to read data from the .resx file, instead you are more likely to compile the .resx file and read it with ResourceManager.

The resgen tool takes either a .txt or a .resx as the input, and more interestingly, it will also take a .resources file as input which means that it will decompile a .resources file to a .txt or a .resx file. The .resx file has a specific schema and it will not compile if you stray from this schema. When you create a Windows Forms project with Visual Studio the project wizard will create a .resx file for each form you add to the project. If you are not using the Visual Studio then you'll have to generate the .resx file yourself.

It is actually quite easy to do this because, as I've already mentioned, the  resgen tool can be used to decompile .resources files. The steps are simple. First create an empty text file (copy nul test.txt), then compile this as a resource file (resgen test.txt) finally decompile this as a .resx file:

resgen test.resources test.resx

This will create an XML file called test.resx that contains a fairly lengthy comment, the schema, and some default values. You can delete the comment. As with most XML files the schema is useful because it allows tools to determine if the file is valid, but if you will guarantee that you'll use entries that obey the schema then you can delete that too. These are the entries that you must have

<?xml version="1.0" encoding="utf-8"?>
<root>
 <resheader name="resmimetype">
  <value>text/microsoft-resx</value>
 </resheader>
 <resheader name="version">
  <value>1.3</value>
 </resheader>
 <resheader name="reader">
  <value>System.Resources.ResXResourceReader,
    System.Windows.Forms, Version=1.0.5000.0,
    Culture=neutral, PublicKeyToken=b77a5c561934e089
  </value>
 </resheader>
 <resheader name="writer">
  <value>System.Resources.ResXResourceWriter,
    System.Windows.Forms, Version=1.0.5000.0,
    Culture=neutral, PublicKeyToken=b77a5c561934e089
  </value>
 </resheader>
</root>

The last two items are self explanatory, they describe the classes used to read and write a .resx file using this schema. However, far more interesting is the resmimetype. This describes the default representation for data, that is, the nodes will contain text.

.NET Version 3.0
You will get different values with .NET version 3.0/2.0. First, the <resheader> version will be 2.0. Second, the resource reader and writer classes will obviously be in version 2.0.0.0 of the framework assemblies. Note that the schema has changed, which means that the <data> node can have an element called <comment>, the inner text of this element cann be used for text comments and it will be ignored by the resource compiler.

Adding data to a resource file is straightforward. Each node is <data> which contains a node called <value>. The <data> node will have a name attribute and (optionally) a type attribute with the fully qualified type that the item represents. The inner text of the <value> node will have the item, and if you don't specify a mimetype attribute on the <data> node then the default resmimetype will be used, which means that the type must be one of the following types:

String, Byte, SByte, Int16, Int32, Int64, UInt16, UInt32, UInt64, Single, Double, DateTime, TimeSpan, Decimal.

The ResourceManager reads the type and the value and will create a type from that value. You can get an item from compiled resources using ResourceManager.GetObject providing the name of the resource (corresponding to the name attribute of the <data> node). GetObject returns an Object so you still have to cast it to the required type.

For example add the following (highlighted) text to the test.resx file.

  <data name="Data" type="System.Int32,
       mscorlib, Version=1.0.5000.0,
       Culture=neutral, PublicKeyToken=b77a5c561934e089">
    <value>42</value>
  </data>
</root>

.NET Version 3.0
Use the following for the assembly name:
mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

This indicates that Data is a 32-bit integer. The code to read this value is straightforward:

static void Main()
{
   Assembly a = Assembly.GetExecutingAssembly();
   ResourceManager resources = new ResourceManager("test", a);
   object obj = resources.GetObject("Data");
   int data = (int)resources.GetObject("Data");
   Console.WriteLine("value is {0}", data);
}

Compile the application with:

resgen test.resx
csc app.cs /res:test.resources

and run it to confirm that the value of 42 is printed on the command line when you run the app.

If you want to use a type other than those identified above, you need to serialize an initialized type (using either BinaryFormatter or SoapFormatter) and then encode the result to base64. The simplest way to do this is to use the ResXResourceWriter class. For example, imagine that you have a .cur file and you want to use this to initialize the cursor of a form. To do this create a new console application called genCursor.cs and use the following code:

// First parameter is the name of the icon in the .resx file
// Second parameter is the name of the cursor file
using System; using System.IO;
using System.Resources;
using System.Windows.Forms;

class App
{
   static void Main(string[] args)
   {
      if (args.Length < 2) return;
      using (ResXResourceWriter writer
         = new ResXResourceWriter(Console.Out))
      {
         using (FileStream fs = File.OpenRead(args[1]))
         {
            Cursor cursor = new Cursor(fs);
            writer.AddResource(args[0], cursor);
            cursor.Dispose();
            writer.Generate();
         }
      }
   }
}

This code redirects the output of the ResXResourceWriter to the console, so that you can either pipe the output to a file, or just copy it from the console. Once you have compiled this you can run it. Create a cursor file with Visual Studio and then use Cursor as the name to be used in the generated .resx file followed by the cursor file name; pipe the output to another file:

genCursor Cursor test.cur > test.resx

Here's a partial output that I get:

<data name="Cursor" type="System.Windows.Forms.Cursor,
      System.Windows.Forms, Version=1.0.5000.0,
      Culture=neutral, PublicKeyToken=b77a5c561934e089"
      mimetype="application/x-microsoft.net.object.bytearray.base64">
  <value>
    AAABAAEAEBAQAAAAAACoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAwAAAAAAAAAAAAAAA
    <!-- other data omitted -->
    AAD+/wAA/v8AAP7/AAD+/wAA/v8AAP7/AAA=
  </value>
</data>

Note that the mimetype is given as application/x-microsoft.net.object-.bytearray.base64. This means that the BinaryFormatter was used to serialize the Cursor object. Compile this resource file.

Now you can create a windows forms application that uses the cursor:

using System;
using System.Resources;
using System.Windows.Forms;
using System.Reflection;

class MainForm : Form
{
   MainForm()
   {
      Assembly a = Assembly.GetExecutingAssembly();
      ResourceManager resources = new ResourceManager("test", a);
      this.Cursor = (Cursor)resources.GetObject("Cursor");
   }
   static void Main()
   {
      Application.Run(new MainForm());
   }
}

Compile this code:

csc /t:winexe app.cs /res:test.resources

Now run the application and see that the cursor is used by the form.

9.4 Culture Specific Resources

So what has all this got to do with Fusion? So far the resources that have been loaded have been from the main assembly and so by default this means that they are the culture neutral resources. The ResourceManager class will attempt to load resources according to the current culture of the thread. The Thread class has two culture properties: CurrentCulture and CurrentUICulture. The CurrentCulture determines the culture used when you format dates and numbers (for example, when you pass a date to Console.WriteLine).

The ResourceManager uses the CurrentUICulture property. When you create a ResourceManager object the constructor will test the CurrentUICulture and it will generate the name of the resource from the string name (RFC1766) of the culture:

<resource name>.<culture>.resources

Here, <resource name> is the name you pass to the constructor, so an example, if you pass test then the localised resource maybe be test.en-US.resources. The ResourceManager then attempts to find a satellite assembly with this resource. A satellite has a short name in the form:

<neutral assembly name>.resources.dll

where <neutral assembly name> is the short name of the assembly that you pass to the ResourceManager constructor. For example app.resources.dll. Of course, this will mean that you may have several satellites with the same short name. The GAC can handle this, but NTFS cannot, so if you have private satellite assemblies these should be stored in subfolders under the application's folder with names that reflect the culture of the satellite. Finally, the full name of the satellite should have a culture. A satellite is resource only, so it contains no code. This means that you should create the satellite using the assembly linker tool, al.

I find the concept of a satellite assembly odd. This is a localised resource, so there really is no need to put it in an assembly. One argument is that using an assembly means that you can use culture in the full name, but as you can see here that means that if the assembly is private you have to provide a subfolder with the culture name, so you have gained nothing using an assembly. Indeed, if the resource was just a .resources file you could give it a name that contains the culture, just as you do for the resource. The only advantage in using a satellite assembly is that you can put it in the GAC. This is only of an use if you are generating resources for a library. If you are creating resources for a process the satellites have to be private assemblies, so they may as well be simple .resources files.

As an example, we will create a console application that will have several satellites. To do this create a new folder and then under that create the following subfolders:

md en
md en-US
md en-GB
md fr

These will contain resources for general English, American English, British English and general French. In each folder create a text file called strings.txt that contain the line:

Language=English

Replace English with the appropriate language name. For each file create the satellite assembly by compiling the resource and then generating the assembly:

resgen strings.txt strings.en.resources
al /embed:strings.en.resources /c:en /t:library /out:app.resources.dll

clearly for each one you should replace the en with the specific culture you are compiling.

Now move to the parent folder and create a text file called strings.txt containing:

Language=Neutral

Compile this file (resgen strings.txt). Note that I have created a batch file that will do all of these steps for you, just type go at the command line.

Now you need to create the assembly that uses the resources, which is the assembly that will contain the neutral resource. The code looks like this:

using System;
using System.Resources;
using System.Threading;
using System.Globalization;
using System.Reflection;

class App
{
   static void Main(string[] args)
   {
      Console.WriteLine("initial UI culture is {0}",
         Thread.CurrentThread.CurrentUICulture);
      if (args.Length > 0)
      {
         Thread.CurrentThread.CurrentUICulture
            = new CultureInfo(args[0]);
         Console.WriteLine("changed UI culture to {0}",
            Thread.CurrentThread.CurrentUICulture);
      }

      Assembly a = Assembly.GetExecutingAssembly();
      ResourceManager resources
         = new ResourceManager("strings", a);
      Console.WriteLine(resources.GetString("Language"));
   }
}

This code takes an RFC1766 locale name as a parameter, and it will change the locale of the current thread to the specified locale. If you do not specify a locale the current one will be used. Compile this code, but make sure that you embed the neutral resource in the application:

csc app.cs /res:strings.resources

Try the parameters in the left hand column in this table, the results should be those in the right hand column:

Locale String
en English
en-GB British English
en-US US English
en-AU English
fr-FR French
de-DE Neutral

What this tells you is that the ResourceManager uses fallback when it chooses the satellite. First, it will try and find the satellite with the specified local. If such a satellite does not exist, it will load the resource with the language ID (for example the en resource is loaded for the en-AU locale). If a language resource is not available then the neutral resource from the main assembly will be used (for example for the de-DE locale).

Now here's an odd thing. My copy of XP Pro is set to British locale, which makes sense because I am English and I prefer my dates in the more logical day-month-year format. However, the call to Thread.CurrentThread.CurrentUICulture returns a value of en-US. Is this cultural imperialism?

9.5 Strong Named Satellites

If you poke around ResourceManager with Reflector you'll see that satellite assemblies are delay loaded: the first time that you request a resource InternalGetResourceSet is called and this calls InternalGetSatelliteAssembly on the main assembly. This final method generates the satellite assembly name in the following fashion:

  • the satellite PublicKeyToken is set to the main assembly's PublicKeyToken
  • if a specific version is not requested, the satellite's Version is set to the main assembly's Version
  • the Culture is set to the specified culture
  • the short name of the satellite is set to the concatenation of the main assembly's short name and ".resources"

The specified culture is the thread's CurrentUICulture. The version is specified in the main assembly using [SatelliteContractVersion].

This name is then passed to Assembly.Load, so it means that Fusion will be used to locate the assembly, either as a private assembly in an appropriate culture named subfolder, or from the GAC.

.NET Version 3.0
InternalGetResourceSet in version 3.0/2.0 of the framework library is implemented differently to the version in 1.1 of the framework library, and although its code is a bit convoluted it still calls InternalGetSatelliteAssembly on the Assembly class.

Now change the last example so that the application uses a library that uses resources. To do this create a file, lib.cs, that looks like this:

using System;
using System.Resources;
using System.Reflection;

public class LibCode
{
   public string GetLanguage()
   {
      Assembly a = Assembly.GetExecutingAssembly();
      ResourceManager resources = new ResourceManager("strings", a);
      return resources.GetString("Language");
   }
}

Change the application file so that it loads the LibCode object:

Console.WriteLine("initial UI culture is {0}", Thread.CurrentThread.CurrentUICulture);
if (args.Length > 0)
{
   Thread.CurrentThread.CurrentUICulture
      = new CultureInfo(args[0]);
   Console.WriteLine("changed UI culture to {0}",
      Thread.CurrentThread.CurrentUICulture);
}

LibCode lib = new LibCode();
Console.WriteLine(lib.GetLanguage());

Now build the library and the process:

csc /t:library lib.cs /res:strings.resources
csc app.cs /r:lib.dll

Note that it is the library that gets the neutral resource. Next you need to go to through the individual subfolders and recompile the satellite so that its short name refers to lib rather than app (because now lib is the base name). (I have created a batch file that will create all the satellite assemblies. To use this batch file create a new folder, copy the process and library source files and then run the batch file. Then compile the library and the process.)

Once the libraries have been created perform the tests given in the last example to confirm that the code works. (If you find that the tests always return the neutral resource then it means that the satellite assemblies do not have the right name, lib.resources.dll.)

Next, sign the library with key.snk by adding the following line:

[assembly:AssemblyKeyFile("key.snk")]

(Clearly, you need to have a key file called key.snk.) Recompile the library and the process. Now repeat the tests from the last example, what do you see? Each of the tests returns the neutral resource. The reason is that an assembly with a strong name can only load assemblies that also have strong names. Also, as I mentioned above, InternalGetSatelliteAssembly expects the satellite to have the same PublicKeyToken as the main assembly. Since you have not signed the satellites this will mean that the .NET name of the satellites are different to the assemblies being requested. So the attempt to load the assembly will fail and hence ResourceManager falls back to loading the neutral resource from the main assembly.

Now recompile each satellite and give it a strong name, for example:

al /embed:strings.en.resources /c:en /t:library
   /out:lib.resources.dll /keyf:..\key.snk

This picks up the key file from the parent folder. Now you can perform the tests again and confirm that the correct resources are loaded.

9.6 Satellite Versions

Of course, if an assembly has a strong name then it can also have a version. The assembly linker tool, al, has a switch /v that you can use to provide the version of the assembly. However, as the name suggests satellites are associated with a main assembly, this ties the name of the satellites to name of the main assembly. Going by this scheme, the version of the satellites should be the same as the version of the main assembly.

To test this out use the files from the previous example app.cs, lib.cs, key.snk, strings.txt) and add the following line to the library:

[assembly:AssemblyVersion("1.0.0.0")]

compile the library and process (at this point you can try the tests again and confirm that the satellites are not being loaded). Next, recompile each satellite and give it the same version, for example:

al /embed:strings.en.resources /c:en /t:library
   /out:lib.resources.dll /keyf:..\key.snk /v:1.0.0.0

Yet again, I have provided a batch file to do this. Create a new folder, copy the library, key file, process and neutral resource to this folder, then run the batch file. You can now build the library and the process.

Now the satellites will be loaded. Note that this is quite a long command line to type and the last two items are the same for each library, and are the same as the main assembly. You could type the wrong values for one satellite which could result in that satellite not being loaded. Because of this, the assembly linker tool provides a command line switch, /template, that you use to specify the name of the main assembly, and the version and key pair will be extracted from the main assembly. The version is easy to obtain from an assembly (it is part of the assembly name), and the public key is also easy to extract (sn -e will extract the public key and put it in a specified file), but how does the assembly linker tool get the private key to sign the satellite given just the main assembly? Well, when you supply the [AssemblyKeyFile] attribute to an assembly two things happen. First, the compiler will obtain the key file and uses this to sign the assembly. The second thing that happens is the compiler adds the custom attribute to the assembly in almost all cases this is a redundant action because this attribute is a message to the compiler and clearly after the assembly has been compiled it will not be used again. However, you can use reflection to read the [AssemblyKeyFile] attribute to get the key file and hence the private key. This is what al does.

al /embed:strings.en.resources /c:en /t:library
   /out:lib.resources.dll /template:..\lib.dll

If you use this for each satellite you are less likely to compile the satellites with the wrong values.

.NET Version 3.0
Clearly, if you use the command line to provide the name of the key file the location of this file will not be put in the assembly and hence /template will not work. In my opinion losing the facility of /template is well worth the trade off.

When you create the first version of your main assembly you are likely to create all your satellites. If you update the main assembly then you may add new resources, but then again you may not. If you do not change the resources it seems pointless to change the version of the satellites, requiring a re-compilation. Furthermore, a satellite can be put in the GAC and so if you change the satellites' version it means that you'll have to add the new satellites to the GAC. To get around this issue you can add the [SatelliteContractVersion] attribute to the main assembly to provide the version of the satellites. The ResourceManager will use this version in preference to the version of the main assembly.

9.7 Delay Signing

What about if the main assembly is delay signed? Give this a try. Extract the public key:

sn -p key.snk pkey.snk

Next change the library so that the library is delay signed:

[assembly:AssemblyVersion("1.0.0.0")]
[assembly:AssemblyKeyFile("pkey.snk")]
[assembly:AssemblyDelaySign(true)]

Compile the library and turn off validation for the library (sn -Vr lib.dll). You can now re-run the application without recompiling the satellites because the only change you have made is to remove the signed hash and turn off validation from the main assembly and so the PublicKeyToken (and hence the assembly name) will still be the same. However, it's an unlikely situation that the main assembly will be delay signed and the satellites will be signed. This is another situation where /template becomes useful because this switch will also pick up the fact that the main assembly is delay signed.

.NET Version 3.0
If you used the command line switches to pass the name of the key file then you cannot use /template, instead, use /delay+.

Try this out with just one satellite, for example en\lib.resources.dll (use the command line given above). Now run the app for that culture (app en) and you'll find that this works as expected. What's unusual about this? Well, the satellite should be delay signed and so that means that there should not be a signed hash so when the satellite is loaded no validation can be performed. The satellite has no code, but the hash will be performed on the resources and hence protect those from tampering. In fact, turning off validation for one assembly turns off validation for the assemblies it loads.

.NET Version 3.0
This feature has been disabled in .NET version 3.0/2.0. If you follow the instructions given above you'll get a MissingManifestResourceException. The type of this exception is confusing, but at least it gives a useful explanation: ...or that all the satellite assemblies required are loadable and fully signed. This indicates that the there is a strong name validation error. To solve this issue you have to register the satellite so that it will not be validated.

For completeness I should mention that al has a command line switch /delay+ that indicates that the assembly should be delay signed, and a satellite can be resigned with sn -R.

Undo the changes you have just made, that is remove delay signing from the main assembly and then re-compile lib.dll and the satellite that you had changed.

Finally it is worth pointing out that when an assembly has a strong name it means that it can be put in the GAC. If the main assembly (the one with the neutral resources) is in the GAC then the satellites should be in the GAC too. When Fusion attempts to load a satellite it will search the GAC first for the satellite.

9.8 Culture Specific Probing

Finally, it is worth pointing out that most assemblies should use satellites for culture specific resources. As a general rule if the assembly contains code then it should not contain culture specific resources, instead you should use satellites. The loose binding between an assembly and its satellites means that you can deploy culture specific resources at some stage after you have deployed the main assembly. Since the main assembly will have culture neutral resources it means that the code will still work, it just won't have access to culture specific resources.

Having said that, it is possible to give a library assembly a culture if you use the [AssemblyCulture] attribute.

You cannot give a process assembly a culture. If you try to do this the compiler will give you an error (CS0647 for C#, BC30129 for VB.NET, LNK1256 for Managed C++).

When you use this culture specific assembly in another assembly the full assembly name will be added to the calling assembly. This means that the culture specific assembly will be specifically loaded when the calling assembly needs a type in the assembly and so the culture specific assembly will be used regardless of the locale of the current thread. This defeats the whole purpose of having localised assemblies.

As an example, create a library with a source file called lib.cs:

using System;
using System.Reflection;

[assembly:AssemblyCulture("en-AU")]

public class LibraryCode
{
   public string GetName()
   {
      Assembly a = Assembly.GetExecutingAssembly();
      return a.GetName().ToString();
   }
}

I have used en-AU as the locale, you can use any locale that you want as long as it's not the same as the locale that your machine is running. Compile this library (csc /t:library lib.cs). Use this assembly in a process (app.cs):

using System;

class App
{
   static void Main()
   {
      LibraryCode lib = new LibraryCode();
      Console.WriteLine(lib.GetName());
   }
}

Now compile the process (csc app.cs /r:lib.dll). The compiler will give you a warning CS1607 complaining that the library is localised. However, if you run the process you'll get an error from Fusion, which cannot find the library. The reason is that if the library has a culture it must be in a subfolder with the locale name. Create the appropriate folder (in this example, md en-AU), and move the library there (move lib.dll en-AU). Now if you run the process the library will be located and loaded.

Next use the configuration tool to add a privatePath (see Example 2.1) to a folder called bin. This tells Fusion to search for private assemblies in the bin subfolder as well as the application's folder. Move the library to bin (move en-AU\lib.dll bin) and run the app. You'll find that Fusion still cannot find the library. The Fusion log gives some clues about the paths it searches:

en-AU/lib.DLL
en-AU/lib/lib.DLL
bin/en-AU/lib.DLL
bin/en-AU/lib/lib.DLL
en-AU/lib.EXE
en-AU/lib/lib.EXE
bin/en-AU/lib.EXE
bin/en-AU/lib/lib.EXE

That is, .dll and .exe is appended to the short name and the culture specific folder is checked, and then a folder under the culture specific folder with the short name of the assembly. Then these two folder beneath the bin folder are checked. Create a locale folder under bin (md bin\en-AU) and move the library there (move bin\lib.dll bin\en-AU). Finally, run the app to show that Fusion can now find the library.

I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com.

Errata

If you see an error on this page, please contact me and I will fix the problem.

Page Ten

This page is (c) 2007 Richard Grimes, all rights reserved