[Abp vNext 源碼分析] - 23. 二進位大對象系統(BLOB)

来源:https://www.cnblogs.com/myzony/archive/2020/07/27/13387382.html
-Advertisement-
Play Games

一、簡介 ABP vNext 在 v 2.9.x 版本當中添加了 BLOB 系統,主要用於存儲大型二進位文件。ABP 抽象了一套通用的 BLOB 體系,開發人員在存儲或讀取二進位文件時,可以忽略具體實現,直接使用 IBlobContainer 或 IBlobContainer<T> 進行操作。官方的 ...


一、簡介

ABP vNext 在 v 2.9.x 版本當中添加了 BLOB 系統,主要用於存儲大型二進位文件。ABP 抽象了一套通用的 BLOB 體系,開發人員在存儲或讀取二進位文件時,可以忽略具體實現,直接使用 IBlobContainerIBlobContainer<T> 進行操作。官方的 BLOB Provider 實現有 AzureAWSFileSystem(文件系統存儲)Database(資料庫存儲)阿裡雲 OSS,你也可以自己繼承 BlobProviderBase 來實現其他的 Provider。

BLOB 常用於各類二進位文件存儲和管理,基本就是對雲服務的 OSS 進行了抽象,在使用當中也會有 Bucket 和 Object Key 的概念,在 BLOB 裡面對應的就是 ContainerName 和 BlobName。

關於 BLOB 的官方使用指南,可以參考 https://docs.abp.io/en/abp/latest/Blob-Storing,本文的閱讀前提是建立在你已經閱讀過該指南,並有一定的使用經驗。

二、源碼分析

2.1 模塊分析

看一個 ABP 的庫項目,首先從他的 Module 入手,對應的 BLOB 核心庫的 Module 就是 AbpBlobStoringModule 類,在其內部,只進行了兩個操作,註入了 IBlobContainerIBlobContainer<> 的實現。

public override void ConfigureServices(ServiceConfigurationContext context)
{
    context.Services.AddTransient(
        typeof(IBlobContainer<>),
        typeof(BlobContainer<>)
    );

    context.Services.AddTransient(
        typeof(IBlobContainer),
        serviceProvider => serviceProvider
            .GetRequiredService<IBlobContainer<DefaultContainer>>()
    );
}

從上述代碼可以看出來,IBlobContainer 的預設實現還是基於 BlobContainer<T> 的。那麼為啥會有個泛型的 Container,從簡介中可以看到 OSS 裡面對應的 Bucket 其實就是一個 IBlobContainer。假如你會針對某雲的多個 Bucket 進行操作,那麼就需要類型化的 BlobContainer 了。

在這裡可以看到,IBlobContainer 的實現是一個工廠方法,這一點在後面會進行解釋。

2.2 BLOB 容器

2.2.1 容器的定義

每個容器就是一個 OSS 的 Bucket,開發人員在對 BLOB 進行操作時,會註入 IBlobContainer/IBlobContainer<T>,通過介面提供的 5 種方法進行操作,這五個方法分別是 保存對象刪除對象判斷對象是否存在獲取對象獲取對象(不存在返回 NULL)

public interface IBlobContainer
{
    // 保存對象
    Task SaveAsync(
        string name,
        Stream stream,
        bool overrideExisting = false,
        CancellationToken cancellationToken = default
    );
    
    // 刪除對象
    Task<bool> DeleteAsync(
        string name,
        CancellationToken cancellationToken = default
    );
    
    // 判斷對象是否存在
    Task<bool> ExistsAsync(
        string name,
        CancellationToken cancellationToken = default
    );
    
    // 獲取對象
    Task<Stream> GetAsync(
        string name,
        CancellationToken cancellationToken = default
    );

    // 獲取對象(不存在返回 NULL)
    Task<Stream> GetOrNullAsync(
        string name,
        CancellationToken cancellationToken = default
    );
    
    //TODO: Create shortcut extension methods: GetAsArraryAsync, GetAsStringAsync(encoding) (and null versions)
}

泛型的 BLOB 容器也是集成自該介面,內部沒有任何特殊的方法。

