0

我很难理解我在这里读到的关于使用 AppDomains 卸载插件 DLL 的一些答案。这是我的架构:

在我的解决方案中,我有一个SharedObjects项目包含一个ModuleBase所有插件(解决方案中的单独项目)都继承的类。在SharedObjects项目中,我还有一个所有插件都实现的接口(所以如果我有六个插件,它们都实现相同的接口,因此使用这些插件的主程序不需要知道甚至关心插件类的名称是什么是在编译的时候;它们都实现了相同的接口,因此公开了相同的信息)。每个插件项目都有一个对该项目的项目引用SharedObjects。(作为旁注,可能很重要,可能不重要 - 该SharedObjects项目具有对另一个解决方案的项目引用,CompanyObjects包含许多常用的类、类型、对象等)当一切都说完了,当任何给定的插件编译时,输出目录包含以下 DLL:

  1. 插件本身的编译DLL
  2. SharedObjects项目中的 DLL
  3. CompanyObjects项目中的 DLL
  4. CompanyObjects项目中引用的四个必备的第 3 方 DLL

我的主程序创建了对我正在执行所有与插件相关的工作的类的引用(该类 ,PluginHelpers存储在SharedObjects项目中)。该程序提供了一个 OpenFileDialog,以便用户可以选择一个 DLL 文件。现在,由于它正在运行,我可以只将插件 DLL 移动到单独的文件夹并使用Assembly.LoadFrom(PathToDLL)语句加载它们。他们加载没有错误;我检查以确保他们在SharedObjects项目中实现接口,收集一些基本信息,并在插件 DLL 本身中初始化一些后台工作,以便接口有一些东西可以暴露。问题是,如果不先退出主程序,我就无法升级这些 DLL,因为一旦我使用LoadFrom这些 DLL,就会被锁定。

这个 MSDN 站点,我找到了锁定 DLL 问题的解决方案。但是使用适用于 OP 的代码,我得到了与 OP 相同的“找不到文件或依赖项”错误。当我从包含其余 DLL 的发布文件夹中打开 DLL 时,我什至得到了错误。

FusionLog 更令人困惑:没有提到我试图打开的路径;它试图查看我从中调试主程序的目录,这是一个与插件完全不同的路径上的完全不同的项目,它正在寻找的文件是 DLL 的名称,但在文件夹中程序正在运行。在这一点上,我不知道为什么它会忽略我给它的路径并在完全不同的文件夹中寻找 DLL。

作为参考,这是我的Loader类和我用来(尝试)加载 DLL 的代码:

Private Class Loader
    Inherits MarshalByRefObject

    Private _assembly As [Assembly]
    Public ReadOnly Property TheAssembly As [Assembly]
        Get
            Return _assembly
        End Get
    End Property

    Public Overrides Function InitializeLifetimeService() As Object
        Return Nothing
    End Function

    Public Sub LoadAssembly(ByVal path As String)
        _assembly = Assembly.Load(AssemblyName.GetAssemblyName(path))
    End Sub

    Public Function GetAssembly(ByVal path As String) As Assembly
        Return Assembly.Load(AssemblyName.GetAssemblyName(path))    'this doesn't throw an error
    End Function
End Class

Public Sub Add2(ByVal PathToDll As String)
    Dim ad As AppDomain = AppDomain.CreateDomain("TempPluginDomain")
    Dim l As Loader = ad.CreateInstanceAndUnwrap(
        GetType(Loader).Assembly.FullName,
        GetType(Loader).FullName
    )
    Dim theDll As Assembly = l.GetAssembly(PathToDll)    'error happens here
    'there's another way to do it that makes the exact point of the error clear:
    'Dim theDll As Assembly = Nothing
    'l.LoadAssembly(PathToDll)    'No problems here. The _assembly variable is successfully set
    'theDll = l.TheAssembly       'Here's where the error occurs, as soon as you try to read that _assembly variable.
    AppDomain.Unload(ad)
End Sub

谁能指出我正确的方向,这样我就可以只根据需要加载和卸载 DLL 并且没有任何依赖错误?

4

1 回答 1

1

我想我终于明白了。它最终变成了几件事——我需要将共享的 DLLs 全部放在一个地方,并且正如 Hans 上面提到的,我需要我的 appdomains 平方。我的解决方案架构如下所示:一个包含我所有插件项目的文件夹;一个“共享对象”程序集,其中一个类文件用于基本插件架构,第二个类包含我的“插件包装器”类和支持类;以及将所有内容联系在一起的控制台应用程序。每个插件项目都有一个对共享对象项目的项目引用,控制台应用程序也是如此。没有任何东西直接引用插件。

So in my Shared Objects project, I have the code for my PluginBase class and my IPlugin interface:

Public Interface IPlugin
    ReadOnly Property Result As Integer
    Sub Calculate(ByVal param1 As Integer, ByVal param2 As Integer)
