.NET Core中插件式開發實現

前言:

 之前在文章- AppDomain實現【插件式】開發 中介紹了在 .NET Framework 中,通過AppDomain實現動態載入和卸載程式集的效果。

 但是.NET Core 僅支援單個默認應用域,那麼在.NET Core中如何實現【插件式】開發呢?

一、.NET Core 中 AssemblyLoadContext的使用

 1、AssemblyLoadContext簡介:

  每個 .NET Core 應用程式均隱式使用 AssemblyLoadContext。 它是運行時的提供程式,用於定位和載入依賴項。 只要載入了依賴項,就會調用 AssemblyLoadContext 實例來定位該依賴項。

  • 它提供定位、載入和快取託管程式集和其他依賴項的服務。

  • 為了支援動態程式碼載入和卸載,它創建了一個獨立上下文,用於在其自己的 AssemblyLoadContext 實例中載入程式碼及其依賴項。 

 2、AssemblyLoadContext和AppDomain卸載差異:

  使用 AssemblyLoadContext 和使用 AppDomain 進行卸載之間存在一個值得注意的差異。 對於 Appdomain,卸載為強制執行。

  卸載時,會中止目標 AppDomain 中運行的所有執行緒,會銷毀目標 AppDomain 中創建的託管 COM 對象,等等。 對於 AssemblyLoadContext,卸載是「協作式的」。

  調用 AssemblyLoadContext.Unload 方法只是為了啟動卸載。以下目標達成後,卸載完成:

  • 沒有執行緒將程式集中的方法載入到其調用堆棧上的 AssemblyLoadContext 中。
  • 程式集中的任何類型都不會載入到 AssemblyLoadContext,這些類型的實例本身由以下引用:
    • AssemblyLoadContext 外部的引用,弱引用(WeakReference 或 WeakReference<T>)除外。
    • AssemblyLoadContext 內部和外部的強垃圾回收器 (GC) 句柄(GCHandleType.Normal 或 GCHandleType.Pinned)。  

二、.NET Core 插件式方式實現

 1、創建可卸載的上下文PluginAssemblyLoadContext

class PluginAssemblyLoadContext : AssemblyLoadContext
{
    private AssemblyDependencyResolver _resolver;

    /// <summary>
    /// 構造函數
    /// isCollectible: true 重點,允許Unload
    /// </summary>
    /// <param name="pluginPath"></param>
    public PluginAssemblyLoadContext(string pluginPath) : base(isCollectible: true)
    {
        _resolver = new AssemblyDependencyResolver(pluginPath);
    }

    protected override Assembly Load(AssemblyName assemblyName)
    {
        string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
        if (assemblyPath != null)
        {
            return LoadFromAssemblyPath(assemblyPath);
        }
        return null;
    }

    protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
    {
        string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
        if (libraryPath != null)
        {
            return LoadUnmanagedDllFromPath(libraryPath);
        }
        return IntPtr.Zero;
    }
}

 2、創建插件介面及實現

  整體項目結構為:

  

  a)添加項目PluginInterface,插件介面:

public interface IPlugin
{
    string Name { get; }
    string Description { get; }
    string Execute(object inPars);
}

  b)添加HelloPlugin項目,實現不引用外部dll插件

public class HelloPlugin : PluginInterface.IPlugin
{
    public string Name => "HelloPlugin";
    public string Description { get => "Displays hello message."; }
    public string Execute(object inPars)
    {return ("Hello !!!" + inPars?.ToString()); 
   }
}

  c)添加JsonPlugin項目,實現引用三方dll插件

public class JsonPlugin : PluginInterface.IPlugin
{
    public string Name => "JsonPlugin";
    public string Description => "Outputs JSON value.";
    private struct Info
    {
        public string JsonVersion;
        public string JsonLocation;
        public string Machine;
        public DateTime Date;
    }
    public string Execute(object inPars)
    {
        Assembly jsonAssembly = typeof(JsonConvert).Assembly;
        Info info = new Info()
        {
            JsonVersion = jsonAssembly.FullName,
            JsonLocation = jsonAssembly.Location,
            Machine = Environment.MachineName,
            Date = DateTime.Now
        };
        return JsonConvert.SerializeObject(info, Formatting.Indented);
    }
}

  d)添加PluginsApp項目,實現調用插件方法:

  修改窗體介面布局:

   

  添加執行方法

/// <summary>
/// 將此方法標記為noinline很重要,否則JIT可能會決定將其內聯到Main方法中。
/// 這可能會阻止成功卸載插件,因為某些實例的生存期可能會延長到預期卸載插件的時間點之外。
/// </summary>
/// <param name="assemblyPath"></param>
/// <param name="inPars"></param>
/// <param name="alcWeakRef"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.NoInlining)]
static string ExecuteAndUnload(string assemblyPath, object inPars, out WeakReference alcWeakRef)
{
    string resultString = string.Empty;
    // 創建 PluginLoadContext對象
    var alc = new PluginAssemblyLoadContext(assemblyPath);

    //創建一個對AssemblyLoadContext的弱引用,允許我們檢測卸載何時完成
    alcWeakRef = new WeakReference(alc);

    // 載入程式到上下文
    // 注意:路徑必須為絕對路徑.
    Assembly assembly = alc.LoadFromAssemblyPath(assemblyPath);

    //創建插件對象並調用
    foreach (Type type in assembly.GetTypes())
    {
        if (typeof(IPlugin).IsAssignableFrom(type))
        {
            IPlugin result = Activator.CreateInstance(type) as IPlugin;
            if (result != null)
            {
                resultString = result.Execute(inPars);
                break;
            }
        }
    }
    //卸載程式集上下文
    alc.Unload();
    return resultString;
}

三、效果驗證

 1、非引用外部dll的插件執行:執行後對應dll成功卸載,程式集數量未增加。

  

  2、引用外部包的插件:執行後對應dll未卸載,程式集數量增加。

   

   通過監視查看對象狀態:該上下文在卸載中。暫未找到原因卸載失敗(疑問?)

  

 四、總結:

 雖然微軟文檔說.Net Core中使用AssemblyLoadContext來實現程式集的載入及卸載實現,但通過驗證在載入引用外部dll後,載入後不能正常卸載。或者使用方式還不正確。

 源碼地址

 參考://docs.microsoft.com/zh-cn/dotnet/standard/assembly/unloadability