Skip to content

Latest commit

 

History

History

Resolving-Event-with-ALC

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

Resolving assembly conflicts

The article Resolving PowerShell module assembly dependency conflicts contains great content about what this problem is, why it happens, and different ways for a module author to mitigate the issue. The most robust solution described in the article leverages the AssemblyLoadContext to handle the loading requests of all a module's dependencies, which makes sure the module gets the exact version of the dependency assemblies that it requests for.

This technique presents a clean solution for a module to avoid dependency conflicts. It is used by the Bicep PowerShell module, and is also documented with a great example in Emanuel Palm's blog post: Resolving PowerShell Module Conflicts.

However, this technique requires the module assembly to not directly reference the dependency assemblies, but instead, to reference a wrapper assembly which then references the dependency assemblies. The wrapper assembly acts like a bridge, forwarding the calls from the module assembly to the dependency assemblies. This makes it usually a non-trivial amount of work to apply this technique --

  • For a new module, this would add additional complexity to the design and implementation;
  • For an existing module, this would require significant refactoring.

Here I want to introduce a simplified solution to mitigate the problem, which comes with two limitations comparing to the above solution but requires way less effort from the module author.

AssemblyLoadContext.Default.Resolving + AssemblyLoadContext

The use of the assembly resolving event is quite common for redirecting loading requests. You can register an assembly resolving handler for the exact versions of your dependency assemblies, and then leverage AssemblyLoadContext in the handler to deal with the loading. With this, there is no need to have a wrapper assembly, and the handler is guaranteed to return the same assembly instance for all the loading requests it receives for the same assembly.

NOTE: Do not use Assembly.LoadFrom in the event handler.
That API always loads an assembly file to the default AssemblyLoadContext, which is actually the source of this assembly-conflict problem.

NOTE: Do not use Assembly.LoadFile for the dependency isolation purpose.
This API does load an assembly to a separate AssemblyLoadContext instance, but assemblies loaded by this API are discoverable by PowerShell's type resolution code (see code here). So, your module could run into the "Type Identity" issue when loading an assembly by Assembly.LoadFile while another module loads a different version of the same assembly into the default AssemblyLoadContext.

To leverage AssemblyLoadContext, you need to create a custom AssemblyLoadContext class and directly use it to load assembly files.

We have the module SampleModule to demonstrate this solution. The whole sample is organized as follows:

  • shared-dependency: it's a project to produce different versions of NuGet packages for SharedDependency.dll. Three such packages of the versions 0.7.0, 1.0.0, and 1.5.0 are available under the folder nuget-packages.
  • SampleModule: it produces the SampleModule that uses "Resolving event + custom AssemblyLoadContext" to handle the conflicting SharedDependency.dll. See its README for details on the module structure and how it works.
  • ConflictWithHigherDeps: it's a module that depends on a higher version of SharedDependency.dll
  • ConflictWithLowerDeps: it's a module that depends on a lower version of SharedDependency.dll
  • scenario-demos: it contains the demos for five scenarios that SampleModule can run into with the modules ConflictWithHigherDeps and ConflictWithLowerDeps.

To build and generate all the 3 modules needed for the demos, run the .\build.ps1 within this folder.

The generated modules will be placed in .\bin. Please make sure .NET SDK 6 is installed and available in PATH before building. The version of the SDK should be 6.0.100 or newer.

Once the 3 modules are generated under .\bin, go ahead to scenario-demos to review the behaviors of SampleModule for those five scenarios.

Limitations

Comparing to technique adopted by the Bicep module, there are 2 limitations with this solution:

  1. If a higher version of the dependency is already loaded in the default AssemblyLoadContext, that version will be used by your module, and the resolving handler will never be triggered.
  2. If another module uses the same technique to handle the same version of the same dependency, and it's loaded before your module, then your module's request for that dependency will be served by that module's resolving handler. This's OK as long as that module is still loaded, but could potentially be a problem if that module is removed and unregistered the resolving handler that served your previous loading request. This is because if your module happens to have a new request for the same dependency after that point, the new request might then be served by your module's resolving handler with a new assembly instance, which could cause the type identity issue.

Please make sure you evaluate the limitations before going forward with this solution:

  • For the 1st limitation, it may be acceptable to depend on a higher version dependency assembly at run time for some modules. For those modules, this solution could be a good fit.
  • For the 2nd limitation, it would be rare to happen in practice, given that most workflows don't involve removing a loaded module.