Resolving PowerShell Module Conflicts


Writing PowerShell sometimes means developing modules. If you’re writing a PowerShell module, chances are that you’re far enough into the PowerShell world that you’re using modules that other people authored as well. That’s great! There’s no need to re-invent any wheels when we can utilize PSGallery, the residence of many great modules.

Sometimes, however, you arrive at a situation where you’re using two different modules that both have a dependency on different versions of the same assembly, the same DLL file. You could imagine two modules using the same SDK to integrate with Azure services for example, or a module using the Json.NET framework from Newtonsoft, but a different version than the one that PowerShell uses internally. When PowerShell loads an assembly into the context of your session, it does so into a shared context between all loaded modules, including standard modules and even assemblies that PowerShell itself uses.

This means that PowerShell really doesn’t know how to handle situations where the user is importing several modules with different versions of the same dependency. Sometimes it works, sometimes it doesn’t, all depending on versions of PowerShell, .NET, import order and if it rained in the past week. At least that’s what it feels like, so let’s figure out what a dependency assembly conflict is and how we can build our own assembly load context in C# to bring some clarity to the issue!

If you’re just here for the source code as an example, check out the resulting project on my GitHub.

I also gave a talk about this specific topic at PowerShell Conference Europe 2023, but with a slightly different angle than the pattern I cover in this blog post. I’d encourage you to check that out too since it might simplify the implementation for you!

Creating a Conflict

To start from the beginning, let’s expand upon the idea of a module that integrates with Azure using an SDK provided by Microsoft, and use it to construct a scenario where a dependency conflict would occur. There’s already a PowerShell module called Az that does just that, which we can use to our advantage to demonstrate a dependency conflict. As an example scenario we’ll create a small PowerShell module using the SDK for Azure Blob Storage, but a newer version of the SDK than the one that Az.Storage uses.

If you want to follow along, you will want to have PowerShell and the .NET SDK installed. I’m using PowerShell 7.1 and .NET 5 for the examples today. You may find issues trying to follow along with older versions, especially of .NET as the logic for loading assemblies has changed between .NET Framework and .NET (previously .NET Core).

Finding Loaded Assemblies

By installing and importing the module Az.Storage we can see any assemblies that it depends on.

PipeHow:\Blog> Install-Module Az.Storage
PipeHow:\Blog> Import-Module Az.Storage
PipeHow:\Blog> $LoadedAssemblies = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object Location -like '*Az.Storage*'
PipeHow:\Blog> $LoadedAssemblies | Select-Object Location

Location
--------
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Microsoft.Azure.Management.Storage.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Microsoft.Azure.Storage.Common.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Microsoft.Azure.Storage.Blob.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Microsoft.Azure.Storage.File.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Microsoft.Azure.Storage.Queue.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Microsoft.Azure.Cosmos.Table.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Microsoft.Azure.DocumentDB.Core.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Microsoft.Azure.Storage.DataMovement.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Microsoft.OData.Core.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Microsoft.OData.Edm.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Microsoft.Spatial.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Microsoft.Azure.KeyVault.Core.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Azure.Storage.Blobs.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Azure.Storage.Common.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Azure.Storage.Files.DataLake.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Azure.Storage.Queues.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Azure.Storage.Files.Shares.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Microsoft.Azure.PowerShell.Cmdlets.Storage.Management.dll
C:\Program Files\PowerShell\Modules\az.storage\3.11.0\Microsoft.Azure.PowerShell.Cmdlets.Storage.dll

As of writing this, the newest version of Az.Storage is 3.11.0. Looking through the assemblies loaded by the module we can see that it uses one called Azure.Storage.Blobs.dll. Let’s have a look at that the full name of that one.

PipeHow:\Blog> $LoadedAssemblies | Where-Object Location -like '*Azure.Storage.Blobs.dll' | Select-Object -ExpandProperty FullName

Azure.Storage.Blobs, Version=12.8.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8

We can see that the module uses the version 12.8.0.0 of the assembly. All we need to do now is create a module which uses a different version of it. The newest version as of writing is 12.10.0.0.

Creating a Binary Module

I’ve previously written about creating script modules directly in PowerShell, but there is also a way to create modules using compiled .NET code. Compiling a module written in C# (or even F#) creates a type of module called binary module which has the advantages of often being faster.

There are a variety of reasons why you may decide to write a binary module instead of a script module, but as a person who loves C# I enjoy the appeal of being able to utilize existing API libraries or SDKs for services that are not always native to PowerShell.

Running the code below will create a .NET 5 class library project called AzBlobConflict which will serve as our PowerShell module. I won’t go into detail in this post about how to go about writing a proper binary PowerShell module, that’s a future post, but you’re welcome to follow along!

I’m adding the newest version of the System.Management.Automation package to allow for the class library project to be imported as a PowerShell module in the newest versions of PowerShell. As of writing, the newest version of PowerShell is 7.1.5, the same as the package being added.

If you’re curious, you can read up on the alternatives for version compatibility and more, but please note that assembly loading works differently depending on the .NET version and and simply changing the version of the PowerShell module may not be enough to follow along if you’re planning to solve a conflict for a module available for older platforms.

dotnet new classlib -f 'net5.0' -n AzBlobConflict -o C:\Temp\AzBlobConflict
Set-Location 'C:\Temp\AzBlobConflict'
dotnet add package 'Azure.Storage.Blobs'
dotnet add package 'System.Management.Automation'
# The project structure with package references
πŸ“‚AzBlobConflict
 ┣ πŸ“„AzBlobConflict.csproj
 ┃ ┣ πŸ“¦Azure.Storage.Blobs
 ┃ β”— πŸ“¦System.Management.Automation
 β”— Class1.cs

We can open our new project in something like Visual Studio or Visual Studio Code and change the code in Class1.cs (renaming is optional but generally recommended).

We will change it into a simple cmdlet class that utilizes the SDK (in a minimum effort way) by outputting the enum value of SkuName.StandardLrs defined in the Azure.Storage.Blobs.Models namespace.

        
// GetBlobStandardLrsSku.cs (renamed from Class1.cs)
using Azure.Storage.Blobs.Models;
using System.Management.Automation;

namespace AzBlobConflict
{
    [Cmdlet(VerbsCommon.Get, "BlobStandardLrsSku")]
    public class GetBlobStandardLrsSku : PSCmdlet
    {
        protected override void ProcessRecord()
        {
            WriteObject(SkuName.StandardLrs);
        }
    }
}

That’s all we need to build the example project into a binary module as a DLL file, with one command to use our dependency SDK.

# Publish with Release configuration, not Debug
dotnet publish -c Release

Invoking the Conflict

Now that we have our module ready, let’s ensure that it works. We will find it in the relative path .\bin\Release\net5.0\publish from the root of our project.

PipeHow:\Blog> Import-Module C:\Temp\AzBlobConflict\bin\Release\net5.0\publish\AzBlobConflict.dll
PipeHow:\Blog> Get-BlobStandardLrsSku

StandardLrs

The output of our command is just a string with the name of the StandardLrs SKU, but what’s important for our example is that we’re using the SDK to output that string.

Let’s also make sure that the Az.Storage module works as expected in a new PowerShell session before trying them together. Which command to use is less relevant, we’re just making sure that it loads and gives us output.

PipeHow:\Blog> Import-Module Az.Storage
PipeHow:\Blog> New-AzStorageContext -StorageAccountName 'PipeHow'

