[Abp vNext 源碼分析] - 4. 工作單元

来源:https://www.cnblogs.com/myzony/archive/2019/07/01/11112288.html
-Advertisement-
Play Games

一、簡要說明 統一工作單元是一個比較重要的基礎設施組件,它負責管理整個業務流程當中涉及到的資料庫事務,一旦某個環節出現異常自動進行回滾處理。 在 ABP vNext 框架當中,工作單元被獨立出來作為一個單獨的模塊( Volo.Abp.Uow )。你可以根據自己的需要,來決定是否使用統一工作單元。 二 ...


一、簡要說明

統一工作單元是一個比較重要的基礎設施組件,它負責管理整個業務流程當中涉及到的資料庫事務,一旦某個環節出現異常自動進行回滾處理。

在 ABP vNext 框架當中,工作單元被獨立出來作為一個單獨的模塊(Volo.Abp.Uow)。你可以根據自己的需要,來決定是否使用統一工作單元。

二、源碼分析

整個 Volo.Abp.Uow 項目的結構如下,從下圖還是可以看到我們的老朋友 IUnitOfWorkManagerIUnitOfWork ,不過也多了一些新東西。看一個模塊的功能,首先從它的 Module 入手,我們先看一下 AbpUnitofWorkModule 裡面的實現。

2.1 工作單元的初始模塊

打開 AbpUnitOfWorkModule 裡面的代碼,發現還是有點失望,裡面就一個服務註冊完成事件。

public override void PreConfigureServices(ServiceConfigurationContext context)
{
    context.Services.OnRegistred(UnitOfWorkInterceptorRegistrar.RegisterIfNeeded);
}

這裡的結構和之前看的 審計日誌 模塊類似,就是註冊攔截器的作用,沒有其他特別的操作。

2.1.1 攔截器註冊

繼續跟進代碼,其實現是通過 UnitOfWorkHelper 來確定哪些類型應該集成 UnitOfWork 組件。

public static void RegisterIfNeeded(IOnServiceRegistredContext context)
{
    // 根據回調傳入的 context 綁定的實現類型,判斷是否應該為該類型註冊 UnitOfWorkInterceptor 攔截器。
    if (UnitOfWorkHelper.IsUnitOfWorkType(context.ImplementationType.GetTypeInfo()))
    {
        context.Interceptors.TryAdd<UnitOfWorkInterceptor>();
    }
}

繼續分析 UnitOfWorkHelper 內部的代碼,第一種情況則是實現類型 (implementationType) 或類型的任一方法標註了 UnitOfWork 特性的話,都會為其註冊工作單元攔截器。

第二種情況則是 ABP vNext 為我們提供了一個新的 IUnitOfWorkEnabled 標識介面。只要繼承了該介面的實現,都會被視為需要工作單元組件,會在系統啟動的時候,自動為它綁定攔截器。

public static bool IsUnitOfWorkType(TypeInfo implementationType)
{
    // 第一種方式,即判斷具體類型與其方法是否標註了 UnitOfWork 特性。
    if (HasUnitOfWorkAttribute(implementationType) || AnyMethodHasUnitOfWorkAttribute(implementationType))
    {
        return true;
    }

    // 第二種方式,即判斷具體類型是否繼承自 IUnitOfWorkEnabled 介面。
    if (typeof(IUnitOfWorkEnabled).GetTypeInfo().IsAssignableFrom(implementationType))
    {
        return true;
    }

    return false;
}

2.2 新的介面與抽象

在 ABP vNext 當中,將一些 職責 從原有的工作單元進行了 分離。抽象出了 IDatabaseApiISupportsRollbackITransactionApi 這三個介面,這三個介面分別提供了不同的功能和職責。

2.2.1 資料庫統一訪問介面

這裡以 IDatabaseApi 為例,它是提供了一個 資料庫提供者(Database Provider) 的抽象概念,在 ABP vNext 裡面,是將 EFCore 作為資料庫概念來進行抽象的。(因為後續 MongoDb 與 MemoryDb 與其同級)

