使用 .NET Core 3.0 的 AssemblyLoadContext 實現插件熱載入

来源:https://www.cnblogs.com/zkweb/archive/2019/10/07/11630228.html
-Advertisement-
Play Games

一般情況下,一個 .NET 程式集載入到程式中以後,它的類型信息以及原生代碼等數據會一直保留在記憶體中,.NET 運行時無法回收它們,如果我們要實現插件熱載入 (例如 Razor 或 Aspx 模版的熱更新) 則會造成記憶體泄漏。在以往,我們可以使用 .NET Framework 的 AppDomain ...


一般情況下,一個 .NET 程式集載入到程式中以後,它的類型信息以及原生代碼等數據會一直保留在記憶體中,.NET 運行時無法回收它們,如果我們要實現插件熱載入 (例如 Razor 或 Aspx 模版的熱更新) 則會造成記憶體泄漏。在以往,我們可以使用 .NET Framework 的 AppDomain 機制,或者使用解釋器 (有一定的性能損失),或者在編譯一定次數以後重啟程式 (Asp.NET 的 numRecompilesBeforeAppRestart) 來避免記憶體泄漏。

因為 .NET Core 不像 .NET Framework 一樣支持動態創建與卸載 AppDomain,所以一直都沒有好的方法實現插件熱載入,好消息是,.NET Core 從 3.0 開始支持了可回收程式集 (Collectible Assembly),我們可以創建一個可回收的 AssemblyLoadContext,用它來載入與卸載程式集。關於 AssemblyLoadContext 的介紹與實現原理可以參考 yoyofx 的文章我的文章

本文會通過一個 180 行左右的示常式序,介紹如何使用 .NET Core 3.0 的 AssemblyLoadContext 實現插件熱載入,程式同時使用了 Roslyn 實現動態編譯,最終效果是改動插件代碼後可以自動更新到正在運行的程式當中,並且不會造成記憶體泄漏。

完整源代碼與文件夾結構

首先我們來看看完整源代碼與文件夾結構,源代碼分為兩部分,一部分是宿主,負責編譯與載入插件,另一部分則是插件,後面會對源代碼的各個部分作出詳細講解。

文件夾結構:

  • pluginexample (頂級文件夾)
    • host (宿主的項目)
      • Program.cs (宿主的代碼)
      • host.csproj (宿主的項目文件)
    • guest (插件的代碼文件夾)
      • Plugin.cs (插件的代碼)
      • bin (保存插件編譯結果的文件夾)
        • MyPlugin.dll (插件編譯後的 DLL 文件)

Program.cs 的內容:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading;

namespace Common
{
    public interface IPlugin : IDisposable
    {
        string GetMessage();
    }
}

namespace Host
{
    using Common;

    internal class PluginController : IPlugin
    {
        private List<Assembly> _defaultAssemblies;
        private AssemblyLoadContext _context;
        private string _pluginName;
        private string _pluginDirectory;
        private volatile IPlugin _instance;
        private volatile bool _changed;
        private object _reloadLock;
        private FileSystemWatcher _watcher;

        public PluginController(string pluginName, string pluginDirectory)
        {
            _defaultAssemblies = AssemblyLoadContext.Default.Assemblies
                .Where(assembly => !assembly.IsDynamic)
                .ToList();
            _pluginName = pluginName;
            _pluginDirectory = pluginDirectory;
            _reloadLock = new object();
            ListenFileChanges();
        }

        private void ListenFileChanges()
        {
            Action<string> onFileChanged = path =>
            {
                if (Path.GetExtension(path).ToLower() == ".cs")
                    _changed = true;
            };
            _watcher = new FileSystemWatcher();
            _watcher.Path = _pluginDirectory;
            _watcher.IncludeSubdirectories = true;
            _watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName;
            _watcher.Changed += (sender, e) => onFileChanged(e.FullPath);
            _watcher.Created += (sender, e) => onFileChanged(e.FullPath);
            _watcher.Deleted += (sender, e) => onFileChanged(e.FullPath);
            _watcher.Renamed += (sender, e) => { onFileChanged(e.FullPath); onFileChanged(e.OldFullPath); };
            _watcher.EnableRaisingEvents = true;
        }

        private void UnloadPlugin()
        {
            _instance?.Dispose();
            _instance = null;
            _context?.Unload();
            _context = null;
        }