StorageAccountName  : PipeHow
BlobEndPoint        : https://pipehow.blob.core.windows.net/
TableEndPoint       : https://pipehow.table.core.windows.net/
QueueEndPoint       : https://pipehow.queue.core.windows.net/
FileEndPoint        : https://pipehow.file.core.windows.net/
Context             : Microsoft.WindowsAzure.Commands.Storage.AzureStorageContext
Name                :
StorageAccount      : BlobEndpoint=https://pipehow.blob.core.windows.net/;QueueEndpoint=https://pipehow.queue.core.windows.net/;TableEndpoint=https://pipehow.table.core.windows.net/;FileEndpoint=https://pipehow.file.core.windows.net/;
TableStorageAccount : BlobEndpoint=https://pipehow.blob.core.windows.net/;QueueEndpoint=https://pipehow.queue.core.windows.net/;TableEndpoint=https://pipehow.table.core.windows.net/;FileEndpoint=https://pipehow.file.core.windows.net/
Track2OauthToken    : Microsoft.WindowsAzure.Commands.Storage.Common.AzureSessionCredential
EndPointSuffix      : core.windows.net/
ConnectionString    : BlobEndpoint=https://pipehow.blob.core.windows.net/;QueueEndpoint=https://pipehow.queue.core.windows.net/;TableEndpoint=https://pipehow.table.core.windows.net/;FileEndpoint=https://pipehow.file.core.windows.net/;
ExtendedProperties  : {}

Both modules are working well. All we need to do to be a victim of the dependency conflict now is to import both modules in the same PowerShell session and try to execute our command.

Import-Module Az.Storage
Import-Module C:\Temp\AzBlobConflict\bin\Release\net5.0\publish\AzBlobConflict.dll
Get-BlobStandardLrsSku
        
Get-BlobStandardLrsSku: Could not load file or assembly 'Azure.Storage.Blobs, Version=12.10.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8'. Could not find or load a specific file.

We can see that PowerShell wasn’t able to load the newer version of the SDK when one was already loaded. If we load the modules in the opposite order we get a different error message.

Import-Module C:\Temp\AzBlobConflict\bin\Release\net5.0\publish\AzBlobConflict.dll
Get-BlobStandardLrsSku
Import-Module Az.Storage
        
Import-Module: Assembly with same name is already loaded

The exact error message and scenario of the conflict depends on which version of the assembly is needed, and which is loaded first, since only the first one can be loaded into the context of PowerShell. It also depends on the version of .NET that is loading the assembly, which in turn depends on which version of PowerShell you’re running.

As you see above, we also need to actually run the command of our module to load the assembly dependency, while Az.Storage loads the assembly on import, which increases the complexity of finding the error even more to an end user, if it only happens when a certain command is run in a larger script or solution.

If the newest version of the assembly is loaded first, it might just work out between certain modules in certain situations, but there are many scenarios where it simply won’t fly. This is what it all boils down to, the assembly dependency conflict.

Solving the Conflict

There are quick fixes like changing the version you’re depending on, or importing things in a different order, but today we will focus on a long term solution for our module that will not break because of factors outside of our control.

Wrapping the Dependency

The most robust solution for solving the problem is for our module to adapt. We will never be able to ask end users of our module to fix an issue as complex as this, especially when it’s not always possible to fix on their end. The way we can make sure that our it always works is to build a separate assembly load context for our dependencies.

A way to do this is to move the code that uses the dependency from the PowerShell cmdlet project in your module into a separate project.

Running the following code will remove our previous example and create a new solution file with the two projects needed, dependencies included.

# Clear what we created before
Get-ChildItem 'C:\Temp\AzBlobConflict' | Remove-Item -Recurse
Set-Location 'C:\Temp\AzBlobConflict'
# Name of solution is taken from folder
dotnet new sln
# Create projects Core and PS for the AzBlobConflict module example
dotnet new classlib -f 'net5.0' -n 'AzBlobConflict.Core'
dotnet new classlib -f 'net5.0' -n 'AzBlobConflict.PS'
# Add projects to solution
dotnet sln add 'AzBlobConflict.Core'
dotnet sln add 'AzBlobConflict.PS'
# Add packages and reference the Core project from PS project
dotnet add 'AzBlobConflict.PS' package 'System.Management.Automation'
dotnet add 'AzBlobConflict.Core' package 'Azure.Storage.Blobs'
dotnet add 'AzBlobConflict.PS' reference 'AzBlobConflict.Core'
# Split project structure (with new classes renamed)
πŸ“‚AzBlobConflict
 ┣ πŸ“‚AzBlobConflict.Core
 ┃ ┣ πŸ“„AzBlobConflict.Core.csproj
 ┃ ┃ β”— πŸ“¦Azure.Storage.Blobs
 ┃ β”— πŸ“„BlobSkuWrapper.cs
 ┣ πŸ“‚AzBlobConflict.PS
 ┃ ┣ πŸ“„AzBlobConflict.PS.csproj
 ┃ ┃ ┣ πŸ“¦System.Management.Automation
 ┃ ┃ β”— πŸ“‘AzBlobConflict.Core.csproj
 ┃ β”— πŸ“„GetBlobStandardLrsSku.cs
 β”— πŸ“„AzBlobConflict.sln

