我想我终于明白了。它最终变成了几件事——我需要将共享的 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.