        private Assembly CompilePlugin()
        {
            var binDirectory = Path.Combine(_pluginDirectory, "bin");
            var dllPath = Path.Combine(binDirectory, $"{_pluginName}.dll");
            if (!Directory.Exists(binDirectory))
                Directory.CreateDirectory(binDirectory);
            if (File.Exists(dllPath))
            {
                File.Delete($"{dllPath}.old");
                File.Move(dllPath, $"{dllPath}.old");
            }

            var sourceFiles = Directory.EnumerateFiles(
                _pluginDirectory, "*.cs", SearchOption.AllDirectories);
            var compilationOptions = new CSharpCompilationOptions(
                OutputKind.DynamicallyLinkedLibrary,
                optimizationLevel: OptimizationLevel.Debug);
            var references = _defaultAssemblies
                .Select(assembly => assembly.Location)
                .Where(path => !string.IsNullOrEmpty(path) && File.Exists(path))
                .Select(path => MetadataReference.CreateFromFile(path))
                .ToList();
            var syntaxTrees = sourceFiles
                .Select(p => CSharpSyntaxTree.ParseText(File.ReadAllText(p)))
                .ToList();
            var compilation = CSharpCompilation.Create(_pluginName)
                .WithOptions(compilationOptions)
                .AddReferences(references)
                .AddSyntaxTrees(syntaxTrees);

            var emitResult = compilation.Emit(dllPath);
            if (!emitResult.Success)
            {
                throw new InvalidOperationException(string.Join("\r\n",
                    emitResult.Diagnostics.Where(d => d.WarningLevel == 0)));
            }
            //return _context.LoadFromAssemblyPath(Path.GetFullPath(dllPath));
            using (var stream = File.OpenRead(dllPath))
            {
                var assembly = _context.LoadFromStream(stream);
                return assembly;
            }
        }

        private IPlugin GetInstance()
        {
            var instance = _instance;
            if (instance != null && !_changed)
                return instance;

            lock (_reloadLock)
            {
                instance = _instance;
                if (instance != null && !_changed)
                    return instance;

                UnloadPlugin();
                _context = new AssemblyLoadContext(
                    name: $"Plugin-{_pluginName}", isCollectible: true);

                var assembly = CompilePlugin();
                var pluginType = assembly.GetTypes()
                    .First(t => typeof(IPlugin).IsAssignableFrom(t));
                instance = (IPlugin)Activator.CreateInstance(pluginType);

                _instance = instance;
                _changed = false;
            }

            return instance;
        }

        public string GetMessage()
        {
            return GetInstance().GetMessage();
        }

        public void Dispose()
        {
            UnloadPlugin();
            _watcher?.Dispose();
            _watcher = null;
        }
    }

    internal class Program
    {
        static void Main(string[] args)
        {
            using (var controller = new PluginController("MyPlugin", "../guest"))
            {
                bool keepRunning = true;
                Console.CancelKeyPress += (sender, e) => {
                    e.Cancel = true;
                    keepRunning = false;
                };
                while (keepRunning)
                {
                    try
                    {
                        Console.WriteLine(controller.GetMessage());
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine($"{ex.GetType()}: {ex.Message}");
                    }
                    Thread.Sleep(1000);
                }
            }
        }
    }
}

host.csproj 的內容:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.3.1" />
  </ItemGroup>

</Project>

Plugin.cs 的內容:

using System;
using Common;

namespace Guest
{
    public class MyPlugin : IPlugin
    {
        public MyPlugin()
        {
            Console.WriteLine("MyPlugin loaded");
        }

        public string GetMessage()
        {
            return "Hello 1";
        }

        public void Dispose()
        {
            Console.WriteLine("MyPlugin unloaded");
        }
    }
}

運行示常式序

進入 pluginexample/host 下運行 dotnet run 即可啟動宿主程式,這時宿主程式會自動編譯與載入插件,檢測插件文件的變化併在變化時重新編譯載入。你可以在運行後修改 pluginexample/guest/Plugin.cs 中的 Hello 1Hello 2,之後可以看到類似以下的輸出:

MyPlugin loaded
Hello 1
Hello 1
Hello 1
MyPlugin unloaded
MyPlugin loaded
Hello 2
Hello 2

我們可以看到程式自動更新並執行修改以後的代碼,如果你有興趣還可以測試插件代碼語法錯誤時會出現什麼。

源代碼講解

接下來是對宿主的源代碼中各個部分的詳細講解:

IPlugin 介面

public interface IPlugin : IDisposable
{
    string GetMessage();
}

這是插件項目需要的實現介面,宿主項目在編譯插件後會尋找程式集中實現 IPlugin 的類型,創建這個類型的實例並且使用它,創建插件時會調用構造函數,卸載插件時會調用 Dispose 方法。如果你用過 .NET Framework 的 AppDomain 機制可能會想是否需要 Marshalling 處理,答案是不需要,.NET Core 的可回收程式集會載入到當前的 AppDomain 中,回收時需要依賴 GC 清理,好處是使用簡單並且運行效率高,壞處是 GC 清理有延遲,只要有一個插件中類型的實例沒有被回收則插件程式集使用的數據會一直殘留,導致記憶體泄漏。

