65.9K
CodeProject 正在变化。 阅读更多。
Home

在运行时加载和卸载程序集

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (14投票s)

2007年5月9日

GPL3

14分钟阅读

viewsIcon

146081

downloadIcon

4016

演示了如何加载和卸载 .NET 程序集,以及如何在应用程序域之间进行通信。

Screenshot - MainWindow.jpg

引言

在我公司,我从事 .NET 3.0 开发。我的一个工作任务包括对运行时工作流更改的研究。我们的目标是在运行时加载工作流规则。当时,我们将工作流信息和规则合并到一个单独的 .NET 程序集中。但它在设计时就已经作为引用被引用到应用程序中。一个工作流工具(我认为是微软的)提供了在运行时创建和修改工作流的功能。我们期望工作流数据以 .NET 程序集的形式提供,并且该程序集应像打开 Word 文件一样加载到应用程序中。

我开始考虑在不停止应用程序的情况下加载和卸载程序集。首先,我像大家一样,在网上搜索了任何示例应用程序。但唉!不幸的是(或者幸运的是!),没有与此主题相关的样本,无论我搜索哪里,我都发现有参考说这非常难实现,并且每个人都建议不要这样做。从根本上说,这是 .NET Framework 的一个限制。但在我们的项目中,此功能是一个关键组件,因为没有它,整个任务将无法使用。所以我直接跳到 MSDN,从应用程序域(AppDomains)开始。因为我已经有了反射和应用程序域的一些实践经验,所以找到该怎么做并不困难。完成需求后,我开始考虑分享我所做的工作,这最终促成了这篇文章的创建。

背景

本文包含与程序集应用程序域以及一些反射基础知识相关的主题。如果您正在寻找关于这些主题的任何文章,那么本文可能适合您。

  1. 应用程序域的基本用法。
  2. 在不卸载应用程序的情况下卸载程序集。
  3. 在运行时加载程序集。
  4. 程序集的运行时版本控制。
  5. 使用反射从程序集中调用任何成员。
  6. 跨应用程序域通信。
  7. 第二个应用程序域的加载和卸载。
  8. 在不锁定 Windows 的程序集文件的情况下使用 .NET 程序集。
  9. 一个基本的结构化编码流程示例。

运行应用程序

为了确保应用程序正常运行,请使用“重新生成解决方案”选项编译解决方案。然后运行应用程序。

Screenshot - MainWindow1.jpg

默认情况下,将加载程序集版本一。您应该能在主窗口中看到当前程序集、当前应用程序域、主应用程序域等信息。单击“反转”按钮后,文本中的值将被反转。但由于存在 bug,每次长度都会缩短。单击“获取值”按钮,我们可以将程序集版本获取为1

现在点击单选按钮“加载程序集 V 2.0”来加载最终版本。您应该能在相应的单选按钮中看到错误提供者的消息。

Screenshot - MainWindow2.jpg

现在应用程序已加载了第二个版本的程序集,该版本已修复了 bug。现在我们可以正确获取“反转”后的值,并将程序集版本获取为2

使用代码

当我开始考虑分享代码时,我发现我的代码过于复杂和臃肿。它包含许多与 .NET 3.0 相关的附加功能,这些功能超出了本主题的范围。我决定创建一个新的 .NET 2.0 Windows 应用程序,并在其中实现所有必需的功能。

我创建了一个新的 Windows 应用程序项目,并设计了基本的用户界面来确定示例的功能和工作流。我继续确定类结构,然后进入最终编码。为了方便您,我将包含我工作步骤的详细过程。

步骤 1:创建基本用户界面

创建一个新的 Windows 应用程序项目,并将其命名为“Code Project - Unload .NEt Assembly”。获取项目属性,并将默认命名空间设置为 MySpace。按照上图创建用户界面。

设计时,请注意应用程序中引用的基本 UI 控件名称。

