Eric K Yung
Eric K Yung

Reputation: 1784

How to resolve assembly dependency version conflict in powershell modules

I have a powershell script that looks like below:

Import-Module module1.dll
Import-Module module2.dll

Get-SomethingFromModule1 | Get-SomethingFromModule2

The problem I am running into is that both module1.dll and module2.dll reference a different version of SomeOtherLibrary.dll and the versions of SomeOtherLibrary.dll contain a breaking change that I happen to use.

I can run

Import-Module module1.dll
Get-SomethingFromModule1

and

Import-Module module2.dll
Get-SomethingFromModule2

in separate powershell sessions and each behaves correctly.

However I want to pipe data from one cmdlet to the other and Get-SomethingFromModule2 throws an exception due to method not found. I believe only the latest version (or the version used by the first module being imported) of SomeOtherLibrary.dll is loaded/used. Is there a way to force module1.dll and module2.dll to load/use their specific version of SomeOtherLibrary.dll?

I am trying to avoid updating references and recompile all these modules.

Thank you

Upvotes: 1

Views: 1511

Answers (2)

killthrush
killthrush

Reputation: 5087

If strong-naming isn't an option, you can also run your cmdlet logic inside separate appdomains, thus keeping your dependencies segregated when Powershell loads and runs your code. With some simple optimizations, the separate-appdomain version can also run fairly quickly if performance is a concern.

Let's say you have a cmdlet that looks kinda like this one:

using System.Management.Automation;

namespace CmdletOne
{
    [Cmdlet(VerbsCommon.Show, "Path")]
    public class ShowPathCmdlet : Cmdlet
    {
        protected override void ProcessRecord()
        {
            // ...
            // Run some business logic & load assemblies
            // ...
            WriteObject("Value is foo");
        }
    }
}

There are four things you need in order to refactor this. First, you need code to manage the appdomain creation and remoting. I used something like this helper class:

using System;
using System.IO;

namespace Common
{
    /// <summary>
    /// General-purpose class that can put a remoting proxy around a given type and create a new appdomain for it to run in.
    /// This effectively "sandboxes" the code being run and isolates its dependencies from other pieces of code.
    /// </summary>
    /// <typeparam name="T">The type of object that will be run in the sandbox.  Must be compatible with Remoting.</typeparam>
    public class ExecutionSandbox<t> : IDisposable 
        where T : MarshalByRefObject
    {
        /// <summary>
        /// Local copy of the sandbox app domain
        /// </summary>
        private AppDomain _domain;

        /// <summary>
        /// Reference of the proxy wrapper for T
        /// </summary>
        public T ObjectProxy { get; private set; }

        /// <summary>
        /// Creates an instance of ExecutionSandbox
        /// </summary>
        /// <param name="assemblyPath" />The path where the assembly that contains type T may be found
        public ExecutionSandbox(string assemblyPath)
        {
            Type sandboxedType = typeof (T);
            AppDomainSetup domainInfo = new AppDomainSetup();
            domainInfo.ApplicationBase = assemblyPath;
            _domain = AppDomain.CreateDomain(string.Format("Sandbox.{0}", sandboxedType.Namespace), null, domainInfo);

            string assemblyFileName = Path.Combine(assemblyPath, sandboxedType.Assembly.GetName().Name) + ".dll";
            object instanceAndUnwrap = _domain.CreateInstanceFromAndUnwrap(assemblyFileName, sandboxedType.FullName);
            ObjectProxy = (T)instanceAndUnwrap;
        }

        /// <summary>
        /// Allows safe cleanup of the sandbox app domain.
        /// </summary>
        public void Dispose()
        {
            if (_domain != null)
            {
                AppDomain.Unload(_domain);
                _domain = null;
            }
            ObjectProxy = null;
        }
    }
}

The second thing you need is an interface that defines the operation(s) your code will perform in the separate appdomain. This piece is actually critically important but as of this writing I'm not sure why - I'll probably post a question of my own. Mine was dead-simple, something like this:

namespace CmdletOne
{
    public interface IProxy
    {
        string DoWork();
    }
}

Third, you need to create a wrapper around your code. This wrapper's type will be used as a remoting proxy and its code will run inside the separate appdomain. Mine looked like this - note the similarity to the original cmdlet, and also note the inheritance chain:

using System;
using Common;

namespace CmdletOne
{
    public class Proxy : MarshalByRefObject, IProxy
    {
        public string DoWork()
        {
            // ...
            // Run some business logic & load assemblies
            // ...
            return "foo";
        }
    }
}

Lastly, you need to refactor your original cmdlet so that it executes your code using the separate appdomain. Mine looked like this, which included some extra code to optimize for performance:

using System;
using System.IO;
using System.Management.Automation;
using System.Reflection;
using Common;

namespace CmdletOne
{
    [Cmdlet(VerbsCommon.Show, "Path")]
    public class ShowPathCmdlet : Cmdlet
    {
        private static ExecutionSandbox _executionSandbox;
        private readonly object _lockObject = new object();

        protected override void ProcessRecord()
        {
            DateTime start = DateTime.Now;

            lock (_lockObject)
            {
                if (_executionSandbox == null)
                {
                    string cmdletExecutionPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
                    _executionSandbox = new ExecutionSandbox(cmdletExecutionPath);
                }
            }

            Proxy proxy = _executionSandbox.Value;
            string path = proxy.DoWork();

            DateTime end = DateTime.Now;

            WriteObject(string.Format("Value is {0}.  Elapsed MS: {1}", path, (end - start).TotalMilliseconds));
        }
    }
}

More details about this technique, including a link to sample code, can be found here.

Upvotes: 0

Eric K Yung
Eric K Yung

Reputation: 1784

I strongly named the assembly SomeOtherLibrary.dll by providing a strong name key file in the .csproj file:

<PropertyGroup>
  <AssemblyOriginatorKeyFile>StrongNameKey.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

I can now import both modules and each module uses its own version of the assembly SomeOtherLibrary.dll. This approach still requires me to update the references and recompile all these modules.

However, it prevents this problem from occurring in the future as long as I strongly named all assemblies that powershell modules reference.

Upvotes: 2

Related Questions