Note how the reference to our dependency now is in the Core project which acts as a dependency wrapper, while the PS project constitutes the actual module and references only the PowerShell library and our new Core project.

The focus of this solution is to solve the conflict for .NET (previously .NET Core) and PowerShell (previously PowerShell Core), and not .NET Framework or Windows PowerShell. While it works in a similar way I’d like you to be aware that there are differences compared between how the versions handle assembly loading.

AzBlobConflict.Core

There are no right or wrong names for the wrapper project that handles the dependency usage, but I like to call it Core. This is also what we named it in the open source module PSBicep which utilizes this technique in the project called BicepNet to avoid dependency conflicts with other modules that utilize the same assemblies as the module.

If we expand on our example module from before, we will really only have to move a single line of code to the Core project, the line where we reference the enum value SkuName.StandardLrs. We cannot have any direct reference to Azure.Storage.Blobs in the PowerShell module project, so in this project we also need to create a “mirror model” of the enum from the SDK if we want to output it to the user.

We will rename the Class1.cs file for this project to BlobSkuWrapper.cs and change it a little.

        
// BlobSkuWrapper.cs
using System;
using Azure.Storage.Blobs.Models;

namespace AzBlobConflict.Core
{
    // This enum is a mirror of the one called SkuName found in the SDK
    public enum BlobSkuName
    {
        StandardLrs,
        StandardGrs,
        StandardRagrs,
        StandardZrs,
        PremiumLrs
    }
    public static class BlobSkuWrapper
    {
        public static BlobSkuName GetStandardLrsName()
        {
            // The only important thing for the example is that we reference the dependency somehow
            BlobSkuName standardLrs;
            // In this case we just parse the SkuName enum from the SDK to our own enum
            Enum.TryParse(SkuName.StandardLrs.ToString(), out standardLrs);

            return standardLrs;
        }
    }
}

For this simple example it’s all we need. As stated above, the only thing we really need to demonstrate the solution is a reference to our dependency assembly, and a way to map the type so that we avoid a direct reference from the actual module project. The way we do that above is to parse one enum from the SDK into another one defined in our Core project.

In a more thorough example that could mean that if we would receive a list of blobs found on the storage account from the SDK, we would have to return a list of mirrored blob items defined in this project instead, with the information we’re interested in. This is because the real type of the blob objects returned from the SDK would be defined in code from the dependency we’re wrapping, something we cannot have a direct reference to from our module code.

In practice this means that the Core project mostly has:

To summarize, this project is mainly a type of mapping layer between the dependency and the PowerShell module. It may contain more functional logic too, but whether or not you let that reside the Core or PS project is more of a choice that’s up to you as the author of the module.

AzBlobConflict.PS

In contrast to the Core project, the PS project is where the actual module resides, together with the logic to handle the assembly loading. Any time we need to use functionality from our dependency, we need to make sure not to receive any object types back that are defined in the dependency assembly. As described above we need to make sure to always reference a mirrored model of any data we want to access from the SDK, otherwise PowerShell will try to load the assembly directly from our module instead of through the dependency wrapper project.

There is only a slight difference to our cmdlet from how it looked before. Instead of referencing the value directly from the SDK, we call the method from the Core project where we do the mapping, as seen above.

        
// GetBlobStandardLrsSku.cs
using AzBlobConflict.Core;
using System.Management.Automation;

namespace AzBlobConflict
{
    [Cmdlet(VerbsCommon.Get, "BlobStandardLrsSku")]
    public class GetBlobStandardLrsSku : PSCmdlet
    {
        protected override void ProcessRecord()
        {
            WriteObject(BlobSkuWrapper.GetStandardLrsName());
        }
    }
}