PluginController 類型

internal class PluginController : IPlugin
{
    private List<Assembly> _defaultAssemblies;
    private AssemblyLoadContext _context;
    private string _pluginName;
    private string _pluginDirectory;
    private volatile IPlugin _instance;
    private volatile bool _changed;
    private object _reloadLock;
    private FileSystemWatcher _watcher;

這是管理插件的代理類,在內部它負責編譯與載入插件,並且把對 IPlugin 介面的方法調用轉發到插件的實現中。類成員包括預設 AssemblyLoadContext 中的程式集列表 _defaultAssemblies,用於載入插件的自定義 AssemblyLoadContext _context,插件名稱與文件夾,插件實現 _instance,標記插件文件是否已改變的 _changed,防止多個線程同時編譯載入插件的 _reloadLock,與監測插件文件變化的 _watcher

PluginController 的構造函數

public PluginController(string pluginName, string pluginDirectory)
{
    _defaultAssemblies = AssemblyLoadContext.Default.Assemblies
        .Where(assembly => !assembly.IsDynamic)
        .ToList();
    _pluginName = pluginName;
    _pluginDirectory = pluginDirectory;
    _reloadLock = new object();
    ListenFileChanges();
}

構造函數會從 AssemblyLoadContext.Default.Assemblies 中獲取預設 AssemblyLoadContext 中的程式集列表,包括宿主程式集、System.Runtime 等,這個列表會在 Roslyn 編譯插件時使用,表示插件編譯時需要引用哪些程式集。之後還會調用 ListenFileChanges 監聽插件文件是否有改變。

PluginController.ListenFileChanges

private void ListenFileChanges()
{
    Action<string> onFileChanged = path =>
    {
        if (Path.GetExtension(path).ToLower() == ".cs")
            _changed = true;
    };
    _watcher = new FileSystemWatcher();
    _watcher.Path = _pluginDirectory;
    _watcher.IncludeSubdirectories = true;
    _watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName;
    _watcher.Changed += (sender, e) => onFileChanged(e.FullPath);
    _watcher.Created += (sender, e) => onFileChanged(e.FullPath);
    _watcher.Deleted += (sender, e) => onFileChanged(e.FullPath);
    _watcher.Renamed += (sender, e) => { onFileChanged(e.FullPath); onFileChanged(e.OldFullPath); };
    _watcher.EnableRaisingEvents = true;
}

這個方法創建了 FileSystemWatcher,監聽插件文件夾下的文件是否有改變,如果有改變並且改變的是 C# 源代碼 (.cs 擴展名) 則設置 _changed 成員為 true,這個成員標記插件文件已改變,下次訪問插件實例的時候會觸發重新載入。

你可能會有疑問,為什麼不在文件改變後立刻觸發重新載入插件,一個原因是部分文件編輯器的保存文件實現可能會導致改變的事件連續觸發幾次,延遲觸發可以避免編譯多次,另一個原因是編譯過程中出現的異常可以傳遞到訪問插件實例的線程中,方便除錯與調試 (儘管使用 ExceptionDispatchInfo 也可以做到)。

PluginController.UnloadPlugin

private void UnloadPlugin()
{
    _instance?.Dispose();
    _instance = null;
    _context?.Unload();
    _context = null;
}

這個方法會卸載已載入的插件,首先調用 IPlugin.Dispose 通知插件正在卸載,如果插件創建了新的線程可以在 Dispose 方法中停止線程避免泄漏,然後調用 AssemblyLoadContext.Unload 允許 .NET Core 運行時卸載這個上下文載入的程式集,程式集的數據會在 GC 檢測到所有類型的實例都被回收後回收 (參考文章開頭的鏈接)。

PluginController.CompilePlugin

private Assembly CompilePlugin()
{
    var binDirectory = Path.Combine(_pluginDirectory, "bin");
    var dllPath = Path.Combine(binDirectory, $"{_pluginName}.dll");
    if (!Directory.Exists(binDirectory))
        Directory.CreateDirectory(binDirectory);
    if (File.Exists(dllPath))
    {
        File.Delete($"{dllPath}.old");
        File.Move(dllPath, $"{dllPath}.old");
    }

    var sourceFiles = Directory.EnumerateFiles(
        _pluginDirectory, "*.cs", SearchOption.AllDirectories);
    var compilationOptions = new CSharpCompilationOptions(
        OutputKind.DynamicallyLinkedLibrary,
        optimizationLevel: OptimizationLevel.Debug);
    var references = _defaultAssemblies
        .Select(assembly => assembly.Location)
        .Where(path => !string.IsNullOrEmpty(path) && File.Exists(path))
        .Select(path => MetadataReference.CreateFromFile(path))
        .ToList();
    var syntaxTrees = sourceFiles
        .Select(p => CSharpSyntaxTree.ParseText(File.ReadAllText(p)))
        .ToList();
    var compilation = CSharpCompilation.Create(_pluginName)
        .WithOptions(compilationOptions)
        .AddReferences(references)
        .AddSyntaxTrees(syntaxTrees);

    var emitResult = compilation.Emit(dllPath);
    if (!emitResult.Success)
    {
        throw new InvalidOperationException(string.Join("\r\n",
            emitResult.Diagnostics.Where(d => d.WarningLevel == 0)));
    }
    //return _context.LoadFromAssemblyPath(Path.GetFullPath(dllPath));
    using (var stream = File.OpenRead(dllPath))
    {
        var assembly = _context.LoadFromStream(stream);
        return assembly;
    }
}

這個方法會調用 Roslyn 編譯插件代碼到 DLL,並使用自定義的 AssemblyLoadContext 載入編譯後的 DLL。首先它需要刪除原有的 DLL 文件,因為卸載程式集有延遲,原有的 DLL 文件在 Windows 系統上很可能會刪除失敗並提示正在使用,所以需要先重命名併在下次刪除。接下來它會查找插件文件夾下的所有 C# 源代碼,用 CSharpSyntaxTree 解析它們,並用 CSharpCompilation 編譯,編譯時引用的程式集列表是構造函數中取得的預設 AssemblyLoadContext 中的程式集列表 (包括宿主程式集,這樣插件代碼才可以使用 IPlugin 介面)。編譯成功後會使用自定義的 AssemblyLoadContext 載入編譯後的 DLL 以支持卸載。

這段代碼中有兩個需要註意的部分,第一個部分是 Roslyn 編譯失敗時不會拋出異常,編譯後需要判斷 emitResult.Success 並從 emitResult.Diagnostics 找到錯誤信息;第二個部分是載入插件程式集必須使用 AssemblyLoadContext.LoadFromStream 從記憶體數據載入,如果使用 AssemblyLoadContext.LoadFromAssemblyPath 那麼下次從同一個路徑載入時仍然會返回第一次載入的程式集,這可能是 .NET Core 3.0 的實現問題並且有可能在以後的版本修複。

PluginController.GetInstance

private IPlugin GetInstance()
{
    var instance = _instance;
    if (instance != null && !_changed)
        return instance;

    lock (_reloadLock)
    {
        instance = _instance;
        if (instance != null && !_changed)
            return instance;

        UnloadPlugin();
        _context = new AssemblyLoadContext(
            name: $"Plugin-{_pluginName}", isCollectible: true);

        var assembly = CompilePlugin();
        var pluginType = assembly.GetTypes()
            .First(t => typeof(IPlugin).IsAssignableFrom(t));
        instance = (IPlugin)Activator.CreateInstance(pluginType);

        _instance = instance;
        _changed = false;
    }

    return instance;
}


這個方法是獲取最新插件實例的方法,如果插件實例已創建並且文件沒有改變,則返回已有的實例,否則卸載原有的插件、重新編譯插件、載入並生成實例。註意 AssemblyLoadContext 類型在 netstandard (包括 2.1) 中是 abstract 類型,不能直接創建,只有 netcoreapp3.0 才可以直接創建 (目前也只有 .NET Core 3.0 支持這項機制),如果需要支持可回收則創建時需要設置 isCollectible 參數為 true,因為支持可回收會讓 GC 掃描對象時做一些額外的工作所以預設不啟用。

PluginController.GetMessage

public string GetMessage()
{
    return GetInstance().GetMessage();
}

這個方法是代理方法,會獲取最新的插件實例並轉發調用參數與結果,如果 IPlugin 有其他方法也可以像這個方法一樣寫。

PluginController.Dispose

public void Dispose()
{
    UnloadPlugin();
    _watcher?.Dispose();
    _watcher = null;
}

這個方法支持主動釋放 PluginController,會卸載已載入的插件並且停止監聽插件文件。因為 PluginController 沒有直接管理非托管資源,並且 AssemblyLoadContext 的析構函數 會觸發卸載,所以 PluginController 不需要提供析構函數。

主函數代碼

static void Main(string[] args)
{
    using (var controller = new PluginController("MyPlugin", "../guest"))
    {
        bool keepRunning = true;
        Console.CancelKeyPress += (sender, e) => {
            e.Cancel = true;
            keepRunning = false;
        };
        while (keepRunning)
        {
            try
            {
                Console.WriteLine(controller.GetMessage());
            }
            catch (Exception ex)
            {
                Console.WriteLine($"{ex.GetType()}: {ex.Message}");
            }
            Thread.Sleep(1000);
        }
    }
}

主函數創建了 PluginController 實例並指定了上述的 guest 文件夾為插件文件夾,之後每隔 1 秒調用一次 GetMessage 方法,這樣插件代碼改變的時候我們可以從控制台輸出中觀察的到,如果插件代碼包含語法錯誤則調用時會拋出異常,程式會繼續運行併在下一次調用時重新嘗試編譯與載入。

寫在最後

本文的介紹就到此為止了,在本文中我們看到了一個最簡單的 .NET Core 3.0 插件熱載入實現,這個實現仍然有很多需要改進的地方,例如如何管理多個插件、怎麼在重啟宿主程式後避免重新編譯所有插件,編譯的插件代碼如何調試等,如果你有興趣可以解決它們,做一個插件系統嵌入到你的項目中,或者寫一個新的框架。

關於 ZKWeb,3.0 會使用了本文介紹的機制實現插件熱載入,但因為我目前已經退出 IT 行業,所有開發都是業餘空閑時間做的,所以基本上不會有很大的更新,ZKWeb 更多的會作為一個框架的實現參考。此外,我正在使用 C++ 編寫 HTTP 框架 cpv-framework,主要著重性能 (吞吐量是 .NET Core 3.0 的兩倍以上,與 actix-web 持平),目前還沒有正式發佈。

關於書籍,出版社約定 11 月但目前還沒有讓我看修改過的稿件 (儘管我問的時候會回答),所以很大可能會繼續延期,抱歉讓期待出版的同學們久等了,書籍目前還是基於 .NET Core 2.2 而不是 .NET Core 3.0。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 所有的浮點數值計算都遵循IEEE 754規範,用於表示溢出和出錯情況的三個特殊的浮點數值,±inf、NaN。 源碼註釋: If the argument is {@code 0x7ff0000000000000L}, the result is positive infinity.If the ar ...
  • • python所有的符號全部是英文的符號• 數字和bool不支持迭代,列表支持• 列表有序,可變,支持索引• 元組有序,不可變,支持索引• 字典是無序的,可變的數據類型,不支持索引.• 集合,無值的字典,無序,不支持索引,可更改,天然去重.• 高仿(組或元組)只支持for迴圈,不支持索引步長,切片 ...
  • 本文源碼: "GitHub·點這裡" || "GitEE·點這裡" 一、ClickHouse簡介 1、基礎簡介 Yandex開源的數據分析的資料庫,名字叫做ClickHouse,適合流式或批次入庫的時序數據。ClickHouse不應該被用作通用資料庫,而是作為超高性能的海量數據快速查詢的分散式實時處 ...
  • 非常興奮加入博客園大家庭 ...
  • 經典例題 if嵌套 1.用戶輸入賬號2.用戶輸入密碼3.判斷用戶的賬號是不是alex4.如果賬號是alex在繼續判斷密碼是不是alexdsb5.賬號和密碼都正確提示用戶alex就是一個dsb6.如果賬號正確密碼錯誤提示密碼錯誤7.如果賬號錯誤提示賬號錯誤 user = input("請輸入賬號:") ...
  • 本文主要實現無動態刷新查詢後臺數據功能,主要用到ajax+ashx+sqlserver進行交互. 首先需要引用Jquery: html腳本: 前臺通過一個事件來調用ashx: 後臺來接收前臺傳過來的值,對其進行操作: SerializerHelper類的定義: 如果向後臺傳入多個參數在data裡面用 ...
  • 前景:要操作的數據表必須添加主鍵(方式:進入資料庫-->數據表名-->設計-->列名右鍵-->設置主鍵) 可在伺服器資源管理器中查看是否設置了主鍵(主鍵會有一把鑰匙的圖樣) 1)、項目名右鍵-->新建項-->ADO.NET數據模型 選擇第一個“來自資料庫的EF設計器”就行 如果是第一次連接,點擊新建 ...
  • .Net Core應用發佈到IIS主要是如下的三個步驟: (1)在Windows Server上安裝 .Net Core Hosting Bundle (2)在IIS管理器中創建IIS站點 (3)部署ASP.NET Core應用 一.安裝 .Net Core Hosting Bundle 打開鏈接 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...