End Interface

Public MustInherit Class PluginBase
    Inherits MarshalByRefObject

    'None of this is necessary for the example to work, but I know I'll need to use an inherited base class later on so I threw it into the example now.

    Protected ReadOnly Property PluginName As String
        Get
            Return CustomAttributes("AssemblyPluginNameAttribute")
        End Get
    End Property

    Protected ReadOnly Property PluginGUID As String
        Get
            Return CustomAttributes("AssemblyPluginGUIDAttribute")
        End Get
    End Property

    Protected IsInitialized As Boolean = False
    Protected CustomAttributes As Dictionary(Of String, String)

    Protected Sub Initialize()
        CustomAttributes = New Dictionary(Of String, String)
        Dim attribs = Me.GetType.Assembly.GetCustomAttributesData
        For Each attrib In attribs
            Dim name As String = attrib.Constructor.DeclaringType.Name
            Dim value As String
            If attrib.ConstructorArguments.Count = 0 Then
                value = ""
            Else
                value = attrib.ConstructorArguments(0).ToString.Replace("""", "")
            End If
            CustomAttributes.Add(name, value)
        Next
        IsInitialized = True
    End Sub
End Class

<AttributeUsage(AttributeTargets.Assembly)>
Public Class AssemblyPluginNameAttribute
    Inherits System.Attribute

    Private _name As String

    Public Sub New(ByVal value As String)
        _name = value
    End Sub

    Public Overridable ReadOnly Property PluginName As String
        Get
            Return _name
        End Get
    End Property
End Class

<AttributeUsage(AttributeTargets.Assembly)>
Public Class AssemblyPluginGUIDAttribute
    Inherits System.Attribute

    Private _g As String

    Public Sub New(ByVal value As String)
        _g = value
    End Sub

    Public Overridable ReadOnly Property PluginGUID As String
        Get
            Return _g
        End Get
    End Property
End Class

And I have my PluginWrapper class with its supporting classes:

Imports System.IO
Imports System.Reflection

''' <summary>
''' The wrapper for plugin-related activities.
''' </summary>
''' <remarks>Each wrapper contains: the plugin; code to load and unload it from memory; and the publicly-exposed name and GUID of the plugin.</remarks>
Public Class PluginWrapper

    Private _pluginAppDomain As AppDomain = Nothing
    Private _isActive As Boolean = False
    Private _plugin As IPlugin = Nothing
    Private _pluginInfo As PluginInfo = Nothing
    Private _pluginPath As String = ""

    Public ReadOnly Property IsActive As Boolean
        Get
            Return _isActive
        End Get
    End Property

    Public ReadOnly Property PluginInterface As IPlugin
        Get
            Return _plugin
        End Get
    End Property

    Public ReadOnly Property PluginGUID As String
        Get
            Return _pluginInfo.PluginGUID
        End Get
    End Property

    Public ReadOnly Property PluginName As String
        Get
            Return _pluginInfo.PluginName
        End Get
    End Property

    Public Sub New(ByVal PathToPlugin As String)
        _pluginPath = PathToPlugin
    End Sub

    Public Sub Load()
        Dim l As New PluginLoader(_pluginPath)

        _pluginInfo = l.LoadPlugin()
        Dim setup As AppDomainSetup = New AppDomainSetup With {.ApplicationBase = System.IO.Directory.GetParent(_pluginPath).FullName}
        _pluginAppDomain = AppDomain.CreateDomain(_pluginInfo.PluginName, Nothing, setup)
        _plugin = _pluginAppDomain.CreateInstanceAndUnwrap(_pluginInfo.AssemblyName, _pluginInfo.TypeName)
        _isActive = True
    End Sub

    Public Sub Unload()
        If _isActive Then
            AppDomain.Unload(_pluginAppDomain)
            _plugin = Nothing
            _pluginAppDomain = Nothing
            _isActive = False
        End If
    End Sub

End Class

<Serializable()>
Public NotInheritable Class PluginInfo
    Private _assemblyname As String
    Public ReadOnly Property AssemblyName
        Get
            Return _assemblyname
        End Get
    End Property

    Private _typename As String
    Public ReadOnly Property TypeName
        Get
            Return _typename
        End Get
    End Property

    Private _pluginname As String
    Public ReadOnly Property PluginName As String
        Get
            Return _pluginname
        End Get
    End Property

    Private _pluginguid As String
    Public ReadOnly Property PluginGUID As String
        Get
            Return _pluginguid
        End Get
    End Property

    Public Sub New(ByVal AssemblyName As String, ByVal TypeName As String, ByVal PluginName As String, ByVal PluginGUID As String)
        _assemblyname = AssemblyName
        _typename = TypeName
        _pluginname = PluginName
        _pluginguid = PluginGUID
    End Sub
End Class

Public NotInheritable Class PluginLoader
    Inherits MarshalByRefObject

    Private _pluginBaseType As Type = Nothing
    Private _pathToPlugin As String = ""

    Public Sub New()
    End Sub

    Public Sub New(ByVal PathToPlugin As String)
        _pathToPlugin = PathToPlugin
        Dim ioAssemblyFile As String = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(_pathToPlugin), GetType(PluginBase).Assembly.GetName.Name) & ".dll")
        Dim ioAssembly As Assembly = Assembly.LoadFrom(ioAssemblyFile)
        _pluginBaseType = ioAssembly.GetType(GetType(PluginBase).FullName)
    End Sub

    Public Function LoadPlugin() As PluginInfo
        Dim domain As AppDomain = Nothing
        Try
            domain = AppDomain.CreateDomain("Discovery")
            Dim loader As PluginLoader = domain.CreateInstanceAndUnwrap(GetType(PluginLoader).Assembly.FullName, GetType(PluginLoader).FullName)
            Return loader.Load(_pathToPlugin)
        Finally
            If Not IsNothing(domain) Then
                AppDomain.Unload(domain)
            End If
        End Try
    End Function

    Private Function Load(ByVal PathToPlugin As String) As PluginInfo
        Dim r As PluginInfo = Nothing
        Try
            Dim objAssembly As Assembly = Assembly.LoadFrom(PathToPlugin)
            For Each objType As Type In objAssembly.GetTypes
                If Not ((objType.Attributes And TypeAttributes.Abstract) = TypeAttributes.Abstract) Then
                    If Not objType.GetInterface("SharedObjects.IPlugin") Is Nothing Then
                        Dim attribs = objAssembly.GetCustomAttributes(False)
                        Dim pluginGuid As String = ""
                        Dim pluginName As String = ""
                        For Each attrib In attribs
                            Dim name As String = attrib.GetType.ToString
                            If name = "SharedObjects.AssemblyPluginGUIDAttribute" Then
                                pluginGuid = CType(attrib, AssemblyPluginGUIDAttribute).PluginGUID.ToString
                            ElseIf name = "SharedObjects.AssemblyPluginNameAttribute" Then
                                pluginName = CType(attrib, AssemblyPluginNameAttribute).PluginName.ToString
                            End If

                            If (Not pluginGuid = "") And (Not pluginName = "") Then
                                Exit For
                            End If
                        Next
                        r = New PluginInfo(objAssembly.FullName, objType.FullName, pluginName, pluginGuid)
                    End If
                End If
            Next
        Catch f As FileNotFoundException
            Throw f
        Catch ex As Exception
            'ignore non-valid dlls
        End Try

        Return r
    End Function
End Class

Finally, each plugin project looks a little like this:

Imports SharedObjects

<Assembly: AssemblyPluginName("Addition Plugin")> 
<Assembly: AssemblyPluginGUID("{4EC46939-BD74-4665-A46A-C99133D8B2D2}")> 

Public Class Plugin_Addition
    Inherits SharedObjects.PluginBase
    Implements SharedObjects.IPlugin

    Private _result As Integer

#Region "Implemented"
    Public Sub Calculate(ByVal param1 As Integer, ByVal param2 As Integer) Implements SharedObjects.IPlugin.Calculate
        If Not IsInitialized Then
            MyBase.Initialize()
        End If
        _result = param1 + param2
    End Sub

    Public ReadOnly Property Result As Integer Implements SharedObjects.IPlugin.Result
        Get
            Return _result
        End Get
    End Property
#End Region

End Class

To set it all up, the main program creates a new instance of the PluginWrapper class, supplies a path to a DLL, and loads it:

Dim additionPlugin As New PluginWrapper("C:\path\to\Plugins\Plugin_Addition.dll")
additionPlugin.Load()

Once you're done doing whatever you need to do with the program...

additionPlugin.PluginInterface.Calculate(3, 2)

...and retrieving the results...

Console.WriteLine("3 + 2 = {0}", additionPlugin.PluginInterface.Result)

...just unload the plugin:

additionPlugin.Unload()

If you need to reload it while the wrapper is still in memory, just call the Load() method again - all the information it needs to create a new AppDomain and reload the assembly is in there. And, in answer to my initial question, once the Unload() method has been called, the assembly is freed and can be replaced/upgraded as necessary, which was the whole point of doing this in the first place.

Part of where I was getting tripped up earlier was that I wasn't including the SharedObjects.dll file in the same folder as the plugins. What I found is that any referenced assembly needs to be present. So in my post-build events for both my plugins and the Shared Objects project, I have this: xcopy /y $(ProjectDir)$(OutDir)$(TargetFileName) c:\path\to\Plugins. Every time I build the solution, all the DLLs are placed in the folder where they need to be.

Sorry if this is a little long, but this is a little complicated. Maybe there's a shorter way to do it...but at the moment, this gets me everything I need.

于 2012-05-16T21:41:16.363 回答