.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后,加載后不能正常卸載。或者使用方式還不正確。

 源碼地址

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

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM