Where to handle AssemblyResolve event in a class l

2019-02-11 05:12发布

问题:

I need to dynamically resolve assembly references from one class library to another. The class libraries are being loaded from a PowerShell script, so the default .NET behaviour of looking for dependent assemblies in the executable directly fails, as the executable is PowerShell itself. How do I make these dependent assembly references resolve / work correctly?

In more detail:

I have two utility libraries: a core one and another one that does some very specific parsing tasks. I want to load them dynamically in a PowerShell script without installing them in the GAC. The second library depends on the first. In the VS solution, the parsing library has a project reference to the core library, with Copy Local = true.

I can load and use both libraries from the parsing library output bin (/Debug|/Release) folder after using (PowerShell here):

[Reflection.Assembly]::LoadFile("C:\...thefile.dll")

However, whenever calling a method in the parsing (dependent) library that calls something from the core library it fails to resolve the core assembly. This is...frustrating...since the files are in the same folder. It makes no difference whether one or both have strong name keys.

My workaround now is to handle the AssemblyResolve event. The tricky thing is figuring out where to put this in a class library, since there's no single entry point that will always execute before anything else like there is in an executable Main() method (see Is there an equivalent of Application_Start for a class library in c#).

For now I've made a static Resolver class with a static constructor that attaches a handler for AssemblyResolve, and then have a static constructor in each of the parsing classes which refers to the static resolver class, forcing the resolver class's static constructor to execute. The result is that the AssemblyResolve event gets attached exactly once and handled with common, central code. So it works. But I hate having to add a funky static constructor to all of my parsing classes.

Is there a better way to handle this?

回答1:

I figured out a solution that follows the "consumer should resolve" pattern, and which works for both PowerShell and normal .NET application consumers.

The idea:

  • Make a class with an internal AssemblyResolve event handler, and a static constructor that attaches the handler to the AppDomain.CurrentDomain.AssemblyResolve event. (So far this is familiar.)
  • Instead of invoking the Resolver class from the same or another class library, invoke it directly by the consumer. When PowerShell is the consumer, invoke the Resolver from PowerShell.
  • This works because any consumer--including PowerShell--has the same CurrentDomain as the assemblies it loads. So even if the event handler is attached in some dynamically loaded assembly, it will still be invoked when a assembly resolve fails in the main consuming application.

My version of Resolver has:

  • A static property AssemblyDirectory that can be used to optionally set the directory to search from. If left blank, it will use the directory found from Assembly.GetCallingAssembly().Location
  • A dummy Register() method which really does nothing except ensure the static constructor has been called.

PowerShell Code:

# First, load the assembly with the Resolver class. I actually put it in the same 
# core library, but it really doesn't matter where it is. It could even be by itself
# in a dynamically compiled assembly built using the Add-Type commandlet
[Reflection.Assembly]::LoadFile("[an assembly that has the Resolver class].dll")

# Call the Register method to force the static constructor to run if it hasn't already
[mycorp.mylib.Resolver]::Register()

$sourcedir = "C:\foo\bar\..some random dir"
# Set the path to search for unresolved assemblies
[mycorp.mylib.Resolver]::AssemblyDirectory = $sourcedir

# Load the dependent assembly
[Reflection.Assembly]::LoadFile("$sourcedir\myparser.dll")

# Create and use an object from the dependent assembly. When the core assembly
# fails at first to resolve, the Resolver's handler is automatically called, 
# and everything is peachy
$encoder = New-Object mycorp.mylib.anamespace.aclass
$result = $encoder.DoStuff( $x, $y, $z ... etc )

If you want to know how to actually handle the AssemblyResolve event, check out the documentation on MSDN: AppDomain.AssemblyResolve Event

Regarding Register-ObjectEvent:

At first I tried to build an assembly resolver directly in PowerShell, and register it using the Register-ObjectEvent commandlet. This would work, but for one problem: PowerShell doesn't support event handlers that return a value. AssemblyResolve handlers return an Assembly object. That's almost their whole purpose.



回答2:

You can register the event with the AppDomain (see here), but that has to be registered/handled in many, many places because for each instance you cannot guarantee that the registration has already taken place at any given entry point.

It's probably better practice to handle this in the application that is using the class library rather than in the library itself.