Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#

Full Name Tutorial Part 2

Rate me:
Please Sign up or sign in to vote.
4.43/5 (3 votes)
7 Sep 2009CPOL5 min read 22.1K   68   15   1
How to extract the full name, including version, public token key from an assembly and then unload the assembly!

Introduction

This is part two of the Full Name extraction tutorial (Full Name Tutorial Part 1). This part will put together the bits of information from part one into a simple utility. This utility will then be extended to show how you can load and more importantly unload an assembly in a separate application domain.

Assembly Viewer

This utility can be added to the external tools list within Visual Studio so it can be available whenever required.

Screen shot of the assembly viewer

I will be using a factory pattern for the creation of the assembly information object for no other reason than it will make my life easier when we get to the loading/unloading from a different application domain.

I am making use of the auto property feature in .NET 3.5, and this is the only feature I am using, so if you have a previous version, then you will need to either remove {get;set;} or add in the internal members.

C#
public class AssemblyDetails
{
    #region Properties

    public string FullName { get; set; }

    public string AssemblyName { get; set; }

    public string Location { get; set; }

    public string FileVersion { get; set; }

    public string AssemblyVersion { get; set; }

    public string CultureName { get; set; }

    public string PublicKeyToken { get; set; }

    #endregion
}

This factory has a static method (AssemblyDetailFactory.ExtractInformation) that takes in a single argument, the location and file name of the assembly. This method, once validated that the argument is an assembly, then returns the AssemblyDetails to be displayed in the form.

Screen shot of the assembly viewer with data

Once the AssemblyDetails has been returned from the factory, the utility will place the contents of FullName into the clipboard.

C#
AssemblyDetails _details = AssemblyDetailFactory.ExtractInformation(fileName);
Clipboard.SetDataObject(_details.FullName, true);

Application Domain

This utility will work happy if you remember to close it when done, but if you forget and then try to recompile the project you will find that an error will appear! This is to do with the fact that the utility has loaded the assembly into memory and still has a handle to it. Therefore no other process can update the assembly. This is a bit of a drawback so I am going to show you how to load and unload the assembly when done with it.

Once an assembly is loaded you cannot unload it, unless you unload the application domain that it is hosted in. Normally there is only one application domain which all assemblies are loaded in. This the one that the .NET Framework creates for you when an application is started. To gain access to it, just use AppDomain.CurrentDomain static method.

You can create an additional application domain whenever you like. These additional application domains can be treated like sandboxes and have dangerous code running in them. For our purposes, we will require communication between both application domains. This is done with the use of serialization.

The first thing we will need to do is make the AssemblyDetails object serializable. This is done by adding the SerializableAttribute to the class.

C#
[Serializable]
public class AssemblyDetails
{
    .....
}

To do the actual work of processing the assembly, we need a class that can be loaded into the new application domain. This class has to inherit from System.MarshalByRefObject so that cross domain communication can happen.

For this, I have called the class AssemblyTester:

C#
public class AssemblyTester
    : MarshalByRefObject
{
}

As we are not going to call any methods within the assembly, to make the testing a bit more secure we will load the meta data only. There is a handy method called Assembly.ReflectionOnlyLoad which will do this, and we use it in the LoadAssembly:

C#
public void LoadAssembly(string path)
{
    try
    {
        FileName = Path.GetFileNameWithoutExtension(path); ;
        Assembly ass = Assembly.ReflectionOnlyLoad(FileName);
        Information = AssemblyDetailFactory.GetDetails(ass);
    }
    catch (FileLoadException ex)
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendFormat(CultureInfo.CurrentCulture, 
		"Problem loading file ({0}) {1}{1}{2}", FileName, 
		Environment.NewLine, ex.Message);
        if (string.IsNullOrEmpty(ex.FusionLog) == false)
        {
            sb.AppendFormat(CultureInfo.CurrentCulture, "{0}{0}{1}", 
				Environment.NewLine, ex.FusionLog);
        }
        LastError = sb.ToString();
    }
    catch (BadImageFormatException ex)
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendFormat(CultureInfo.CurrentCulture, 
		"Problem loading file ({0}) {1}{1}{2}", FileName, 
		Environment.NewLine, ex.Message);
        if (string.IsNullOrEmpty(ex.FusionLog) == false)
        {
            sb.AppendFormat(CultureInfo.CurrentCulture, "{0}{0}{1}", 
				Environment.NewLine, ex.FusionLog);
        }
        LastError = sb.ToString();
    }
}

As you can see, we catch two exceptions that could cause us an issue :

  • FileLoadException - Assembly was found but could not be loaded
  • BadImageFormatException - File is not an assembly or assembly was complied with a newer version of the framework!