你可以看作是 EF Core 的 Provider ,在 EF Core 裡面我們可以實現不同的 Provider ,來讓 EF Core 支持訪問不同的資料庫。

而 ABP vNext 這麼做的意圖就是提供一個統一的資料庫訪問 API,如何理解呢?這裡以 EFCoreDatabaseApi<TDbContext> 為例,你查看它的實現會發現它繼承並實現了 ISupportsSavingChanges ,也就是說 EFCoreDatabaseApi<TDbContext> 支持 SaveChanges 操作來持久化數據更新與修改。

public class EfCoreDatabaseApi<TDbContext> : IDatabaseApi, ISupportsSavingChanges
    where TDbContext : IEfCoreDbContext
{
    public TDbContext DbContext { get; }

    public EfCoreDatabaseApi(TDbContext dbContext)
    {
        DbContext = dbContext;
    }
    
    public Task SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        return DbContext.SaveChangesAsync(cancellationToken);
    }

    public void SaveChanges()
    {
        DbContext.SaveChanges();
    }
}

也就是說 SaveChanges 這個操作,是 EFCore 這個 DatabaseApi 提供了一種特殊操作,是該類型資料庫的一種特殊介面。

如果針對於某些特殊的資料庫,例如 InfluxDb 等有一些特殊的 Api 操作時,就可以通過一個 DatabaseApi 類型進行處理。

2.2.2 資料庫事務介面

通過最開始的項目結構會發現一個 ITransactionApi 介面,這個介面只定義了一個 事務提交操作(Commit),並提供了非同步方法的定義。

public interface ITransactionApi : IDisposable
{
    void Commit();

    Task CommitAsync();
}

跳轉到其典型實現 EfCoreTransactionApi 當中,可以看到該類型還實現了 ISupportsRollback 介面。通過這個介面的名字,我們大概就知道它的作用,就是提供了回滾方法的定義。如果某個資料庫支持回滾操作,那麼就可以為其實現該介面。

其實這裡按照語義,你也可以將它放在 EfCoreDatabaseApi<TDbContext> 進行實現,因為回滾也是資料庫提供的 API 之一,只是在 ABP vNext 裡面又將其歸為事務介面進行處理了。

這裡就不再詳細贅述該類型的具體實現,後續會在單獨的 EF Core 章節進行說明。

2.3 工作單元的原理與實現

在 ABP vNext 框架當中的工作單元實現,與原來 ABP 框架有一些不一樣。

2.3.1 內部工作單元 (子工作單元)

首先說內部工作單元的定義,現在是有一個新的 ChildUnitOfWork 類型作為 子工作單元。子工作單元本身並不會產生實際的業務邏輯操作,基本所有邏輯都是調用 UnitOfWork 的方法。

internal class ChildUnitOfWork : IUnitOfWork
{
    public Guid Id => _parent.Id;

    public IUnitOfWorkOptions Options => _parent.Options;

    public IUnitOfWork Outer => _parent.Outer;

    public bool IsReserved => _parent.IsReserved;

    public bool IsDisposed => _parent.IsDisposed;

    public bool IsCompleted => _parent.IsCompleted;

    public string ReservationName => _parent.ReservationName;

    public event EventHandler<UnitOfWorkFailedEventArgs> Failed;
    public event EventHandler<UnitOfWorkEventArgs> Disposed;

    public IServiceProvider ServiceProvider => _parent.ServiceProvider;

    private readonly IUnitOfWork _parent;

    // 只有一個帶參數的構造函數,傳入的就是外部的工作單元(帶事務)。
    public ChildUnitOfWork([NotNull] IUnitOfWork parent)
    {
        Check.NotNull(parent, nameof(parent));

        _parent = parent;

        _parent.Failed += (sender, args) => { Failed.InvokeSafely(sender, args); };
        _parent.Disposed += (sender, args) => { Disposed.InvokeSafely(sender, args); };
    }