So now we’re done, right? We’ve wrapped the dependency of our simple example in a separate project and no longer have any direct reference to the SDK. Unfortunately not, because of how PowerShell imports all assemblies into a shared context. We need to create our own context and make sure we only import it there.

If we rebuild the module and try to import the modules together again we will still see the same errors as before.

dotnet publish -c Release
Import-Module Az.Storage
Import-Module C:\Temp\AzBlobConflict\AzBlobConflict.PS\bin\Release\net5.0\publish\AzBlobConflict.dll
Get-BlobStandardLrsSku
        
Get-BlobStandardLrsSku: Could not load file or assembly 'Azure.Storage.Blobs, Version=12.10.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8'. Could not find or load a specific file.

The Build Process

Before we take the next step to create our own context, we will want to make sure that PowerShell doesn’t find the assembly dependencies when loading the module.

It might sound counterintuitive, but to make sure that we can customize the assembly loading for our context we need to make sure that PowerShell doesn’t pick it up first.

A good way to do that is by creating a simple build.ps1 script in the root directory. The script will still run the dotnet publish command like we did before to build the module, but it will copy the module and dependency files into a new structure, where PowerShell won’t automatically load the dependency into the shared context when importing the module.

# build.ps1
$Configuration = 'Release'
$DotNetVersion = 'net5.0'

# Define build output locations
$OutDir = "C:\Temp\AzBlobConflict\output"
$OutDependencies = "$OutDir\dependencies"

# Build both Core and PS projects
dotnet publish -c $Configuration

# Ensure output directories exist and are clean for build
New-Item -Path $OutDir -ItemType Directory -ErrorAction Ignore
Get-ChildItem $OutDir | Remove-Item -Recurse
New-Item -Path $OutDependencies -ItemType Directory

# Create array to remember copied files
$CopiedDependencies = @()

# Copy .dll and .pdb files from Core to the dependency directory
Get-ChildItem -Path "AzBlobConflict.Core\bin\$Configuration\$DotNetVersion\publish" |
    Where-Object { $_.Extension -in '.dll','.pdb' } |
    ForEach-Object {
        $CopiedDependencies += $_.Name
        Copy-Item -Path $_.FullName -Destination $OutDependencies
    }

# Copy files from PS to output directory, except those already copied from Core
Get-ChildItem -Path "AzBlobConflict.PS\bin\$Configuration\$DotNetVersion\publish" |
    Where-Object { $_.Name -notin $CopiedDependencies -and $_.Extension -in '.dll','.pdb' } |
    ForEach-Object {
        Copy-Item -Path $_.FullName -Destination $OutDir
    }

Once we have the new module output folder structure in place when building our module, we can create our assembly load context in the PS project. This structure also lets us more easily load other dependencies to our module in the future if we would need to.

DependencyAssemblyLoadContext.cs

The first of our last two classes to add is our custom AssemblyLoadContext, a class we will inherit from to create our own context with modified loading logic.

This is where we define how we will handle loading our specific dependency assembly, which will be placed in a subfolder called dependencies after running our build.ps1 script.

        
// DependencyAssemblyLoadContext.cs
using System.IO;
using System.Reflection;
using System.Runtime.Loader;

namespace AzBlobConflict.PS
{
    public class DependencyAssemblyLoadContext : AssemblyLoadContext
    {
        private readonly string dependenciesDirectory;

        public DependencyAssemblyLoadContext(string path)
        {
            // Save the full path to the dependencies directory when creating the context
            dependenciesDirectory = path;
        }

        protected override Assembly Load(AssemblyName assemblyName)
        {
            // Create a path to the assembly in the dependencies directory
            string assemblyPath = Path.Combine(
                dependenciesDirectory,
                $"{assemblyName.Name}.dll");

            // Make sure the assembly exists in the directory before attempting to load it
            // Otherwise we will try to load things like the netstandard assembly directly
            if (File.Exists(assemblyPath))
            {
                // Use an inherited method for loading
                // A Load method from the Assembly class would load the assembly into the shared context
                return LoadFromAssemblyPath(assemblyPath);
            }

            return null;
        }
    }
}

