Chris's coding blog

Inside .NET Assemblies (part 2)

July 29, 2010

View part 1

Basic resource embedding

Embedding images, video, text files and so on into your .NET assembly is incredibly simple in Visual Studio, simply add the file to the project, press F4 and change its Build Action to “embedded resource”. It can then be referenced with one line:

public void Main()
{
// Basic text file reading
StreamReader reader = new StreamReader(Assembly.GetExecutingAssembly().GetManifestResourceStream("YourNameSpace.Resources.mydoc.txt"));
string contents = reader.ReadToEnd();
// RTF file
Stream stream = this.GetType().Assembly.GetManifestResourceStream( "YourNameSpace.Resources.mydoc.rtf" );
this.richTextBoxMain.LoadFile( stream, RichTextBoxStreamType.RichText );
// Image
Stream stream = this.GetType().Assembly.GetManifestResourceStream( "YourNameSpace.Resources.myimage.gif" );
Image image = Image.FromStream(stream);
}
// Audio and Video would require a 3rd party library or Windows Media Player to play
// This method takes a string in a path format, such as "/resources/mydoc.txt" and returns it as a string
// (the resource has to be a string)
public static string GetStringFromResource(string path)
{
if (string.IsNullOrEmpty(path))
return "";
path = path.Replace("/", ".");
path = "MyNameSpace" + path;
Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(path);
if (stream == null)
return "";
using (StreamReader reader = new StreamReader(stream))
{
string result = reader.ReadToEnd();
}
return result;
}
view raw gistfile1.cs hosted with ❤ by GitHub

The code above assumes you have a folder called Resources. The reference string for the path is the namespace of your project, then the folder names separated with a dot. Case sensitivity is important for this. You can get all resources in your assembly using the following:

foreach ( string name in GetType().Assembly.GetManifestResourceNames() )
{
   Console.WriteLine( name );
}

Visual Studio hides what it is doing when it embeds resources into the assembly and you probably won’t care most of the time. The action it performs isn’t hugely complicated, it simply appends the /resource switch to the C# compiler (csc) like so:

csc /target:exe /resource:image.gif Program.cs

You append a /resource switch for each resource:

csc /target:exe /resource:image.gif /resource:image2.gif Program.cs

Where are the resources stored in the assembly?

All of the resources are stored in either the #US or the #blob heaps which were described above. Each stream has a start and offset in its header which allows the CLR to retrieve each resource quickly. You can view assembly resources quickly by using .NET reflector or CFF explorer, the links are at the end of the article.

Localization, Satellite assemblies, .resource files and .resx files

For those special occasions where you want your application or class library to have resources that are specific to a culture, for example labels on a Windows or ASP.NET form that vary based on language, you will create a set of satellite assemblies that are loaded by the CLRdepending on the current culture.

Localization is made a lot simpler by Visual Studio as all the command line and folder creation is automated. To add a new culture to your project, use Add->New Item… Select the General item in the tree, and ‘Resource file’. You must then name the file in the format

<Namespace>.<Classname>.<Culture-code>

So it might be MyNamespace.Program.fr.resx (case sensitivity is important). The culture codes are available at MSDN online.

Once you have added this, you can add strings or other resources via the gui. It can also be done manually by editing the .resx file if you prefer, the schema is fairly simple (it looks long as the XSD is embedded in the top of each file). You will need to store any binary as base64 in the format.

Compiling your project will produce the following in the debug/bin folder:

YourProject.dll
fr/YourProject.dll

What Visual Studio actually does to turn the MyNamespace.Program.fr.resx file in your project to the output folder is:

resgen MyNamespace.Program.fr.resx
csc /t:library /out:MyNamespace.dll /resource:MyNamespace.Program.fr.resources
md fr
xcopy MyNamespace.dll fr
del MyNamespace.dll
csc /t:exe /out:MyNamespace.exe Program.cs

(if your directory has a long path, you might want to type “prompt $N:$G” to reduce it down).

The resx file - which is just an XML file with your resources in - is transformed into the .resource file format that the C# compiler (or Al.exe) recognises. It’s then turned into an assembly and copied to the fr directory. More information on .resource files and resgen.exe can be found at MSDN online.

The CLR searches using the culture names for folders, starting at the assembly’s base directory, then the culture name. The same applies if the machine that is running the application is different from the base culture of the main assembly. Satellite assemblies are never intended to contain code, just host resources for the culture they represent. You can load a culture manually and use its resource string using the following (assuming you have a string called Label1):

CultureInfo ci = new CultureInfo( "Fr" );
ResourceManager resmgr = new ResourceManager(typeof(Program) );
string x = resmgr.GetString( "Label1" );

The main assembly that contains your actual code is known as the fallback assembly. If any resource can’t be found for the culture being used then it will be taken from this main ‘fallback’ assembly. You can also change the fallback assembly to represent another culture using the [assembly: AssemblyCulture( “” )] attribute, though this isn’t recommended.

Changing referenced assembly versions

When a C# source file is compiled into an assembly, information is embedded into the metadata header of the assembly with the version and key information of each of the referenced assemblies in the modules. There are situations where you will need to change your own or another assembly so it uses a different version of a referenced assembly. This can be performed inside the app.config/web.config file using the following format:

<configuration>
	<runtime>
		<assemblyBinding xmlns="urn:schemas-microsoft-.com:asm.v1">
			<dependentAssembly>
				<assemblyIdentity name="AnotherAssembly" publicKeyToken="xxx" culture="neutral" />
				<bindingRedirect oldVersion="1.5.0.0" newVersion="2.0.0.0" />
			</dependentAssembly>
		</assemblyBinding>
	</runtime>
</configuration>

You can also specify a range of assembly versions using the format 1.5.0.0-1.7.0.0 for the oldversion attribute. Full MSDN information is available here.

Adding search paths for the CLR to look in

By default the CLR will look in the application’s base directory for assemblies that it references. Normally all the referenced assemblies are thrown into the same directory as the output assembly, however you can alter your app.config or web.config file so that the CLR looks in other folders. These folders always have to be below the assembly’s base directory. The format is:

<configuration>
	<runtime>
		<assemblyBinding xmlns="urn:schemas-microsoft-.com:asm.v1">
			<probing privatePath="lib;moreFiles" />
		</assemblyBinding>
	</runtime>
</configuration>

The above will make the CLR search inside the “lib” and “moreFiles” folders which should live underneath the assembly’s base directory (for example debug/bin/lib and debug/bin/moreFiles).

Links

Bibliography

The following article and books helped a lot with this article, I’d definitely recommend buying the books if you are interested in the insides of .NET and the CLR.

.net

I'm Chris Small, a software engineer working in London. This is my tech blog. Find out more about me via GithubStackoverflowResume