.NET Core中外掛式開發實現

chaney1992發表於2021-05-30

前言:

 之前在文章- 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 

 

 

相關文章