Control 类型 文本 附加设置
radioAssembly1 RadioButton 加载程序集 V 1.0 -
radioAssembly2 RadioButton 加载程序集 V 2.0 -
lblCurrentAssembly Label 当前程序集 -
lblCurrentAppDomain Label 当前应用程序域 -
lblMainAppDomian 文本框 默认应用程序域 ReadOnly=true, BorderStyle=None
txtReverseData 文本框 要反转的字符串数据 -
txtReturnedData 文本框 (null) -
btnCalculate Button Reverse -
btnGetValue Button 获取值 -
errorProvider1 错误提供程序 errorProvider1 -

步骤 2:确定业务流程 - 确定接口、类和层次结构

该示例为不同级别的用户呈现了三种业务场景。

对于最终用户

该程序可用于获取已加载程序集的版本信息,并且可以反转传递到程序集的字符串。当应用程序启动时,它会加载程序集的 beta 版本,之后用户可以在不关闭应用程序的情况下切换到最终发布的版本。程序将继续工作,无论加载了哪个库文件,但功能会根据加载的程序集文件而有所变化。

对于底层程序员

启动时,Form1_Load 将创建一个代理实例,该代理使用默认库(“版本 1.0”)和域名称。此代理具有两个方法,它们构成了示例的核心业务逻辑。一个用于返回版本信息,另一个用于执行已加载程序集中的操作。还公开了两个属性,它们返回默认的应用程序域和程序集名称。对于最终用户/底层程序员来说,这些是唯一公开的详细信息。用户只需添加对代理和接口的引用,然后创建实例并利用其公开的方法。用户不应该知道正在工作的动态加载功能。

对于组件程序员

下一步更复杂,面向想要创建程序集附加版本的程序员。如果您计划创建程序集的下一个版本,例如 3.0 版本,则需要了解这些详细信息。

基本的业务模型定义在名为 BaseInterface 的项目中作为一个接口。它包含了将在程序集中实现的所有方法。主应用程序、代理和所有程序集都需要引用此程序集并实现其功能。如果您想添加任何附加功能,则可以更改此接口并重新编译解决方案。然后,在显示错误消息的地方,相应地实现该方法以使其兼容。

关于程序集的动态加载,使用了两个类:AssemblyCore 执行文件相关的函数,如设置默认程序集,并存储程序集类型和文件相关信息;AppDomainCore 执行加载和卸载应用程序域等操作,存储默认应用程序域名称和实例等。

步骤 3:创建附加项目、类结构和控制流 - 接口、代理、beta 程序集、最终程序集

为了继续,我们必须创建另外四个项目,全部是类库类型。

项目 1. 接口

BaseInterface:包含要在整个解决方案中共享的基本业务模型。

  • Name = BaseInterface
  • 设置 RootNamespace = BaseInterface
  • Class1.cs 重命名为 IBaseInterface.cs

类 - IBaseInterface.cs

将此代码粘贴到代码窗口

using System;
using System.Collections.Generic;
using System.Text;
        
namespace BaseInterface
{
    public interface IBaseInterface
    {
        string ReturnBaseValue();
        string ReverseValue(string Value);
    }
}
项目 2. 代理

基本的代理,将底层程序员从动态程序集加载机制中抽象出来。

  • Name = Proxy
  • 设置 RootNamespace = MySpace
  • 引用 = Solution.BaseInterface

项目需要三个类文件:Proxy.csAssemblyCore.csAppDomainCore.cs

类 1 - AssemblyCore.cs

公开的接口
  • public bool SetDefaultAssemblyFile(string AssemblyFileName) - 更改默认程序集文件。
  • public FileInfo DefaultAssemblyFile - 返回默认程序集文件的 FileInfo 实例。
  • public string DefaultAssemblyFileName - 始终相同。应用程序将其用作程序集。
  • public string CurrentType - 获取当前使用的对象类型。
  • public string ActiveAssemblyFile - 获取活动程序集文件的原始名称。
  • public AssemblyCore(string AssemblyFileName,string TypeName) - 构造函数。

首先,将骨架代码粘贴到代码窗口。

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
        
namespace MySpace
{
     internal class AssemblyCore
     {
        const string OriginalAssemblyFileName = "DefaultAssembly.dll";
        