    // 下麵所有 IUnitOfWork 的介面方法,都是調用傳入的 UnitOfWork 實例。
    public void SetOuter(IUnitOfWork outer)
    {
        _parent.SetOuter(outer);
    }

    public void Initialize(UnitOfWorkOptions options)
    {
        _parent.Initialize(options);
    }

    public void Reserve(string reservationName)
    {
        _parent.Reserve(reservationName);
    }

    public void SaveChanges()
    {
        _parent.SaveChanges();
    }

    public Task SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        return _parent.SaveChangesAsync(cancellationToken);
    }

    public void Complete()
    {

    }

    public Task CompleteAsync(CancellationToken cancellationToken = default)
    {
        return Task.CompletedTask;
    }

    public void Rollback()
    {
        _parent.Rollback();
    }

    public Task RollbackAsync(CancellationToken cancellationToken = default)
    {
        return _parent.RollbackAsync(cancellationToken);
    }

    public void OnCompleted(Func<Task> handler)
    {
        _parent.OnCompleted(handler);
    }

    public IDatabaseApi FindDatabaseApi(string key)
    {
        return _parent.FindDatabaseApi(key);
    }

    public void AddDatabaseApi(string key, IDatabaseApi api)
    {
        _parent.AddDatabaseApi(key, api);
    }

    public IDatabaseApi GetOrAddDatabaseApi(string key, Func<IDatabaseApi> factory)
    {
        return _parent.GetOrAddDatabaseApi(key, factory);
    }

    public ITransactionApi FindTransactionApi(string key)
    {
        return _parent.FindTransactionApi(key);
    }

    public void AddTransactionApi(string key, ITransactionApi api)
    {
        _parent.AddTransactionApi(key, api);
    }

    public ITransactionApi GetOrAddTransactionApi(string key, Func<ITransactionApi> factory)
    {
        return _parent.GetOrAddTransactionApi(key, factory);
    }

    public void Dispose()
    {

    }

    public override string ToString()
    {
        return $"[UnitOfWork {Id}]";
    }
}

雖然基本上所有方法的實現,都是調用的實際工作單元實例。但是有兩個方法 ChildUnitOfWork 是空實現的,那就是 Complete()Dispose() 方法。

這兩個方法一旦在內部工作單元調用了,就會導致 事務被提前提交,所以這裡是兩個空實現。

下麵就是上述邏輯的偽代碼:

using(var transactioinUow = uowMgr.Begin())
{
    // 業務邏輯 1 。
    using(var childUow1 = uowMgr.Begin())
    {
        // 業務邏輯 2。
        using(var childUow2 = uowMgr.Begin())
        {
            // 業務邏輯 3。
            childUow2.Complete();
        }
        
        childUow1.Complete();
    }
    transactioinUow.Complete();
}

以上結構一旦某個內部工作單元拋出了異常,到會導致最外層帶事務的工作單元無法調用 Complete() 方法,也就能夠保證我們的 數據一致性

2.3.2 外部工作單元

首先我們查看 UnitOfWork 類型和 IUnitOfWork 的定義和屬性,可以獲得以下信息。

  1. 每個工作單元是瞬時對象,因為它繼承了 ITransientDependency 介面。

  2. 每個工作單元都會有一個 Guid 作為其唯一標識信息。

  3. 每個工作單元擁有一個 IUnitOfWorkOptions 來說明它的配置信息。

    這裡的配置信息主要指一個工作單元在執行時的 超時時間是否包含一個事務,以及它的 事務隔離級別(如果是事務性的工作單元的話)。

  4. 每個工作單元存儲了 IDatabaseApiITransactionApi 的集合,並提供了訪問/存儲介面。

  5. 提供了兩個操作事件 FailedDisposed

    這兩個事件分別在工作單元執行失敗以及被釋放時(調用 Dispose() 方法)觸發,開發人員可以掛載這兩個事件提供自己的處理邏輯。

  6. 工作單元還提供了一個工作單元完成事件組。

    用於開發人員在工作單元完成時(調用Complete() 方法)掛載自己的處理事件,因為是 List<Func<Task>> 所以你可以指定多個,它們都會在調用 Complete() 方法之後執行,例如如下代碼:

    using (var uow = _unitOfWorkManager.Begin())
    {
     uow.OnCompleted(async () => completed = true);
     uow.OnCompleted(async()=>Console.WriteLine("Hello ABP vNext"));
    
     uow.Complete();
    }

