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

在不锁定文件的情况下加载和卸载插件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (25投票s)

2015年3月24日

CPOL

4分钟阅读

viewsIcon

30385

downloadIcon

938

本文介绍了一种解决方案,该方案允许应用程序加载和执行插件,并在不锁定程序集文件的情况下卸载它。

引言

使用插件扩展应用程序的逻辑是代码抽象的一个很好的例子。插件接口描述了功能,而实现与应用程序是解耦的,除了插件接口本身。此外,插件在运行时加载和卸载是一个强大的功能。我在互联网上找到的许多文章都描述了如何将插件加载到临时的 AppDomain 中。但大多数情况下,插件实际上仍保留在应用程序的 AppDomain 中,因此无法卸载,导致硬盘上的插件程序集被文件锁定。本文介绍了一种方法,可以在卸载插件后,在不锁定插件文件的情况下使用和释放插件。

背景

最近,我参与了一个项目,该项目使用插件框架来更新另一个系统。应用程序会检查是否有更新的插件版本,并在可用时下载它们。下载完成后,插件将被执行以更新正在运行的系统。该系统的一个缺点是,为了卸载旧插件以使用最新版本,它必须重新启动自身。

问题的根源在于为插件创建了一个临时的 AppDomain,但在加载它时,它仍然与应用程序的 AppDomain 关联。尽管系统为插件使用了单独的 AppDomain,但在卸载它时,已加载的插件仍然锁定驱动器上的程序集,并且无法删除,因为它实际上已加载到应用程序的 AppDomain 中。删除旧版本的插件是系统要求之一,以确保系统使用最新版本。这种意外的文件锁定引起了对 AppDomain 使用不当的怀疑。

我在这个问题here的答案中找到了解决我的问题的方法

https://stackoverflow.com/questions/425077/how-to-delete-the-pluginassembly-after-appdomain-unloaddomain/2475177#2475177.

使用代码

示例应用程序将从指定文件夹加载和执行插件。插件在为给定文件夹中找到的每个插件单独创建的临时 AppDomain 中加载和执行。插件的加载和执行在单独的插件 AppDomain 上下文中完成。执行完毕后,插件将从硬盘删除。这样做是为了表明在临时 AppDomain 卸载后,应用程序不会对程序集产生文件锁定。

实现

以下各节将描述示例代码的实现

  • 类图:UML概述,描述了包(程序集)、类及其关系
  • PluginBase:基础程序集,包含插件接口描述
  • PlugIn1:示例插件实现
  • Application:包含插件加载和执行例程

类图

该示例包含一个控制台应用程序、插件接口和一个示例插件实现。以下类图显示了接口和类在其程序集中的排列。

PluginBase

PluginBase 程序集包含插件的接口定义。

public interface IPlugin
{
    void Activate();
    void Execute();
    void Deactivate();
}

Plugin1

一个演示插件,通过显示消息框来展示方法调用。

using System.Windows.Forms;
using PluginBase;

public class Plugin1 : IPlugin
{
    public void Activate()
    {
        MessageBox.Show("Activating Plugin 1");
    }

    public void Execute()
    {
        MessageBox.Show("Peforming Action Plugin 1");
    }

    public void Deactivate()
    {
        MessageBox.Show("Deactivating Plugin 1");
    }
}

PluginContext

插件上下文类,用作跨 AppDomain 数据传输的示例。它包含插件的文件路径(从何处加载)以及 CanDeletePlugin 标志,以返回插件是否可以从硬盘删除的状态。请注意,类必须是可序列化的,以便可以在 AppDomain 边界之间交换。

using System;

[Serializable]
public class PluginContext
{
    public string FilePath { get; set; }

    public bool CanDeletePlugin { get; set; }
}

Application

测试应用程序包含两个方法

  1. Main():扫描插件目录并为找到的每个插件创建一个新的临时 AppDomain
  2. PluginCallback():临时 AppDomain 的回调方法,在其中加载和执行插件
public class Program
{
    static void Main()
    {
        try
        {
            // Iterate through all plug-ins.
            foreach (var filePath in Directory.GetFiles(Constants.PluginPath,
                                                        Constants.PluginSearchPattern))
            {
                // Create the plug-in AppDomain setup.
                var pluginAppDomainSetup = new AppDomainSetup();
                pluginAppDomainSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;

                // Create the plug-in AppDomain with the setup.
                var plugInAppDomain = AppDomain.CreateDomain(filePath, null, pluginAppDomainSetup);

                // Pass the plug-in file path to the AppDomain
                var pluginContext = new PluginContext { FilePath = filePath };
                plugInAppDomain.SetData(Constants.PluginContextKey, pluginContext);

                // Execute the loader in the plug-in AppDomain's context.
                // This will also execute the plug-in.
                plugInAppDomain.DoCallBack(PluginCallback);

                // Retrieve the flag if the plug-in has executed and can be deleted.
                pluginContext = plugInAppDomain.GetData(Constants.PluginContextKey) as PluginContext;

                // Unload the plug-in AppDomain.
                AppDomain.Unload(plugInAppDomain);

                // Delete the plug-in if applicable.
                if (pluginContext != null && pluginContext.CanDeletePlugin)
                {
                    File.Delete(filePath);
                }
            }
        }
        catch (Exception exception)
        {
            Console.WriteLine(exception);
        }
    }