        private string _activeAssemblyFile;
        public string ActiveAssemblyFile
        {
            get { return _activeAssemblyFile; }
        }
        
        private string _CurrentType;
        public string CurrentType
        {
            get { return _CurrentType; }
            set { _CurrentType = value; }
        }
        
        public string DefaultAssemblyFileName
        {
        }
        
        private FileInfo _DefaultAssemblyFile;
        public FileInfo DefaultAssemblyFile
        {
            get { return _DefaultAssemblyFile; }
        }
        
        public AssemblyCore(string AssemblyFileName,string TypeName)
        {
        }
        
        public bool SetDefaultAssemblyFile(string AssemblyFileName)
        {
        }
    }
}

类 2 - AppDomainCore.cs

公开的接口
  • public AppDomain DefaultAppDomain - 返回正在使用的应用程序域实例。
  • public string DefaultAppdomainName - 返回正在使用的应用程序域的友好名称。
  • public AppDomainCore(string AppDoaminName) - 构造函数。

将骨架代码粘贴到代码窗口

using System;
using System.Collections.Generic;
using System.Text;
        
namespace MySpace
{
     internal class AppDomainCore 
     {
          public AppDomainCore(string AppDoaminName)
          {
          }
        
          private AppDomain _DefaultAppDomain;
          public AppDomain DefaultAppDomain
          {
               get { return _DefaultAppDomain; }
          }
        
          private string _DefaultAppdomainName;
          public string DefaultAppdomainName
          {
               get { return _DefaultAppdomainName; }
          }
     }
}

类 3 - Proxy.cs

实现基本的业务接口 BaseInterface.IBaseInterface

公开的接口
  • public string ReverseValue(string Value) - 返回传递字符串的反转值。
  • public string ReturnBaseValue() - 从程序集返回一个值。这里是程序集版本信息。
  • public Proxy(string AssemblyFileName, string AppDomainName,string CurrentType) - 构造函数。
  • public Proxy(string AssemblyFileName,string AppDomainName) - 构造函数。
  • public string DefaultAssemblyFileName - 默认程序集文件名。
  • public string DefaultAppDomain - 第二个应用程序域名称。

将代码粘贴到代码窗口

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.Windows.Forms;

namespace MySpace
{
    public class Proxy:BaseInterface.IBaseInterface
    {
        public string DefaultAppDomain
        {
        }

        public string DefaultAssemblyFileName
        {
        }        
        public Proxy(string AssemblyFileName,string AppDomainName)
        {
        }
        
        public Proxy(string AssemblyFileName, 
               string AppDomainName,string CurrentType)
        {
        }
        
        #region BaseInterface Members
        public string ReturnBaseValue()
        {
        }
        
        public string ReverseValue(string Value)
        {
        }
        #endregion
    }
}
项目 3. Beta 程序集

程序集的第一个版本。

  • Name = Assembly v1.0
  • 程序集版本 = 1.0.0.0
  • 引用 = Solution.BaseInterface
  • Debug 编译路径 = “..\..\CodeProject - Unload.Net Assembly\CodeProject - Unload.Net Assembly\bin\Debug\”(这是为了使程序集位于主可执行文件路径下)
  • Class1.cs 重命名为 ClassLibrary.cs

类 - ClassLibrary.cs

公开了从引用的接口 BaseInterface.IBaseInterface 实现的方法。

将代码粘贴到代码窗口

using System;
using System.Collections.Generic;
using System.Text;
namespace MyAssembly
{
    public class ClassLibrary : BaseInterface.IBaseInterface
       {
          #region BaseInterface Members
          public string ReturnBaseValue()
          {
          }         
       
          public string ReverseValue(string Value)
          {
          }     
          #endregion
     }
}
项目 4. 最终程序集

程序集的下一个版本。

  • Name = Assembly v2.0
  • 程序集版本 = 2.0.0.0
  • 引用 = Solution.BaseInterface
  • Debug 编译路径 = “..\..\CodeProject - Unload.Net Assembly\CodeProject - Unload.Net Assembly\bin\Debug\”(这是为了使程序集位于主可执行文件路径下)
  • Class1.cs 重命名为 ClassLibrary.cs

