Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ResolveEventHandler after Assembly.LoadFrom

I am loading an Assembly using Assembly.LoadFrom() as the assemblies are located in a different path from Application Base directory.

Dim oAssembly As Assembly = _
Assembly.LoadFrom("C:\\MyFolder\\" + ddlXlate.SelectedItem.ToString() + ".dll")

And I consume a Type from that assembly without any problem:

oXML = CType(oAssembly.CreateInstance(sBaseType + ".XlateContainer"), _
XlateBase.XlateContainer)

However, the problem occurs when I try to use a Type from this assembly from within another method like the one below:

oComboBox.DataSource = _
[Enum].GetValues(Type.GetType(sType + "+ItemEnum," + sAssemblyName))

sAssemblyName is the one I loaded using LoadFrom() actually. After it said it cannot find the assembly, I used AssemblyResolve event which solved my problem :

Subscribing AssemblyResolve event :

AddHandler AppDomain.CurrentDomain.AssemblyResolve, _
AddressOf MyResolveEventHandler

Event Handler Method:

Private Shared Function MyResolveEventHandler(ByVal sender As Object, _
    ByVal args As ResolveEventArgs) As Assembly
    Return Assembly.LoadFrom("C:\\PSIOBJ\\" + args.Name + ".dll")
End Function

And I thought maybe the error occurs because it cannot find a dependent assembly defined in assembly manifest file I loaded using LoadFrom() already but when I checked the args.Name, I saw it was trying to load same assembly and after that it worked without any problem. So basically a type in the loaded assembly cannot be found before the event adding change.

My old code was using AppDomain.CurrentDomain.Load() and Assembly.Load() methods and they were working fine without the AssemblyResolve event. I was able to reach types in dynamically loaded Assembly from every where within the same AppDomain.

LoadFrom() can find dependencies automatically within the same requested assembly path and that couldn't be problem as everything this dll needs was there. So at first it looked like a AppDomain problem to me as it looks like it seems it can reach assemblies from Load context instead of LoadFrom context and I am now using LoadFrom context.

  1. But now it seems I should pass oAssembly instance evertwhere to use any type from the loaded assembly?
  2. Doesn't it load the assembly where I can reach it everywhere (same AppDomain) using simple Type.GetType(...) method?

Can some one please fill the missed points and answer my questions?

You can use C#, in fact I don't like VB.NET but I have to use it here in Office.

like image 451
Tarik Avatar asked Jan 18 '23 00:01

Tarik


1 Answers

If I understand your question correctly, you are trying to do something along those lines:

var asm = Assembly.LoadFrom(@"D:\Projects\_Libraries\FluentNH 1.1\Castle.Core.dll");
var obj = asm.CreateInstance("Castle.Core.GraphNode");
var type = Type.GetType(obj.GetType().AssemblyQualifiedName, true);  // fails

The problem that you encounter is, that whatever form of assembly loading you use, when the library is not in the same path as your executable, the variable type will always be null.

Cause

What you encounter is the problem of different loading contexts for assemblies in .NET. There are generally three, actually four types of loading contexts, in short:

  • The default load context. This is used for all assemblies in the GAC, the current executing assembly, the assemblies in the current path (see BaseDirectory) and those in the PrivatePath (see RelativeSearchPath). Assembly.Load(string,..) uses this context.
  • The load-from context. This is the context used for any assembly on disk not in the probing path, usually loaded with Assembly.LoadFrom.
  • The reflection-only context. Types in this context cannot be executed.
  • The no-context context. When you load an assembly using any of Assembly.Load(byte\[\],..) and Assembly.LoadFile methods, or when you load a dynamic assembly not saved to disk, this context is used.

Types loaded in one context, are not compatible with another context (you cannot even cast equal types from one context to another!). Methods specifically operating on one context, cannot access another context. Type.GetType(string) can only load types in the default context, unless you help the method a little.

This is exactly what you encountered. When the assembly dll was in the path of your application, everything worked fine. As soon as you moved it, things started to fall apart.

More specifically:
When you call Type.GetType(string), it will query all statically referenced assemblies in the path and dynamically loaded assemblies in the current path (AppDomain.BaseDirectory), the GAC and in AppDomain.RelativeSearchPath and. Unfortunately, the relative search path must be relative to the base directory.

Result:
The result of this behavior is that GetType does not simply check all loaded assemblies. Instead, it works the other way around, it does:

  1. GetType uses the assembly part of the first parameter to locate the assembly using Assembly.Load
  2. If found, reflect the type from that assembly.
  3. If not found, returns null without trying anything else (or throws FileNotFoundException, which can be rather confusing).

You can test this for yourself: Assembly.Load will not work when you just supply the assembly name to it.

Solutions

There are several solutions. One you already named yourself and that's keeping the assembly object around. There are some more, each with their own drawbacks:

  1. Use GetType() on the instantiated object itself, instead of the static method Type.GetType(string). This has the advantage that you don't need the assembly qualified name of the type, which can be hard to get (in your example, you don't say how you set sAssemblyName, but isn't that also something you need floating around?).

  2. Use a generic resolver that checks the loaded assemblies and returns the loaded assembly. You don't need to call LoadFrom again. I tested the following and that works splendidly and quite fast:

    // works for any loaded assembly, regardless of the path
    private static Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args)
    {
        // you may not want to use First() here, consider FirstOrDefault() as well
        var asm = (from a in AppDomain.CurrentDomain.GetAssemblies()
                  where a.GetName().FullName == args.Name
                  select a).First();
        return asm;
    }
    
    // set it as follows somewhere in the beginning of your program:
    AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainOnAssemblyResolve;
    
  3. Use the AppDomain.CurrentDomain.AssemblyLoad and .AssemblyResolve events together. The first you use to memorize each loaded assembly in a dictionary cache (by full name), the second you use to probe that dictionary by getting the value from it by name. This is relatively trivial to implement and might perform slightly better than the previous solution.

  4. Use the AppDomain.CurrentDomain.TypeResolve event handler. I haven't tried this, so I'm not certain it'll work in your scenario.: this doesn't work. GetType first tries to load the assembly, when that fails, it doesn't try to resolve the type and this event never fires.

  5. Add the libraries you want to resolve to the GAC or to any (relative) path of your application. This is by far the easiest solution.

  6. Add the paths to app.config. This only works for strongly typed assemblies, in which case you could just as easily load them in the GAC. Not-strongly-typed assemblies must still be in a relative path to the current application.

Conclusion

The static method group Type.GetType(..) behaves rather unintuitively when it comes to loaded assemblies at first look. Once you understand the ideas behind the several contexts, try to place the assembly in the default context. When that is not possible, you can create an AssemblyResolve event handler, which isn't that hard to make generically applicable.

like image 111
Abel Avatar answered Jan 26 '23 01:01

Abel