There is an issue with loading an assembly for viewing of metadata only you cannot retrieve any of the assembly custom attributes directly. This is a bit of a pain as the only one we really want to retrieve is the AssemblyFileVersionAttribute. To retrieve the custom attribute data, we need to iterate though all the custom attributes to find the correct one and then work out what the constructor argument was in the assembly. There is a static class called CustomAttributeData that allows us to retrieve all the custom attributes and the data they where constructed with.

The follow bit of code will be added to the LoadAssembly method after Information = AssemblyDetailFactory.GetDetails(ass); line.

C#
foreach (CustomAttributeData data in CustomAttributeData.GetCustomAttributes(ass))
{
    if(data.ToString().Contains("AssemblyFileVersionAttribute"))
    {
        Information.FileVersion = data.ConstructorArguments[0].Value.ToString(); 
    }
}

So that information can be used to return to our calling domain, we will need some properties to be serialised.

C#
public AssemblyDetails Information { get; private set; }
public string FileName { get; private set; }
public string LastError { get; private set; }

Let's add a new method to the AssemblyDetailFactory class called ExtractInformationSafely. This method will handle the creation of the domain and the retrieval of the AssemblyDetails instance.

I am using the CreateInstanceFromAndUnwrap method as it just combines two other methods in one (see MSDN documentation).

The process of using AssemblyTester is as follows:

  1. Create the domain to run the AssemblyTester in.
  2. Create an instance of AssemblyTester in the domain.
  3. Use the AssemblyTester.LoadAssembly on the instance.
  4. Unload the domain.

Create the Domain

By default, any new domain will have limited configuration when created, so it is always advisable to use the AppDomainSetup class when creating the new domain.

By adding the path to the assembly we wish to load, the new domain will be able to load any dependency assemblies as well. Give the new domain a name and pass in the current domain security evidence as well.

C#
private static AppDomain CreateDomain(string path)
{
    AppDomainSetup adSetup = new AppDomainSetup();
    adSetup.ApplicationBase = Path.GetDirectoryName(path);
    adSetup.PrivateBinPath = Path.GetDirectoryName(path); 
    adSetup.ShadowCopyFiles = "true";

    if ( string.IsNullOrEmpty(adSetup.ApplicationBase))
    {
        adSetup.ApplicationBase = Path.GetDirectoryName(".\\");
    }
    AppDomain domain = AppDomain.CreateDomain
	("AssemblyDetailsTest", AppDomain.CurrentDomain.Evidence, adSetup);
    return domain;
}

Create the Instance

From the new domain, we can create the AssemblyTester instance using the CreateInstanceFromAndUnwrap method.

C#
AssemblyTester tester =  domain.CreateInstanceFromAndUnwrap
			("AssemblyViewer.exe",
                     	typeof(AssemblyTester).FullName)  as AssemblyTester;

Use LoadAssembly Method

Use the instance to load the assembly into the new domain and return the AssemblyDetails instance.

C#
tester.LoadAssembly(path);
if (tester.Information != null)
{
    details = tester.Information;
}

Unload the Domain

Once the details have been returned, we don't need to retain any handles to the assembly being viewed. So just unload the domain.

C#
AppDomain.Unload(domain);

Here is the full ExtractInformationSafely method:

C#
public static AssemblyDetails ExtractInformationSafely(string path)
{
    AssemblyDetails details = null;
    LastError = string.Empty;
    if (File.Exists(path))
    {
        AppDomain domain = CreateDomain(path);

        AssemblyTester tester =  domain.CreateInstanceFromAndUnwrap
				("AssemblyViewer.exe",
                                     typeof(AssemblyTester).FullName)  as AssemblyTester;
        if (tester != null)
        {
            tester.LoadAssembly(path);
            if (tester.Information != null)
            {
                details =tester.Information;
            }
        }
        AppDomain.Unload(domain);
    }
    return details;
}

The utility will now load the information and not retain any handle to the assembly it is viewing.

There were a couple of problems that appeared when writing the code. One of them was that you could not use the custom attributes directly. Another was that I had to hard code the assembly name ("AssemblyViewer.exe") in CreateInstanceFromAndUnwrap and not use typeof(AssemblyTester).Assembly.GetName().Name property. This is a bit of a pain as I will have to update the assembly name if it changes.

History

  • 7th September, 2009 - Initial release

License

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


Written By
Database Developer Workshare
United States United States
I have been in computer industry since 1984 and have seen a lot changes during this time. I have a very board experience of client and server technologies, so basically a 'jack of all trades, master of none' Smile | :)

Comments and Discussions

 
Generalvery cool Pin
Donsw3-Dec-09 14:41
Donsw3-Dec-09 14:41 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.