The code in this class is fairly simple. We inherit from the AssemblyLoadContext class and save the path to our dependency directory when our context is created. When the context is asked to load the assembly, we do so by overriding the loading logic to see if the assembly file exists in the dependency directory, in which case we load it.

ModuleAssemblyContextHandler.cs

When we have the assembly load context in place the last step is to make sure that we hook it up to be used when importing our module.

We do that by creating another class that implements the interface IModuleAssemblyInitializer which lets us run code when our module is imported.

        
// ModuleAssemblyContextHandler
using System.IO;
using System.Management.Automation;
using System.Reflection;
using System.Runtime.Loader;

namespace AzBlobConflict.PS
{
    public class ModuleAssemblyContextHandler : IModuleAssemblyInitializer
    {
        // Get the path of the dependencies directory relative to the module file
        private static readonly string dependencyDirPath = Path.GetFullPath(
            Path.Combine(
                Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location),
                "Dependencies"));

        // Create the custom load context to use, with the path to the dependencies directory
        private static readonly DependencyAssemblyLoadContext dependencyLoadContext = new DependencyAssemblyLoadContext(dependencyDirPath);

        // This will run when the module is imported
        public void OnImport()
        {
            // Hook up our own assembly resolving method
            // It will run when the default load context fails to resolve an assembly
            AssemblyLoadContext.Default.Resolving += ResolveAssembly;
        }

        private static Assembly ResolveAssembly(
            AssemblyLoadContext defaultAlc,
            AssemblyName assemblyToResolve)
        {
            // If the assembly is our dependency assembly
            if (assemblyToResolve.Name == "AzBlobConflict.Core")
            {
                // Load it using our custom assembly load context
                return dependencyLoadContext.LoadFromAssemblyName(assemblyToResolve);
            }
            else
            {
                // Otherwise indicate that nothing was loaded
                return null;
            }
        }
    }
}

It’s a little more complex than our assembly load context class, but we only really do three things here.

Something to note is that we call the LoadFromAssemblyName here, but if we look at the load context class we created above we actually handle the loading in the Load method. This is not a typo, the reason is simply because it uses that method internally.

The way that the Resolving event is handled is the reason we needed to separate the dependencies from the module. Our context is only given a change to load the assembly if the default load context has already failed. If the default context succeeds in loading an assembly, the event is never fired.

Using the Assembly Load Context

Now that we’ve written and hooked up our own assembly load context for our module, let’s run our new build script and verify that the conflict is solved.

PipeHow:\Blog> .\build.ps1

If we take a look at the output directory we can see that there are a lot of files. We can make sure that they’re not created and copied to the output directory by adding the property PrivateAssets="All" to our System.Management.Automation package reference in AzBlobConflict.PS.csproj. The property controls what dependencies our module will expose.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Management.Automation" Version="7.1.5" PrivateAssets="All"/>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\AzBlobConflict.Core\AzBlobConflict.Core.csproj" />
  </ItemGroup>

</Project>

After that small fix we’re finally ready to see if our hard work paid off! Let’s run the build and import the modules together again.

PipeHow:\Blog> .\build.ps1
PipeHow:\Blog> Import-Module Az.Storage
PipeHow:\Blog> Import-Module .\AzBlobConflict\output\AzBlobConflict.PS.dll
PipeHow:\Blog> Get-BlobStandardLrsSku
StandardLrs
PipeHow:\Blog> New-AzStorageContext -StorageAccountName 'PipeHow'