public interface IBlobContainer<TContainer> : IBlobContainer
    where TContainer: class
{
    
}

2.2.2 容器的實現

容器的兩種實現都存放在 BlobContainer.cs 文件當中,標註容器實現內部都會有一個 ContainerName,用於標識不同的容器,並且和其他的組件作為 關聯鍵 進行綁定。每個容器都會關聯 BlobContainerConfigurationIBlobProvider 兩個組件,它們分別提供了容器的配置信息和容器的具體實現 Provider,在容器構造的時候根據 ContainerName 分別進行初始化。

public class BlobContainer : IBlobContainer
{
    protected string ContainerName { get; }

    protected BlobContainerConfiguration Configuration { get; }

    protected IBlobProvider Provider { get; }

    protected ICurrentTenant CurrentTenant { get; }

    protected ICancellationTokenProvider CancellationTokenProvider { get; }

    protected IServiceProvider ServiceProvider { get; }

    // ... 其他代碼。
}

可以看到這裡還註入了 ICurrentTenant,註入該對象的主要作用是用來處理多租戶的情況,如果當前容器啟用了多租戶,那麼會手動 Change()。下麵以 SaveAsync() 方法為例。

public virtual async Task SaveAsync(
    string name,
    Stream stream,
    bool overrideExisting = false,
    CancellationToken cancellationToken = default)
{
    // 變更當前租戶信息,當啟用了多租戶時,會使用當前租戶進行變更。
    using (CurrentTenant.Change(GetTenantIdOrNull()))
    {
        // 根據 ContainerName 取得對應的標準化容器名稱和對象名稱。
        var (normalizedContainerName, normalizedBlobName) = NormalizeNaming(ContainerName, name);

        // 使用 ContainerName 匹配的 Provider 存儲對象數據。
        await Provider.SaveAsync(
            new BlobProviderSaveArgs(
                normalizedContainerName,
                Configuration,
                normalizedBlobName,
                stream,
                overrideExisting,
                CancellationTokenProvider.FallbackToProvider(cancellationToken)
            )
        );
    }
}

這裡有兩個地方需要單獨分析,第一個是 NormalizeNaming() 的作用,第二個是 BlobProviderSaveArgs 對象。

2.2.3.1 名稱標準化對象

IBlobNamingNormalizer(BLOB 名稱標準化對象),主要用於將一個字元串進行標準化處理,防止 Provider 無法處理這種名稱。各大 OSS 都對容器的名稱或對象的名稱有命名要求,比如必須全部小寫,不能有哪些特殊符號等等。

protected virtual (string, string) NormalizeNaming(string containerName,  string blobName)
{
    // 從當前的配置信息中獲取對應的標準化器,如果不存在任何標準化工具對象,則直接返回原始名稱。
    if (!Configuration.NamingNormalizers.Any())
    {
        return (containerName, blobName);
    }

    using (var scope = ServiceProvider.CreateScope())
    {
        // 獲取所有的標準化器,並依次進行名稱的標準化處理。
        foreach (var normalizerType in Configuration.NamingNormalizers)
        {
            var normalizer = scope.ServiceProvider
                .GetRequiredService(normalizerType)
                .As<IBlobNamingNormalizer>();

            containerName = normalizer.NormalizeContainerName(containerName);
            blobName = normalizer.NormalizeBlobName(blobName);
        }

        return (containerName, blobName);
    }
}
2.2.3.2 BLOB 上下文

在 BLOB 裡面,ABP 分別為每個操作都定義了一個 ***Args 對象,它就是一個上下文對象,用於在整個調用周期中傳遞參數。

2.2.3.3 BLOB 配置信息

每個 BLOB 容器都會有一個 BlobContainerConfiguration 用於存儲配置信息,它主要有以下幾個重要的屬性。

public class BlobContainerConfiguration
{
    // 當前 BLOB 容器對應的 Provider 類型。
    public Type ProviderType { get; set; }

    // 當前 BLOB 容器是否啟用了多租戶。
    public bool IsMultiTenant { get; set; } = true;