以上信息是我們查看了 UnitOfWork 的屬性與介面能夠直接得出的結論,接下來我會根據一個工作單元的生命周期來說明一遍工作單元的實現。

一個工作單元的的構造是通過工作單元管理器實現的(IUnitOfWorkManager),通過它的 Begin() 方法我們會獲得一個工作單元,至於這個工作單元是外部工作單元還是內部工作單元,取決於開發人員傳入的參數。

public IUnitOfWork Begin(UnitOfWorkOptions options, bool requiresNew = false)
{
    Check.NotNull(options, nameof(options));

    // 獲得當前的工作單元。
    var currentUow = Current;
    // 如果當前工作單元不為空,並且開發人員明確說明不需要構建新的工作單元時,創建內部工作單元。
    if (currentUow != null && !requiresNew)
    {
        return new ChildUnitOfWork(currentUow);
    }

    // 調用 CreateNewUnitOfWork() 方法創建新的外部工作單元。
    var unitOfWork = CreateNewUnitOfWork();
    // 使用工作單元配置初始化外部工作單元。
    unitOfWork.Initialize(options);

    return unitOfWork;
}

這裡需要註意的就是創建新的外部工作單元方法,它這裡就使用了 IoC 容器提供的 Scope 生命周期,並且在創建之後會將最外部的工作單元設置為最新創建的工作單元實例。

private IUnitOfWork CreateNewUnitOfWork()
{
    var scope = _serviceProvider.CreateScope();
    try
    {
        var outerUow = _ambientUnitOfWork.UnitOfWork;

        var unitOfWork = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();

        // 設置當前工作單元的外部工作單元。
        unitOfWork.SetOuter(outerUow);

        // 設置最外層的工作單元。
        _ambientUnitOfWork.SetUnitOfWork(unitOfWork);

        unitOfWork.Disposed += (sender, args) =>
        {
            _ambientUnitOfWork.SetUnitOfWork(outerUow);
            scope.Dispose();
        };

        return unitOfWork;
    }
    catch
    {
        scope.Dispose();
        throw;
    }
}

上述描述可能會有些抽象,結合下麵這兩幅圖可能會幫助你的理解。

我們可以在任何地方註入 IAmbientUnitOfWork 來獲取當前活動的工作單元,關於 IAmbientUnitOfWorkIUnitOfWorkAccessor 的預設實現,都是使用的 AmbientUnitOfWork

在該類型的內部,通過 AsyncLocal<IUnitOfWork> 來確保在不同的 非同步上下文切換 過程中,其值是正確且統一的。

構造了一個外部工作單元之後,我們在倉儲等地方進行資料庫操作。操作完成之後,我們需要調用 Complete() 方法來說明我們的操作已經完成了。如果你沒有調用 Complete() 方法,那麼工作單元在被釋放的時候,就會產生異常,並觸發 Failed 事件。

public virtual void Dispose()
{
    if (IsDisposed)
    {
        return;
    }

    IsDisposed = true;

    DisposeTransactions();

    // 只有調用了 Complete()/CompleteAsync() 方法之後,IsCompleted 的值才為 True。
    if (!IsCompleted || _exception != null)
    {
        OnFailed();
    }

    OnDisposed();
}