StorageAccountName  : PipeHow
BlobEndPoint        : https://pipehow.blob.core.windows.net/
TableEndPoint       : https://pipehow.table.core.windows.net/
QueueEndPoint       : https://pipehow.queue.core.windows.net/
FileEndPoint        : https://pipehow.file.core.windows.net/
Context             : Microsoft.WindowsAzure.Commands.Storage.AzureStorageContext
Name                :
StorageAccount      : BlobEndpoint=https://pipehow.blob.core.windows.net/;QueueEndpoint=https://pipehow.queue.core.windows.net/;TableEndpoint=https://pipehow.table.core.windows.net/;FileEndpoint=https://pipehow.file.core.windows.net/;
TableStorageAccount : BlobEndpoint=https://pipehow.blob.core.windows.net/;QueueEndpoint=https://pipehow.queue.core.windows.net/;TableEndpoint=https://pipehow.table.core.windows.net/;FileEndpoint=https://pipehow.file.core.windows.net/
Track2OauthToken    : Microsoft.WindowsAzure.Commands.Storage.Common.AzureSessionCredential
EndPointSuffix      : core.windows.net/
ConnectionString    : BlobEndpoint=https://pipehow.blob.core.windows.net/;QueueEndpoint=https://pipehow.queue.core.windows.net/;TableEndpoint=https://pipehow.table.core.windows.net/;FileEndpoint=https://pipehow.file.core.windows.net/;
ExtendedProperties  : {}

PipeHow:\Blog> [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object Location -like '*Azure.Storage.Blobs.dll' | Select-Object FullName,Location

FullName                                                                                 Location
--------                                                                                 --------
Azure.Storage.Blobs, Version=12.8.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8  C:\Program Files\PowerShell\Modules\Az.Storage\3.11.0\Azure.Storage.Blobs.dll
Azure.Storage.Blobs, Version=12.10.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8 C:\Temp\AzBlobConflict\output\Dependencies\Azure.Storage.Blobs.dll

We now get the expected results from the commands without errors, and we can see that the different versions of the dependency assembly have both been loaded. The conflict is solved!

# The final project structure
πŸ“‚AzBlobConflict
 ┣ πŸ“‚AzBlobConflict.Core
 ┃ ┣ πŸ“„AzBlobConflict.Core.csproj
 ┃ ┃ β”— πŸ“¦Azure.Storage.Blobs
 ┃ β”— πŸ“„BlobSkuWrapper.cs
 ┣ πŸ“‚AzBlobConflict.PS
 ┃ ┣ πŸ“„AzBlobConflict.PS.csproj
 ┃ ┃ ┣ πŸ“¦System.Management.Automation
 ┃ ┃ β”— πŸ“‘AzBlobConflict.Core.csproj
 ┃ ┣ πŸ“„DependencyAssemblyLoadContext.cs
 ┃ ┣ πŸ“„GetBlobStandardLrsSku.cs
 ┃ β”— πŸ“„ModuleAssemblyContextHandler.cs
 ┣ πŸ“‚output # build.ps1 copies PS (module) here
 ┃ ┣ πŸ“‚dependencies # build.ps1 copies Core (dependencies) here
 ┃ ┃ ┣ πŸ“šAzBlobConflict.Core.dll
 ┃ ┃ β”— πŸ“šAzure.Storage.Blobs.dll
 ┃ β”— πŸ“šAzBlobConflict.PS.dll
 ┣ πŸ“„AzBlobConflict.sln
 β”— πŸ“„build.ps1

Conclusion

Assembly load contexts absolutely sound more intimidating than they are, but if you grasp the concept there’s not a lot of code required to set up a small project.

I like this a lot as a solution to the dependency conflict problem for modules! If you build modules on a regular basis I think that being able to wrap your dependencies in a separate load context is a superb tool to have. I hope you’ll find it as useful as I expect to!

The code written in the post can be found as an example project here on GitHub. Both as a module and as an example for the load context the code has several areas that could be improved (such as having a module manifest and actual functionality), but for the purpose of keeping it simple to grasp and easy to follow along I wanted to keep the implementation on the shorter side.

I wouldn’t be able to finish this post with a clear conscience without mentioning Rob Holt from the PowerShell team and his in-depth post about several scenarios on the subject, it helped me immensely in understanding the concept and the caveats in implementing it.

Finally I’d like to give a special thanks to my coworkers and friends Simon WΓ₯hlin and Stefan Ivemo for inspiring me to write the post on this subject as well as bouncing ideas and questions.

Comments

comments powered by Disqus