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:
Next create a process assembly app.cs that has this code:
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:
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
Within the results you'll see this:
{
}
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:
.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:
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:
two=second string
This can be compiled with the resgen utility:
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.Collections;
Next, change the Main method to load the resources through the
ResourceReader and then access the items in the resources:
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:
{
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:
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:
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:
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
<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:
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.
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:
{
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:
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:
// 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:
Here's a partial output that I get:
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 |
Compile this code:
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:
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:
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-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:
Replace English with the appropriate language name. For each
file create the satellite assembly by compiling the resource and then generating
the assembly:
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:
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.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:
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
PublicKeyTokenis set to the main assembly'sPublicKeyToken - if a specific version is not requested, the satellite's
Versionis set to the main assembly'sVersion - the
Cultureis 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.0InternalGetResourceSet 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.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:
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 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:
(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:
/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:
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:
/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.
/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:
Next change the library so that the library is delay signed:
[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.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/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.