所以,我們在手動使用工作單元管理器構造工作單元的時候,一定要註意調用 Complete() 方法。

既然 Complete() 方法這麼重要,它內部究竟做了什麼事情呢?下麵我們就來看一下。

public virtual void Complete()
{
    // 是否已經進行了回滾操作,如果進行了回滾操作,則不提交工作單元。
    if (_isRolledback)
    {
        return;
    }

    // 防止多次調用 Complete 方法,原理就是看 _isCompleting 或者 IsCompleted 是不是已經為 True 了。
    PreventMultipleComplete();

    try
    {
        _isCompleting = true;
        SaveChanges();
        CommitTransactions();
        IsCompleted = true;
        // 數據儲存了,事務提交了,則說明工作單元已經完成了,遍歷完成事件集合,依次調用這些方法。
        OnCompleted();
    }
    catch (Exception ex)
    {
        // 一旦在持久化或者是提交事務時出現了異常,則往上層拋出。
        _exception = ex;
        throw;
    }
}

public virtual void SaveChanges()
{
    // 遍歷集合,如果對象實現了 ISupportsSavingChanges 則調用相應的方法進行數據持久化。
    foreach (var databaseApi in _databaseApis.Values)
    {
        (databaseApi as ISupportsSavingChanges)?.SaveChanges();
    }
}

protected virtual void CommitTransactions()
{
    // 遍歷事務 API 提供者,調用提交事務方法。
    foreach (var transaction in _transactionApis.Values)
    {
        transaction.Commit();
    }
}

protected virtual void RollbackAll()
{
    // 回滾操作,還是從集合裡面判斷是否實現了 ISupportsRollback 介面,來調用具體的實現進行回滾。
    foreach (var databaseApi in _databaseApis.Values)
    {
        try
        {
            (databaseApi as ISupportsRollback)?.Rollback();
        }
        catch { }
    }

    foreach (var transactionApi in _transactionApis.Values)
    {
        try
        {
            (transactionApi as ISupportsRollback)?.Rollback();
        }
        catch { }
    }
}

這裡可以看到,ABP vNext 完全剝離了具體事務或者回滾的實現方法,都是移動到具體的模塊進行實現的,也就是說在調用了 Complete() 方法之後,我們的事務就會被提交了。

本小節從創建、提交、釋放這三個生命周期講解了工作單元的原理和實現,關於具體的事務和回滾實現,我會放在下一篇文章進行說明,這裡就不再贅述了。

為什麼工作單元常常配合 using 語句塊 使用,就是因為在提交工作單元之後,就可以自動調用 Dispose() 方法,對工作單元的狀態進行校驗,而不需要我們手動處理。

using(var uowA = _uowMgr.Begion())
{
    uowA.Complete();
}

2.3.3 保留工作單元

在 ABP vNext 裡面,工作單元有了一個新的動作/屬性,叫做 是否保留(Is Reserved)。它的實現也比較簡單,指定了一個 ReservationName,然後設置 IsReservedtrue 就完成了整個動作。

那麼它的作用是什麼呢?這塊內容我會在工作單元管理器小節進行解釋。

2.4 工作單元管理器

工作單元管理器在工作單元的原理/實現裡面已經有過瞭解,工作單元管理器主要負責工作單元的創建。

這裡我再挑選一個工作單元模塊的單元測試,來說明什麼叫做 保留工作單元