类 - ClassLibrary.cs

公开了从引用的接口 BaseInterface.IBaseInterface 实现的方法。

将代码粘贴到代码窗口

using System;
using System.Collections.Generic;
using System.Text;
        
namespace MyAssembly
{
    public class ClassLibrary : BaseInterface.IBaseInterface
    {
        #region BaseInterface Members
        public string ReturnBaseValue()
        {
        }
        
        public string ReverseValue(string Value)
        {
        }
        #endregion
    }
}

步骤 4:最终构建

所有基本结构都已创建,现在我们可以进行实际编码了。我将从需要集成业务逻辑的基本库开始。

项目 1. Beta 程序集

我们首先要做的是使类实例能够跨越应用程序边界。为此,主要要求是继承该类来自 MarshalByRefObject 基类。此外,我们必须将类标记为 [Serializable]。为了完成实现,我们必须在两个方法中编写功能。在 ReturnBaseValue() 中插入以下代码

return "Value=Assembly Version 1.0";

并将此代码插入到方法 ReverseValue(string Value)

return ReverseString(Value);

然后在此代码下方添加以下代码 #endregion

private string ReverseString(string Value)
{
    StringBuilder tmp = new StringBuilder();
   
    //the actual requirement is >=0.
    //intentionaly made an error in alggoritum to make an error.
    for (int i = Value.Length - 1; i > 1; i--)
    {
        tmp.Append(Value.Substring(i, 1));
    }
    return tmp.ToString();
}
项目 2. 最终程序集

与上述项目一样,继承该类来自 MarshalByRefObject 基类,并将类标记为 [Serializable]。然后,在 ReturnBaseValue() 中插入以下代码

return "Value=Assembly Version 2.0";

并将此代码插入到方法 ReverseValue(string Value)

return ReverseString(Value);

然后在此代码下方添加以下代码 #endregion

private string ReverseString(string Value)
{
    StringBuilder tmp = new StringBuilder(); 
    
    for (int i = Value.Length - 1; i >= 0; i--)
    {
        tmp.Append(Value.Substring(i, 1));
    }
    return tmp.ToString();
}
项目 3. 代理

在完成功能所需的业务逻辑后,我们需要将关联的代码提供给代理。所以我想现在我们可以修改代理项目了。它包含三个类。

类 - AssemblyCore.cs

我们已经为常量 OriginalAssemblyFileName 赋值为“DefaultAssembly.dll”。此文件名将是应用程序已知的唯一程序集文件名。将此代码插入到属性 public string DefaultAssemblyFileName

get { return OriginalAssemblyFileName; }

创建对象实例时,我们需要保存当前类型并替换使用的程序集为新程序集,以便应用程序能够引用它。为此,请将以下代码插入到构造函数 public AssemblyCore(string AssemblyFileName,string TypeName)

CurrentType = TypeName;
SetDefaultAssemblyFile(AssemblyFileName); 

函数 SetDefaultAssemblyFile 使用指定的程序集文件替换当前使用的程序集。它还保存新创建的程序集文件实例和原始程序集名称。

将此代码粘贴到方法 public bool SetDefaultAssemblyFile(string AssemblyFileName)

try
{
    _activeAssemblyFile = AssemblyFileName;
    File.Copy(AssemblyFileName, OriginalAssemblyFileName, true);
    _DefaultAssemblyFile = new FileInfo(OriginalAssemblyFileName);
    return true;
}
catch(Exception Err)
{
    MessageBox.Show("An Error Occured. Versioning Failed. Details : " + Err.Message);
    return false;
}

类 – AppDomainCore.cs

将以下代码插入到构造函数 public AppDomainCore(string AppDoaminName)

_DefaultAppdomainName = AppDoaminName;
LoadAppDomain();

这将把活动的应用程序域名称分配给属性 DefaultAppDomain 并调用 LoadAppDomain 函数来创建新的应用程序域。将下面的代码粘贴到类的底部

