Loading Dependent Assemblies Manually
Asked Answered
A

2

5

I have a project that loads multiple versions of the same assembly using either Assembly.Load or Assembly.LoadFile. I then use Assembly.CreateInstance to create a type from that specific assembly.

This works great until the type I'm creating references another dependent assembly. I need a way to intercept this specific assembly's request to load another assembly and provide it with the correct version (or, even better, probing path) to look for its dependency.

This is required because v1 and v2 of the assemblies I'm creating with Assembly.CreateInstance will often need different versions of their dependent assemblies as well, but both v1 and v2 will, by default, probe the same directories.

I've seen examples of how to do generally for an AppDomain, but I need to do this in a way that handles all resolution from a particular root assembly. Assuming I do something like:

AppDomain.CurrentDomain.AssemblyResolve += delegate(object sender, ResolveEventArgs args)
{
    //Use args.RequestingAssembly to determine if this is v1 or v2 based on path or whatever
    //Load correct dependent assembly for args.RequestinAssembly
    Console.WriteLine(args.Name);
    return null;
};

This may work for dependencies immediately referenced by my target assembly, but what about the assemblies that those dependencies reference? If v1 references Depv1 which itself references DepDepv1, I'll need to be able to know this so I can ensure it can find them properly.

In that case, I supposed I would need to track this somehow. Perhaps by adding custom assembly evidence - although I haven't been able to get that to work, and there doesn't appear to be any "assembly meta data" property that I can add to at runtime.

It would be far, far easier if I could simply instruct a particular assembly to load all its dependencies from a particular directory.

Update

I managed to use the AssemblyResolve event to load the dependent assemblies based on the path of the RequestingAssembly, but it seems to be a flawed approach. It seems as though the which dependent assembly version while be used is entirely dependent on which version happens to be loaded first.

For instance:

  1. Load v1
  2. Load v2
  3. Reference v1 causes load of Depv1
  4. Reference v2 causes load of Depv2
  5. Code in v1 uses type from Depv1 (Works)
  6. Code in v2 uses type from Depv2 <-- fails because it gets type from Depv1!

I'm only inferring steps 5 and 6 at this point, but I do see Depv1 AND Depv2 being loaded.

Alpenstock answered 9/4, 2014 at 15:58 Comment(4)
Why can't you use the AppDomain event handler and just act on the assembly in question?Riddell
After posting this, I realized exactly what you just said. I'm still having issues, though. I'll document them in the question.Alpenstock
You are trying to re-implement the GAC. There's little point, just use it.Kulsrud
I'm not trying to reimplement the GAC. I'm actually writing an Entity Framework Code First Migration manager that can automatically handle choosing the correct migration assembly for both upward and downward migrations during continuous integration and deployment. The only way I could achieve this with the GAC would be to strong name a large subset of my assemblies, and I have no interest in that.Alpenstock
A
3

As it turns out, the key to making this work is to ensure you use Assembly.LoadFile. LoadFile is the only method that will load an assembly even if it matches an assembly that .NET thinks is already loaded. I discovered this from an article on codeproject.

Since I needed to load two different assemblies that both had identical full names (i.e. "App.Test.Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null") but had different contents, LoadFile was the only way to accomplish this. My initial attempts used the Load overload that accepted the type AssemblyName, but it would ignore the path defined in the AssemblyName instance and instead return the already loaded type.

To force an entire dependency graph to load from a specific location regardless of what other types are already loaded is to register for the AssemblyResolve event:

AppDomain.CurrentDomain.AssemblyResolve += ResolveDependentAssembly;

And ensure that we use LoadFile to load the dependency:

private Assembly ResolveDependentAssembly(object sender, ResolveEventArgs args)
{
    var requestingAssemblyLocation = args.RequestingAssembly.Location;

    if (thePathMatchesSomeRuleSoIKnowThisIsWhatIWantToIntercept)
    {
        var assemblyName = new AssemblyName(args.Name);
        string targetPath = Path.Combine(Path.GetDirectoryName(requestingAssemblyLocation), string.Format("{0}.dll", assemblyName.Name));
        assemblyName.CodeBase = targetPath; //This alone won't force the assembly to load from here!

        //We have to use LoadFile here, otherwise we won't load a differing
        //version, regardless of the codebase because only LoadFile
        //will actually load a *new* assembly if it's at a different path
        //See: http://msdn.microsoft.com/en-us/library/b61s44e8(v=vs.110).aspx
        return Assembly.LoadFile(assemblyName.CodeBase);
    }

    return null;
}

Yes, this code assumes that if your root assembly has dependencies, that they're all located at the same path. That's a limitation, no doubt, but you could fairly easily add additional hints for non-local dependencies. This also would only be an issue if the already loaded version of those additional dependencies wouldn't work.

Lastly, none of this would be necessary if the assembly versions were properly incremented. The Load call would not treat an already loaded Depv1 as the same as a request Depv2. In my case, that wasn't something I was willing to deal with as part of my continuous integration and deployment process.

Alpenstock answered 9/4, 2014 at 21:23 Comment(0)
B
3

Try Assembly.LoadFrom(path); which will resolve dependencies automatically.

Broadbent answered 16/7, 2015 at 10:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.