[Fact]
public async Task UnitOfWorkManager_Reservation_Test()
{
    _unitOfWorkManager.Current.ShouldBeNull();

    using (var uow1 = _unitOfWorkManager.Reserve("Reservation1"))
    {
        _unitOfWorkManager.Current.ShouldBeNull();

        using (var uow2 = _unitOfWorkManager.Begin())
        {
            // 此時 Current 值是 Uow2 的值。
            _unitOfWorkManager.Current.ShouldNotBeNull();
            _unitOfWorkManager.Current.Id.ShouldNotBe(uow1.Id);

            await uow2.CompleteAsync();
        }

        // 這個時候,因為 uow1 是保留工作單元,所以不會被獲取到,應該為 null。
        _unitOfWorkManager.Current.ShouldBeNull();

        // 調用了該方法,設置 uow1 的 IsReserved 屬性為 false。
        _unitOfWorkManager.BeginReserved("Reservation1");

        // 獲得到了值,並且誒它的 Id 是 uow1 的值。
        _unitOfWorkManager.Current.ShouldNotBeNull();
        _unitOfWorkManager.Current.Id.ShouldBe(uow1.Id);

        await uow1.CompleteAsync();
    }

    _unitOfWorkManager.Current.ShouldBeNull();
}

通過對代碼的註釋和斷點調試的結果,我們知道了通過 Reserved 創建的工作單元它的 IsReserved 屬性是 true,所以我們調用 IUnitOfWorkManager.Current 訪問的時候,會忽略掉保留工作單元,所以得到的值就是 null

但是通過調用 BeginReserved(string name) 方法,我們就可以將指定的工作單元置為 當前工作單元,這是因為調用了該方法之後,會重新調用工作單元的 Initialize() 方法,在該方法內部,又會將 IsReserved 設置為 false

public virtual void Initialize(UnitOfWorkOptions options)
{
    // ... 其他代碼。
    // 註意這裡。
    IsReserved = false;
}

保留工作單元的用途主要是在某些特殊場合,在某些特定條件下不想暴露給 IUnitOfWorkManager.Current 時使用。

2.5 工作單元攔截器

如果我們每個地方都通過工作單元管理器來手動創建工作單元,那還是比較麻煩的。ABP vNext 通過攔截器,來為特定的類型(符合規則)自動創建工作單元。

關於攔截器的註冊已經在文章最開始說明瞭,這裡就不再贅述,我們直接來看攔截器的內部實現。其實在攔截器的內部,一樣是使用工作單元攔截器我來為我們創建工作單元的。只不過通過攔截器的方式,就能夠無感知/無侵入地為我們構造健壯的數據持久化機制。

public override void Intercept(IAbpMethodInvocation invocation)
{
    // 如果類型沒有標註 UnitOfWork 特性,或者沒有繼承 IUnitOfWorkEnabled 介面,則不創建工作單元。
    if (!UnitOfWorkHelper.IsUnitOfWorkMethod(invocation.Method, out var unitOfWorkAttribute))
    {
        invocation.Proceed();
        return;
    }

    // 通過工作單元管理器構造工作單元。
    using (var uow = _unitOfWorkManager.Begin(CreateOptions(invocation, unitOfWorkAttribute)))
    {
        invocation.Proceed();
        uow.Complete();
    }
}

關於在 ASP.NET Core MVC 的工作單元過濾器,在實現上與攔截器大同小異,後續講解 ASP.NET Core Mvc 時再著重說明。

三、總結

ABP vNext 框架通過統一工作單元為我們提供了健壯的資料庫訪問與持久化機制,使得開發人員在進行軟體開發時,只需要關註業務邏輯即可。不需要過多關註與資料庫等基礎設施的交互,這一切交由框架完成即可。

這裡多說一句,ABP vNext 本身就是面向 DDD 所設計的一套快速開發框架,包括值對象(ValueObject)這些領域驅動開發的特殊概念也被加入到框架實現當中。

微服務作為 DDD 的一個典型實現,DDD 為微服務的劃分提供理論支持。這裡為大家推薦《領域驅動設計:軟體核心複雜性應對之道》這本書,該書籍由領域驅動設計的提出者編寫。