private bool LoadAppDomain()
{
    AppDomainSetup ads = new AppDomainSetup();
    _DefaultAppDomain = 
      AppDomain.CreateDomain(DefaultAppdomainName, null, ads);
    _DefaultAppDomain.SetShadowCopyFiles();
    return true;
}

在这里,我们使用 AppdomainSetup 类来创建新的应用程序域。SetShadowCopyFiles() 函数使应用程序能够将程序集复制到另一个位置并从那里加载,从而释放原始程序集的锁定。

下一步是一个可选组件。在此场景中,它实际上不是必需的,因为当创建 Proxy 对象的新实例时,GC 会自动处理第二个应用程序域的卸载。将下面的代码粘贴到类的底部

private bool ClearAppDomain()
{
    try
    {
        AppDomain.Unload(DefaultAppDomain);
        _DefaultAppDomain = null;
        return true;
    }
    catch
    {
        return false;
    }
}
~AppDomainCore()
{
    ClearAppDomain();
}

我提供了 ClearAppDomain 函数,但它没有被调用,因为 GC 会在创建新的 Proxy 实例时负责清理应用程序域。如果您想显式卸载应用程序域,则需要此函数。即使未调用清理应用程序域的调用,也不会有任何区别。

如果在应用程序域类析构函数的析构函数中设置断点,您会发现一个延迟调用,它实际上是由 GC 调用,并且以一种不寻常的方式调用。这实际上是由 GC 在垃圾回收的三个代中的某个点触发的。此外,在析构函数中的调用是不必要的,因为在正常情况下 GC 会释放对象。我将此部分留给读者进行实验。

类 – Proxy.cs

Proxy 类抽象了上述两个类。它利用了 AppdomainCoreAssemblyCore 类。我们还需要 IBaseInterface 的一个实例来获取业务实体的本地代理。对于这些功能,请将以下代码粘贴到类开头的代码中

AppDomainCore _appDomainController;
AssemblyCore _assemblyController;
BaseInterface.IBaseInterface _proxy;

Proxy 公开了两个属性来启用其功能;将以下代码粘贴到方法 public string DefaultAppDomain

get { return _appDomainController.DefaultAppdomainName; }

以及将以下代码粘贴到 public string DefaultAssemblyFileName

get { return _assemblyController.ActiveAssemblyFile; }

下一步是定义构造函数。将以下代码放入 public Proxy(string AssemblyFileName,string AppDomainName)

Init(AssemblyFileName, AppDomainName, "MyAssembly.ClassLibrary");

并将以下代码放入重载的构造函数 public Proxy(string AssemblyFileName, string AppDomainName,string CurrentType)

public Proxy(string AssemblyFileName, string AppDomainName, string CurrentType)

这些构造函数调用私有方法 Init,您可以在此处找到该方法

private bool Init(string AssemblyFileName, string AppDomainName, string CurrentType)
{
    _assemblyController = new AssemblyCore(AssemblyFileName, CurrentType);
    _appDomainController = new AppDomainCore(AppDomainName);
    return true;
}

此方法初始化在类中创建的两个对象。

现在主要任务是完成接口的实现。将以下代码粘贴到方法 public string ReturnBaseValue() 中。

_proxy = (BaseInterface.IBaseInterface)_appDomainController.DefaultAppDomain.
CreateInstanceFromAndUnwrap(_assemblyController.DefaultAssemblyFileName, 
_assemblyController.CurrentType);

if (_proxy != null)
{
    return _proxy.ReturnBaseValue();
}
return null;

此方法 CreateInstanceFromAndUnwrap 创建并解包一个远程代理实例,该实例被分配给 _proxy

下面的方法也可以用同样的方式完成,但我采用了另一种方法使用反射来完成。将代码粘贴到方法 public string ReverseValue(string Value)

return ((string)GetReversedString("ReverseString", new object[] { Value })); 

并将下面的方法 GetReversedString(..) 放在函数下。