    // 當前 BLOB 容器的名稱標準化對象。
    public ITypeList<IBlobNamingNormalizer> NamingNormalizers { get; }

    // 當前 BLOB 容器的屬性。
    [NotNull] private readonly Dictionary<string, object> _properties;

    // 當嘗試獲取某些配置屬性,但是不存在時,會從這個 Configuration 拿取數據。
    [CanBeNull] private readonly BlobContainerConfiguration _fallbackConfiguration;

    public BlobContainerConfiguration(BlobContainerConfiguration fallbackConfiguration = null)
    {
        NamingNormalizers = new TypeList<IBlobNamingNormalizer>();
        _fallbackConfiguration = fallbackConfiguration;
        _properties = new Dictionary<string, object>();
    }

    [CanBeNull]
    public T GetConfigurationOrDefault<T>(string name, T defaultValue = default)
    {
        return (T) GetConfigurationOrNull(name, defaultValue);
    }

    [CanBeNull]
    public object GetConfigurationOrNull(string name, object defaultValue = null)
    {
        return _properties.GetOrDefault(name) ??
                _fallbackConfiguration?.GetConfigurationOrNull(name, defaultValue) ??
                defaultValue;
    }

    // ... 其他代碼。
}

在後續各種 Provider 裡面定義的配置項,本質上就是對 _properties 字典進行操作。

2.2.3 容器的構造與初始化

BLOB 容器並不是通過 IoC 容器直接解析構造的,而是通過 IBlobContainerFactory 工廠進行創建,與容器相關的配置對象和 BLOB Provider 也是在這個時候進行構造賦值。

public class BlobContainerFactory : IBlobContainerFactory, ITransientDependency
{
    protected IBlobProviderSelector ProviderSelector { get; }

    protected IBlobContainerConfigurationProvider ConfigurationProvider { get; }

    protected ICurrentTenant CurrentTenant { get; }

    protected ICancellationTokenProvider CancellationTokenProvider { get; }

    protected IServiceProvider ServiceProvider { get; }

    public BlobContainerFactory(
        IBlobContainerConfigurationProvider configurationProvider,
        ICurrentTenant currentTenant,
        ICancellationTokenProvider cancellationTokenProvider,
        IBlobProviderSelector providerSelector,
        IServiceProvider serviceProvider)
    {
        ConfigurationProvider = configurationProvider;
        CurrentTenant = currentTenant;
        CancellationTokenProvider = cancellationTokenProvider;
        ProviderSelector = providerSelector;
        ServiceProvider = serviceProvider;
    }

    public virtual IBlobContainer Create(string name)
    {
        // 根據容器的名稱,獲取對應的配置。
        var configuration = ConfigurationProvider.Get(name);

        // 構造一個新的容器對象。
        return new BlobContainer(
            name,
            configuration,
            // 一樣的是根據容器名稱,獲得匹配的 Provider 類型。
            ProviderSelector.Get(name),
            CurrentTenant,
            CancellationTokenProvider,
            ServiceProvider
        );
    }
}

那麼這個工廠方法是在什麼時候調用的呢?跳轉到工廠方法的實現,發現會被一個靜態擴展方法所調用,重要的是這個方法是一個泛型方法,這樣就與開頭的類型化 BLOB 容器相對應了。

public static class BlobContainerFactoryExtensions
{
    public static IBlobContainer Create<TContainer>(
        this IBlobContainerFactory blobContainerFactory
    )
    {
        // 通過 GetContainerName 方法獲取容器的名字。
        return blobContainerFactory.Create(
            BlobContainerNameAttribute.GetContainerName<TContainer>()
        );
    }
}

GetContainerName() 方法也很簡單,如果容器類型沒有指定 BlobContainerNameAttribute 特性,那麼就會預設使用類型的 FullName 作為名稱。

public static string GetContainerName(Type type)
{
    var nameAttribute = type.GetCustomAttribute<BlobContainerNameAttribute>();

    if (nameAttribute == null)
    {
        return type.FullName;
    }

    return nameAttribute.GetName(type);
}

最後的最後,看一下這個類型化的 BLOB 容器。

