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






4.71/5 (14投票s)
演示了如何加载和卸载 .NET 程序集,以及如何在应用程序域之间进行通信。
引言
在我公司,我从事 .NET 3.0 开发。我的一个工作任务包括对运行时工作流更改的研究。我们的目标是在运行时加载工作流规则。当时,我们将工作流信息和规则合并到一个单独的 .NET 程序集中。但它在设计时就已经作为引用被引用到应用程序中。一个工作流工具(我认为是微软的)提供了在运行时创建和修改工作流的功能。我们期望工作流数据以 .NET 程序集的形式提供,并且该程序集应像打开 Word 文件一样加载到应用程序中。
我开始考虑在不停止应用程序的情况下加载和卸载程序集。首先,我像大家一样,在网上搜索了任何示例应用程序。但唉!不幸的是(或者幸运的是!),没有与此主题相关的样本,无论我搜索哪里,我都发现有参考说这非常难实现,并且每个人都建议不要这样做。从根本上说,这是 .NET Framework 的一个限制。但在我们的项目中,此功能是一个关键组件,因为没有它,整个任务将无法使用。所以我直接跳到 MSDN,从应用程序域(AppDomains)开始。因为我已经有了反射和应用程序域的一些实践经验,所以找到该怎么做并不困难。完成需求后,我开始考虑分享我所做的工作,这最终促成了这篇文章的创建。
背景
本文包含与程序集、应用程序域以及一些反射基础知识相关的主题。如果您正在寻找关于这些主题的任何文章,那么本文可能适合您。
- 应用程序域的基本用法。
- 在不卸载应用程序的情况下卸载程序集。
- 在运行时加载程序集。
- 程序集的运行时版本控制。
- 使用反射从程序集中调用任何成员。
- 跨应用程序域通信。
- 第二个应用程序域的加载和卸载。
- 在不锁定 Windows 的程序集文件的情况下使用 .NET 程序集。
- 一个基本的结构化编码流程示例。
运行应用程序
为了确保应用程序正常运行,请使用“重新生成解决方案”选项编译解决方案。然后运行应用程序。
默认情况下,将加载程序集版本一。您应该能在主窗口中看到当前程序集、当前应用程序域、主应用程序域等信息。单击“反转”按钮后,文本中的值将被反转。但由于存在 bug,每次长度都会缩短。单击“获取值”按钮,我们可以将程序集版本获取为1。
现在点击单选按钮“加载程序集 V 2.0”来加载最终版本。您应该能在相应的单选按钮中看到错误提供者的消息。
现在应用程序已加载了第二个版本的程序集,该版本已修复了 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.cs、AssemblyCore.cs、AppDomainCore.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
类抽象了上述两个类。它利用了 AppdomainCore
和 AssemblyCore
类。我们还需要 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 应用程序中以使其工作。为此,我们要做的第一件事是添加对 Proxy 和 BaseInterface 项目的引用。将此代码添加到 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;
在 radioAssembly1
的 Click
事件中,粘贴以下代码
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");
然后在 radioAssembly2
的 Click
事件中,粘贴以下代码
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");
上述语句创建了代理的一个新实例,该实例内部销毁了现有的应用程序域,用新文件替换了程序集文件,并将其加载到应用程序中。为了利用代理中实现的业务功能,我们必须在按钮的点击事件中使用。
在 btnCalculate
的 Click
事件中,添加以下代码
txtReverseData.Text = DefaultProxy.ReverseValue(txtReverseData.Text);
在 btnGetValue
的 Click
事件中,添加以下代码
txtReturnedData.Text = DefaultProxy.ReturnBaseValue();
未来计划
由于我目前正在使用 .NET 3.0 和 Composite UI Application Block,我正试图在 CAB 应用程序中实现此功能。我已经创建了一个示例,可以在单击按钮或任何 UI 事件时加载 UI 模块。但是 .NET 2.0 应用程序存在同样的问题。我现在正在寻找一种方法来卸载从 CAB 应用程序中动态加载的模块。这可能需要一些时间,因为我正忙于许多事情,但您可以期待一篇有关此更新的文章。