Andrew
Andrew

Reputation: 2335

C# Reflection - determine location of dependencies at runtime

On the project I'm currently working on we've undertaken a massive piece of work to style the forms and make them more consistent with the business rules. This has involved things such as padding of forms, changing labels, changing column headers etc - loads of stuff basically.

I've been asked to look into the feasibility of producing a tool to compare our original forms vs. the new ones to see what has been changed. I've gone down the route of using reflection for this - getting the original DLLs and loading the forms in turn and comparing the controls (I couldn't think of an easier way to do it, as a lot of our forms have other items "injected" at run time, so a comparison of the designer files wouldn't work.

In my project I've created two directories called "OriginalAssemblies" and "CurrentAssemblies" and within each of these there is a dependencies directory, which contains all the assemblies on which the forms are reliant. These files will, in the main be different between Original and Current.

My first method reads in the assemblies, based on a directory and stores then in a list - there is one for original and one for the current:

private static void LoadInAssemblies(string sourceDir, List<Assembly> assemblyList)
{
    DirectoryInfo dir = new DirectoryInfo(sourceDir);

    foreach (FileInfo currentFile in dir.GetFiles().Where(f => f.Name.Contains("dll")))
    {
        assemblyList.Add(Assembly.LoadFile(currentFile.FullName));
    }
}

This appears to work fine in that it loads the DLLs, which contains the forms. Next, I have a method which loads in the distinct forms and processes them. This runs, but it loads in the dependencies from the same directory each time

foreach (Assembly current in originalAssemblies)
{
    var items = current.GetTypes().Where(t => t.IsSubclassOf(typeof(Form)) && !t.Name.Contains("Base")).ToList();
    foreach (var newForm in items)
    {
        Object originalForm = Activator.CreateInstance(newForm);

        // get the associated assembly from the new form
        Object currentForm = Activator.CreateInstance (currentAssemblies.SelectMany(a => a.GetTypes()).Where (t => t.Name == newForm.Name).First());

        ((Form)originalForm).ShowDialog();
        ((Form)currentForm).ShowDialog();
    }
}

After a bit of research I found reference to probing in the app.config, so set it as follows:

<runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <probing privatePath="OriginalAssemblies/Dependencies"/>
    </assemblyBinding>
</runtime>

Now I realise that I can separate directories and have Current and Original directories, but in this case it won't help because the DLLs in the dependencies folder will be identical - just different assembly versions.

My question is, based on the code above is there any way I can tell the GetType() command when creating the object currentForm to use the correct directory, which in this case would be CurrentAssemblies?

Upvotes: 2

Views: 763

Answers (2)

Measurity
Measurity

Reputation: 1346

I've had a go with loading multiple versions of the same kind of dll. This is what I came up with:

You'll need to use the AssemblyResolve event on two AppDomain to handle the redirect of the referenced dll files in your project automatically (here my "current" dlls were in "bin" and the "original" were in "bin/original"):

private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    // Path for to "Current" dlls.
    string newPath = Path.Combine(Path.GetDirectoryName(Assembly.GetCallingAssembly().Location),
        "bin", new AssemblyName(args.Name).Name + ".dll");

    if (File.Exists(newPath))
        return Assembly.LoadFrom(newPath);
    return args.RequestingAssembly;
}

private static Assembly CurrentDomain_AssemblyResolve_Original(object sender, ResolveEventArgs args)
{
    // Path for the "Original" dlls.
    string newPath = Path.Combine(Path.GetDirectoryName(Assembly.GetCallingAssembly().Location),
        "bin", "original", new AssemblyName(args.Name).Name + ".dll");

    if (File.Exists(newPath))
        return Assembly.LoadFrom(newPath);
    return args.RequestingAssembly;
}

IMPORTANT Make sure to put the code below as soon as possible (in the very first constructor called) or the event subscriptions won't be called!!:

private static readonly AppDomain OriginalAppDomain;

// Here, "Program" is my first class constructor called on startup.
static Program()
{
    AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
    AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += CurrentDomain_AssemblyResolve;

    // Second app domain for old DLLs.
    OriginalAppDomain = AppDomain.CreateDomain("Original DLLs", new Evidence());
    OriginalAppDomain.AssemblyResolve += CurrentDomain_AssemblyResolve_Original;
    OriginalAppDomain.ReflectionOnlyAssemblyResolve += CurrentDomain_AssemblyResolve_Original;
}

Now you can test both dlls in one go like this:

// Runs on current AppDomain.
new SpecialForm().ShowDialog();

// Runs on OriginalAppDomain (created above).
OriginalAppDomain.DoCallBack(() => new SpecialForm().ShowDialog());

Possible improvements

Changing the AssemblyResolve methods with checks on version instead of directory would be possible as well. So you'll have more control on which Assembly will be loaded.

Upvotes: 2

Erdogan Kurtur
Erdogan Kurtur

Reputation: 3685

Your problem is that when an assembly loaded, it will stay in memory and subsequent calls to assemblies that require that assembly will automatically use it. This is why AppDomain.AssemblyResolve will not help you and cause more trouble.

My approach would be like this

Create a tool that creates your forms and dumps them to text files. Then this tool in two times, first in original, second in modified folder. Compare text files using any text compare tool you like, or compare yourself if you feel like it.

Upvotes: 2

Related Questions