    /// <summary>
    /// The callback routine that is executed in the plug-in AppDomain context.
    /// </summary>
    private static void PluginCallback()
    {
        try
        {
            // Retrieve the filePath from the plug-in AppDomain
            var pluginContext = AppDomain.CurrentDomain.GetData(Constants.PluginContextKey)
                                  as PluginContext;
            if (pluginContext != null)
            {
                // Load the plug-in.
                var pluginAssembly = Assembly.LoadFrom(pluginContext.FilePath);

                // Iterate through types of the plug-in assembly to find the plug-in class.
                foreach (var type in pluginAssembly.GetTypes())
                {
                    if (type.IsClass && typeof(IPlugin).IsAssignableFrom(type))
                    {
                        // Create the instance of the plug-in and call the interface methods.
                        var plugin = Activator.CreateInstance(type) as IPlugin;
                        if (plugin != null)
                        {
                            plugin.Activate();
                            plugin.Execute();
                            plugin.Deactivate();

                            // Set the delete flag to true, to signal that the plug-in can be deleted.
                            pluginContext.CanDeletePlugin = true;
                            AppDomain.CurrentDomain.SetData(Constants.DeletePluginKey, pluginContext);
                            break;
                        }
                    }
                }
            }
        }
        catch (Exception exception)
        {
            Console.WriteLine(exception);
        }
    }
}

关注点

AppDomain.DoCallback(CrossAppDomainDelegate)

AppDomain.DoCallback() 方法是魔法发生的地方。它将在临时插件 AppDomain 上下文中调用分配的回调例程。

插件实例化和执行

在临时 AppDomain 上下文中加载插件程序集后,将搜索插件类类型。找到后,将使用 Activator 类创建插件的新实例。成功实例化后,将调用插件的方法。最后,通过调用 SetData() 设置 CanDeletePlugin 标志并将其返回给主应用程序。

AppDomain.SetData(string, object), AppDomain.GetData(string)

由于插件在单独的 AppDomain 中运行,因此必须使用 AppDomain 的 SetData() 和 GetData() 方法进行跨 AppDomain 通信。在此示例中,我使用 PluginContext 类来显示插件在另一个 AppDomain 中执行,并且 PluginContext 类必须是可序列化的,以便它可以从一个域发送到另一个域。

失败的实现

为了完整起见:下一个代码部分描述了失败的实现。在我对互联网的研究中,我多次发现了这种类型的实现。请注意,插件部分是有效的!插件被加载到内存中,位于其自己的 AppDomain 中,使用后,临时 AppDomain 将被卸载。但是文件删除失败,因为插件实际上已加载到应用程序的域中。

public class Program
{
    static void Main()
    {
        try
        {
            // Iterate through all plug-ins.
            foreach (var filePath in Directory.GetFiles(Constants.PluginPath,
                                                        Constants.PluginSearchPattern))
            {
                // Create the plug-in AppDomain setup.
                var pluginAppDomainSetup = new AppDomainSetup();
                pluginAppDomainSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;

                // Create the plug-in AppDomain with the setup.
                var plugInAppDomain = AppDomain.CreateDomain(filePath, null, pluginAppDomainSetup);
                AppDomain.CurrentDomain.AssemblyResolve += CurrentDomainAssemblyResolve;

                byte[] fileContent;
                using (FileStream dll = File.OpenRead(filePath))
                {
                    fileContent = new byte[dll.Length];
                    dll.Read(fileContent, 0, (int)dll.Length);
                }

                var pluginAssembly = plugInAppDomain.Load(fileContent);
                bool canDelete = false;

                // Iterate through types of the plug-in assembly to find the plug-in class.
                foreach (var type in pluginAssembly.GetTypes())
                {
                    if (type.IsClass && typeof(IPlugin).IsAssignableFrom(type))
                    {
                        // Create the instance of the plug-in and call the interface methods.
                        var plugin = Activator.CreateInstance(type) as IPlugin;
                        if (plugin != null)
                        {
                            plugin.Activate();
                            plugin.Execute();
                            plugin.Deactivate();
                            canDelete = true;
                            break;
                        }
                    }
                }

                // Unload the plug-in AppDomain.
                AppDomain.Unload(plugInAppDomain);

                // Delete the plug-in if applicable.
                if (canDelete)
                {
                    File.Delete(filePath);
                }
            }
        }
        catch (Exception exception)
        {
            Console.WriteLine(exception);
        }
    }

    private static Assembly CurrentDomainAssemblyResolve(object sender, ResolveEventArgs args)
    {
       // Quick and dirty
       string fileName = args.Name.Split(new[] { ',' })[0];
       fileName = Path.ChangeExtension(fileName, "DLL");
       fileName = Path.Combine(Constants.PluginPath, fileName);
       fileName = Path.GetFullPath(fileName);
       return Assembly.LoadFile(fileName);
    }
}

结论

此示例展示了一种方法,可以使用临时 AppDomain 加载和执行插件,在卸载临时 AppDomain 后不对程序集保持文件锁定。

我希望本文能为您提供一个关于如何使用临时 AppDomain 的完整场景。

© . All rights reserved.