如何解决PowerShell模块中的程序集依赖性版本冲突

时间:2014-05-12 17:42:30

标签: .net c#-4.0 powershell-v3.0

我有一个PowerShell脚本,如下所示:

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

Get-SomethingFromModule1 | Get-SomethingFromModule2

我遇到的问题是,module1.dllmodule2.dll都引用了SomeOtherLibrary.dll的不同版本,SomeOtherLibrary.dll的版本包含了我发生的重大变化使用

我可以跑

Import-Module module1.dll
Get-SomethingFromModule1

Import-Module module2.dll
Get-SomethingFromModule2

在单独的PowerShell会话中,每个都表现正确。

但是,我想将数据从一个cmdlet传递到另一个cmdlet,Get-SomethingFromModule2由于找不到方法而引发异常。我相信只加载/使用SomeOtherLibrary.dll的最新版本(或导入的第一个模块使用的版本)。有没有办法强制module1.dllmodule2.dll加载/使用特定版本的SomeOtherLibrary.dll

我试图避免更新引用并重新编译所有这些模块。

谢谢

2 个答案:

答案 0 :(得分:2)

我通过在.csproj文件中提供强名称密钥文件来强烈命名程序集SomeOtherLibrary.dll

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

我现在可以导入两个模块,每个模块都使用自己版本的程序集SomeOtherLibrary.dll。这种方法仍然需要我更新引用并重新编译所有这些模块。

但是,只要我强烈命名所有PowerShell模块引用的程序集,它就可以防止将来发生此问题。

答案 1 :(得分:0)

如果强命名不是一个选项,您还可以在单​​独的appdomains中运行cmdlet逻辑,从而在Powershell加载和运行代码时保持依赖关系的隔离。通过一些简单的优化,如果考虑性能,单独的appdomain版本也可以相当快地运行。

假设您有一个看起来像这样的cmdlet:

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");
        }
    }
}

为了重构这个,你需要做四件事。首先,您需要代码来管理appdomain创建和远程处理。我使用过这样的辅助类:

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;
        }
    }
}

您需要的第二件事是一个界面,它定义您的代码将在单独的appdomain中执行的操作。这篇文章实际上非常重要,但在撰写本文时我不确定为什么 - 我可能会发布一个我自己的问题。我很简单,就像这样:

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

第三,您需要在代码周围创建一个包装器。此包装器的类型将用作远程代理,其代码将在单独的appdomain中运行。我看起来像这样 - 注意与原始cmdlet的相似性,并注意继承链:

using System;
using Common;

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

最后,您需要重构原始cmdlet,以便使用单独的appdomain执行代码。我看起来像这样,其中包括一些额外的代码来优化性能:

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));
        }
    }
}

有关此技术的更多详细信息,包括示例代码的链接,可以找到here