private object GetReversedString(string MethodName,object[] Arguments)
{
    object proxy = (BaseInterface.IBaseInterface)
      _appDomainController.DefaultAppDomain.CreateInstanceFromAndUnwrap(
      _assemblyController.DefaultAssemblyFileName, _assemblyController.CurrentType);
    if (proxy != null)
    {
        MethodInfo mi = proxy.GetType().GetMethod(MethodName, 
           BindingFlags.DeclaredOnly | BindingFlags.NonPublic | 
           BindingFlags.Instance);
        if (mi != null)
        {
            object rv = mi.Invoke(proxy, Arguments);
            return rv;
        }
        else
        {
            MessageBox.Show("Oops. Such a method not found...!", ".Net Reflection", 
            MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
    return null;
}

上述替代方法在没有公开接口,或者在最坏的情况下,在设计时方法签名不可用的情况下会很有用。这里不是从接口实例调用方法,而是查询对象实例的方法签名并调用它。

项目 4. 基本接口

这里没有什么遗留的。一切都在第一阶段完成了。

项目 5. 主应用程序

最后,在定义和实现所有这些类之后,我们将它们集成到主 Windows 应用程序中以使其工作。为此,我们要做的第一件事是添加对 ProxyBaseInterface 项目的引用。将此代码添加到 Form1 代码窗口的顶部

using MySpace;

将此代码粘贴到 Form1 类的顶部

Proxy DefaultProxy;

Form1_Load 中,添加以下代码

DefaultProxy = new Proxy("Assembly v1.0.dll", "Domain1");
lblCurrentAppDomain.Text = DefaultProxy.DefaultAppDomain;
lblCurrentAssembly.Text = DefaultProxy.DefaultAssemblyFileName;
lblMainAppDomian.Text = AppDomain.CurrentDomain.FriendlyName;

radioAssembly1Click 事件中,粘贴以下代码

DefaultProxy = new Proxy("Assembly v1.0.dll", "Domain1");
lblCurrentAppDomain.Text = DefaultProxy.DefaultAppDomain;
lblCurrentAssembly.Text = DefaultProxy.DefaultAssemblyFileName;
lblMainAppDomian.Text = AppDomain.CurrentDomain.FriendlyName;

//Extras
errorProvider1.BlinkStyle = ErrorBlinkStyle.AlwaysBlink;
errorProvider1.SetError(radioAssembly2, "");
errorProvider1.SetError(radioAssembly1,
  "This Dll is a Beta Version. It won't reverse the string properly");

然后在 radioAssembly2Click 事件中,粘贴以下代码

DefaultProxy = new Proxy("Assembly v2.0.dll", "Domain2");
lblCurrentAppDomain.Text = DefaultProxy.DefaultAppDomain;
lblCurrentAssembly.Text = DefaultProxy.DefaultAssemblyFileName;
lblMainAppDomian.Text = AppDomain.CurrentDomain.FriendlyName;
        
//Extras
errorProvider1.BlinkStyle = ErrorBlinkStyle.AlwaysBlink;
errorProvider1.SetError(radioAssembly1, "");
errorProvider1.SetError(radioAssembly2,"This Dll is the Final Version");

上述语句创建了代理的一个新实例,该实例内部销毁了现有的应用程序域,用新文件替换了程序集文件,并将其加载到应用程序中。为了利用代理中实现的业务功能,我们必须在按钮的点击事件中使用。

btnCalculateClick 事件中,添加以下代码

txtReverseData.Text = DefaultProxy.ReverseValue(txtReverseData.Text);

btnGetValueClick 事件中,添加以下代码

txtReturnedData.Text = DefaultProxy.ReturnBaseValue();

未来计划

由于我目前正在使用 .NET 3.0 和 Composite UI Application Block,我正试图在 CAB 应用程序中实现此功能。我已经创建了一个示例,可以在单击按钮或任何 UI 事件时加载 UI 模块。但是 .NET 2.0 应用程序存在同样的问题。我现在正在寻找一种方法来卸载从 CAB 应用程序中动态加载的模块。这可能需要一些时间,因为我正忙于许多事情,但您可以期待一篇有关此更新的文章。

© . All rights reserved.