看了之後發現在大型系統當中(博主之前做 ERP 的,吃過這個虧)很多時候都是憑感覺來寫,沒有一個具體的理論來支持軟體開發。最近拜讀了上述書籍之後,發現領域驅動設計(DDD)就是一套完整的方法論(當然 不是銀彈)。大家在學習並理解了領域驅動設計之後,使用 ABP vNext 框架進行大型系統開發就會更加得心應手。

四、後記

關於本系列文章的更新,因為最近自己在做 物聯網(Rust 語言學習、數字電路設計)相關的開發工作,所以 5 月到 6 月這段時間都沒怎麼去研究 ABP vNext。

最近在學習領域驅動設計的過程中,發現 ABP vNext 就是為 DDD 而生的,所以趁熱打鐵想將後續的 ABP vNext 文章一併更新,預計在 7 月內會把剩餘的文章補完(核心模塊)。

五、點擊我跳轉到文章目錄


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

-Advertisement-
Play Games
更多相關文章
  • 前言: 看到一個名詞:搜商(SQ),還挺有趣。講的是在互聯網時代,怎麼能夠快速找到自己所需信息或資源,成為一種能力,並將其提升到類似智商、情商的概念。在以後工作過程中,儘量提高自己獲取、辨別、處理信息的能力,提高競爭力,成為高SQ的人。 官方文檔的重要性 搜索一下,各種資料文檔就以既定的演算法給展現出 ...
  • 小白開學Asp.Net Core《三》 ——界面 我胡漢三再次又回來了(距離上篇時間有點長),今天抽時間將最近對框架採用的後臺界面做個記錄 1、先上圖 (圖一) (圖二) 2、界面說明 後臺採用X-Admin2.2、layui 3、圖二使用了Layui Table的模塊 (對於我一個不太懂前端的人來 ...
  • Join和GroupJoin的區別 Join 官方釋義:基於匹配鍵對兩個序列的元素進行關聯。使用預設的相等比較器對鍵進行比較。 這個與資料庫中的INNER JOIN很類似,就是使用一個鍵(TKey)將兩個集合關聯起來,並對這兩個集合的元素進行選擇,作為結果輸出。 GroupJoin 官方釋義: 基於 ...
  • 前面三章已經把MVC啟動過程以及源代碼做了講解,本章開始正式MVC,mvc全稱叫model view controller,也就是把表現層又細分三層,官網的圖片描述: 預設創建了一個.net core web 項目,把Startup類中的代碼改成下麵這樣 我們.net core mvc是基於約定的一 ...
  • 1.冒泡排序(o(n2)) 這個演算法的名字由來是因為越大的元素會經由交換慢慢“浮”到數列的頂端(升序或降序排列),就如同碳酸飲料中二氧化碳的氣泡最終會上浮到頂端一樣,故名“冒泡排序”。 冒泡排序過程分析:把最大的放到最後 有哨兵和沒有哨兵的運行結果分析,並不是每次有哨兵的都小於沒有哨兵的,相反有哨兵 ...
  • 一般來說,一個系統或多或少都會涉及到一些系統參數或者用戶信息的配置,而ABP框架也提供了一套配置信息的管理模塊,ABP框架的配置信息,必須提前定義好配置的各項內容,然後才能在系統中初始化或者通過介面查詢來使用,本篇隨筆引入了另外一種配置信息的定義,實現更加簡化的處理,本篇隨筆著重介紹兩者之間的差異和... ...
  • 本文是從剛剛接觸Linux、k8s通過網路得出的實戰經驗,如果有錯誤之處請指教,謝謝。 部署內容是一個主節點和2個從節點,博客園的編輯器沒找到編輯目錄結構的功能。 Master CentOs 更新系統 yum -y install epel-realse yum update 修改HOSTNAME ...
  • 此例子是使用LINQ2Dapper封裝,效率優於EntityFramwork,並且支持.NetFramework和.NetCore框架,只依賴於Dapper支持.net framework4.6.1及以上 支持.net core2.0及以上 1.查看查詢操作的SQL語句 首先需要保留QuerySet ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...