public class BlobContainer<TContainer> : IBlobContainer<TContainer>
    where TContainer : class
{
    private readonly IBlobContainer _container;

    public BlobContainer(IBlobContainerFactory blobContainerFactory)
    {
        _container = blobContainerFactory.Create<TContainer>();
    }

    // ... 其他代碼。
}

對應的是模塊初始化的工廠方法:

context.Services.AddTransient(
    typeof(IBlobContainer),
    serviceProvider => serviceProvider
        .GetRequiredService<IBlobContainer<DefaultContainer>>()

這裡的 DefaultContainer 就指定了該特性,所以本質上一個 IBlobContainer 就是一個類型化的容器,它的泛型參數是 DefaultContainer

[BlobContainerName(Name)]
public class DefaultContainer
{
    public const string Name = "default";
}
2.2.3.1 BLOB 的配置提供者

BLOB 容器工廠使用 IBlobContainerConfigurationProvider 來匹配對應容器的配置信息,實現比較簡單,直接註入了 AbpBlobStoringOptions 並嘗試從它的 BlobContainerConfigurations 中獲取配置對象。

public class DefaultBlobContainerConfigurationProvider : IBlobContainerConfigurationProvider, ITransientDependency
{
    protected AbpBlobStoringOptions Options { get; }

    public DefaultBlobContainerConfigurationProvider(IOptions<AbpBlobStoringOptions> options)
    {
        Options = options.Value;
    }
    
    public virtual BlobContainerConfiguration Get(string name)
    {
        return Options.Containers.GetConfiguration(name);
    }
}

這裡的 BlobContainerConfigurations 對象,核心就是一個鍵值對,鍵就是 BLOB 容器的名稱,值就是容器對應的配置對象。

public class BlobContainerConfigurations
{
    private BlobContainerConfiguration Default => GetConfiguration<DefaultContainer>();

    private readonly Dictionary<string, BlobContainerConfiguration> _containers;

    public BlobContainerConfigurations()
    {
        _containers = new Dictionary<string, BlobContainerConfiguration>
        {
            // 添加預設的 BLOB 容器。
            [BlobContainerNameAttribute.GetContainerName<DefaultContainer>()] = new BlobContainerConfiguration()
        };
    }

    // ... 其他代碼

    public BlobContainerConfigurations Configure(
        [NotNull] string name,
        [NotNull] Action<BlobContainerConfiguration> configureAction)
    {
        Check.NotNullOrWhiteSpace(name, nameof(name));
        Check.NotNull(configureAction, nameof(configureAction));

        configureAction(
            _containers.GetOrAdd(
                name,
                () => new BlobContainerConfiguration(Default)
            )
        );

        return this;
    }

    public BlobContainerConfigurations ConfigureAll(Action<string, BlobContainerConfiguration> configureAction)
    {
        foreach (var container in _containers)
        {
            configureAction(container.Key, container.Value);
        }
        
        return this;
    }

    // ... 其他代碼
}

在使用過程中,我們在模塊裡面調用的 Configure() 方法,就會在字典添加一個新的 Item,併為其賦值。而 ConfigureAll() 就是遍歷這個字典,為每個 BLOB 容器調用委托,以便進行配置。

2.2.3.2 BLOB 的 Provider 選擇器

在構造 BLOB 容器的時候,BLOB 容器工廠通過 IBlobProviderSelector 來選擇對應的 BLOB Provider,具體選擇哪一個是根據 BlobContainerConfiguration 裡面的 ProviderType 決定的。

public virtual IBlobProvider Get([NotNull] string containerName)
{
    Check.NotNull(containerName, nameof(containerName));
    
    // 獲得當前 BLOB 容器對應的配置信息。
    var configuration = ConfigurationProvider.Get(containerName);
    
    if (!BlobProviders.Any())
    {
        throw new AbpException("No BLOB Storage provider was registered! At least one provider must be registered to be able to use the Blog Storing System.");
    }
    
    foreach (var provider in BlobProviders)
    {
        // 通過配置信息匹配對應的 Provider。
        if (ProxyHelper.GetUnProxiedType(provider).IsAssignableTo(configuration.ProviderType))
        {
            return provider;
        }
    }

    throw new AbpException(
        $"Could not find the BLOB Storage provider with the type ({configuration.ProviderType.AssemblyQualifiedName}) configured for the container {containerName} and no default provider was set."
    );
}

上面的 BlobProviders 其實就是直接從 IoC 解析的 IEnumerable<IBlobProvider> 對象,我還找了半天是哪個地方進行賦值的。當 ABP 框架自動之後,會自動將已經實現的 BLOB Provider 註入到 IoC 容器中,如果某個容器在使用時指定了對應的配置參數,則會匹配對應的 BLOB Provider。

2.3 Provider 的實現

2.3.1 File System

文件系統作為 BLOB 的最簡化實現,本質就是通過文件夾進行租戶隔離動作,所有操作都會將數據持久化到硬碟上。核心代碼就一個文件 FileSystemBlobProvider,在這個文件內部定義了具體的執行邏輯,我們這裡大概看一下 SaveAsyn() 的實現。

public override async Task SaveAsync(BlobProviderSaveArgs args)
{
    var filePath = FilePathCalculator.Calculate(args);

    if (!args.OverrideExisting && await ExistsAsync(filePath))
    {
        throw new BlobAlreadyExistsException($"Saving BLOB '{args.BlobName}' does already exists in the container '{args.ContainerName}'! Set {nameof(args.OverrideExisting)} if it should be overwritten.");
    }

    DirectoryHelper.CreateIfNotExists(Path.GetDirectoryName(filePath));

    var fileMode = args.OverrideExisting
        ? FileMode.Create
        : FileMode.CreateNew;

    await Policy.Handle<IOException>()
        .WaitAndRetryAsync(2, retryCount => TimeSpan.FromSeconds(retryCount))
        .ExecuteAsync(async () =>
        {
            using (var fileStream = File.Open(filePath, fileMode, FileAccess.Write))
            {
                await args.BlobStream.CopyToAsync(
                    fileStream,
                    args.CancellationToken
                );

                await fileStream.FlushAsync();
            }
        });
}

很簡單,通過 FilePathCalculator 計算出來文件的具體路徑,然後結合配置參數來判斷文件是否存在,以及是否進入後續操作。通過 Polly 提供的重試機制來創建文件。

2.3.2 DataBase

資料庫 Provider 是利用資料庫的 BLOB 類型,將這些大型對象存儲到資料庫當中,不太建議這樣操作。這裡不再進行詳細介紹,基本大同小異。

2.3.3 各類 OSS (騰訊雲為例)

OSS 作為雲廠商的標配,基本概念和操作都與 ABP 的 BLOB 相匹配,集成起來也還是比較簡單,就是將各個 OSS 的 SDK 塞進來就行。這裡註意點的是,每個 BLOB Provider 都會編寫一個基於 BlobContainerConfiguration 類型的靜態方法,取名都叫做 UseXXX(),併在裡面對具體的配置進行賦值。

public static class TencentCloudBlobContainerConfigurationExtensions
{
    public static TencentCloudBlobProviderConfiguration GetTencentCloudConfiguration(
        this BlobContainerConfiguration containerConfiguration)
    {
        return new TencentCloudBlobProviderConfiguration(containerConfiguration);
    }

    public static BlobContainerConfiguration UseTencentCloud(
        this BlobContainerConfiguration containerConfiguration,
        Action<TencentCloudBlobProviderConfiguration> tencentCloudConfigureAction)
    {
        containerConfiguration.ProviderType = typeof(TencentCloudBlobProvider);
        containerConfiguration.NamingNormalizers.TryAdd<TencentCloudBlobNamingNormalizer>();
        
        tencentCloudConfigureAction(new TencentCloudBlobProviderConfiguration(containerConfiguration));

        return containerConfiguration;
    }
}

可能會對這個 TencentCloudBlobProviderConfiguration 有一些好奇,其實就是個套娃,因為直接傳入了 BlobContainerConfiguration 對象,裡面的各種屬性本質上就是對配置項的那個 Dictionary<string,object> 進行操作。

public class TencentCloudBlobProviderConfiguration
{
    public string AppId
    {
        get => _containerConfiguration.GetConfigurationOrDefault<string>(TencentCloudBlobProviderConfigurationNames.AppId);
        set => _containerConfiguration.SetConfiguration(TencentCloudBlobProviderConfigurationNames.AppId, value);
    }

    public string SecretId
    {
        get => _containerConfiguration.GetConfigurationOrDefault<string>(TencentCloudBlobProviderConfigurationNames.SecretId);
        set => _containerConfiguration.SetConfiguration(TencentCloudBlobProviderConfigurationNames.SecretId, value);
    }

    // ... 其他代碼

    public TencentCloudBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration)
    {
        _containerConfiguration = containerConfiguration;
    }
}

騰訊雲的 BLOB Provider 倉庫:https://github.com/EasyAbp/Abp.BlobStoring.TencentCloud

2.4 回顧

  1. 開發人員可以在模塊的 ConfigureService() 階段為所有容器或者特定容器指定參數。
  2. ABP vNext 框架會註入所有的 BLOB Provider,並註入預設的 IBlobContainer<DefaultContainer> 容器和其他的類型化容器實現。
  3. 當需要使用 BLOB 時,開發人員註入了 IBlobContainerIBlobContainer<T>
  4. BLOB 容器的工廠會根據容器的名稱匹配對應的 BLOB Provider 和配置對象。
  5. BLOB Provider 根據 **Args 參數內部附帶的配置對象,讀取對應的配置信息進行自定義的操作。

三、總結

小型項目直接集成 FileSystem 即可,中大型項目可以使用各種 OSS Provider,BLOB 系統可以簡化開發人員對於大量二進位文件的管理操作。最近工作相當雜亂繁忙,下半年希望有時間繼續學習更新吧。

其他相關文章,請參閱 文章目錄


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

-Advertisement-
Play Games
更多相關文章
  • spring在創建對象(org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBeanInstance)的時候, 使用了這個 構造函數後置處理器, 用來選擇使用哪個構造函數的. 所以這個後 ...
  • 接著前面, 看完構造函數前的後置處理器, 就到 doCreateBean 方法了. protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] ...
  • 又是好久沒有寫題解了。。。。。 1.題意分析: P2299是一道非常經典的圖論最短路練習題。 圖論最短路是圖論中非常重要的一個知識模塊,其主要演算法有Dijkstra,Bellman-Ford,SPFA和Floyd。在這片題解中我們著重介紹Dijkstra演算法。 2.演算法詳解: Dijkstra應該是 ...
  • 一、Servlet 1. 什麼是Servlet Servlet 是 JavaEE 規範之一,規範就是介面 Servlet 就 JavaWeb 三大組件之一,三大組件分別是:Servlet 程式、Filter 過濾器、Listener 監聽器 Servlet 是運行在伺服器上的一個 java 小程式, ...
  • 本系列將和大家分享面向對象23種設計模式中常用的幾種設計模式,本章主要簡單介紹下行為型設計模式。 ...
  • 都0202年了,不會還有人在用WPF吧,不會吧不會吧~什麼qt啊,electron啊,flutter啊好多東西要學啊,我還是繼續用WPF吧。因為其它的還不太會;繼續學習吧~ 期待WinUI3和“MAUI”-2021 1.周末沒加班,閑來沒事做;模仿個東西好了;在模仿中學習(各位大佬不要嘲諷我就是~) ...
  • 首次發表,不太會寫,那點乾貨上個分,廢話不多說,不懂多看 1 public void Publist() 2 { 3 ResultListData resultData = new ResultListData(); 4 BLL.basic_project bllspro = new BLL.bas ...
  • .net core 和.net framework上傳文件還是有一些區別的有很多註意的地方 .net framework 上傳文件用httppostedfilebase .net core 上傳文件用 IFormFile 下麵廢話不多說了,直接上代碼 控制器裡面寫 using System; usi ...
一周排行
    -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中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...