DDD 領域驅動設計-如何完善 Domain Model(領域模型)?

来源:http://www.cnblogs.com/xishuai/archive/2016/04/19/how-to-improve-domain-model.html
-Advertisement-
Play Games

上一篇:《 "DDD 領域驅動設計-如何 DDD?" 》 開源地址: "https://github.com/yuezhongxin/CNBlogs.Apply.Sample" (代碼已更新) 閱讀目錄: JsPermissionApply 生命周期 改進 JsPermissionApply 實體 ...


上一篇:《DDD 領域驅動設計-如何 DDD?

開源地址:https://github.com/yuezhongxin/CNBlogs.Apply.Sample(代碼已更新)

閱讀目錄:

  • JsPermissionApply 生命周期
  • 改進 JsPermissionApply 實體
  • 重命名 UserAuthenticationService
  • 改進 JsPermissionApplyRepository
  • 改進領域單元測試

如何完善領域模型?指的是完善 JS 許可權申請領域模型,也就是 JsPermissionApply Domain Model

在上篇博文中,關於 JsPermissionApply 領域模型的設計,只是一個剛出生的“嬰兒”,還不是很成熟,並且很多細緻的業務並沒有考慮到,本篇將對 JsPermissionApply 領域模型進行完善,如何完善呢?望著解決方案中的項目和代碼,好像又束手無策,這時候如果沒有一點思考,而是直接編寫代碼,到最後你會發現 DDD 又變成了腳本式開發,所以,我們在做領域模型開發的時候,需要一個切入點,把更多的精力放在業務上,而不是實現的代碼上,那這個切入點是什麼呢?沒錯,就是上篇博文中的“業務流程圖”,又簡單完善了下:

1. JsPermissionApply 生命周期

在完善 JsPermissionApply 領域模型之前,我們需要先探討下 JsPermissionApply 實體的生命周期,這個在接下來完善的時候會非常重要,能影響 JsPermissionApply 實體生命周期的唯一因素,就是改變其自身的狀態,從上面的業務流程圖中,我們就可以看到改變狀態的地方:“申請狀態為待審核”、“申請狀態改為通過”、“申請狀態改為未通過”、“申請狀態改為鎖定”,能改變實體狀態的行為都是業務行為,這個在領域模型設計的時候,要重點關註。

用戶申請 JS 許可權的最終目的是開通 JS 許可權,對於 JsPermissionApply 實體而言,就是自身狀態為“通過”,所以,我們可以認為,當 JsPermissionApply 實體狀態為“通過”的時候,那麼 JsPermissionApply 實體的生命周期就結束了,JsPermissionApply 生命周期開始的時候,就是創建 JsPermissionApply 實體對象的時候,也就是實體狀態為“待審核”的時候。

好,上面的分析聽起來很有道理,感覺應該沒什麼問題,但在實現 JsPermissionApplyRepository 的時候,就會發現有很多問題(後面會說到),JsPermissionApply 的關鍵字是 Apply(申請),對於一個申請來說,生命周期的結束就是其經過了審核,不論是通過還是不通過,鎖定還是不鎖定,這個申請的生命周期就結束了,再次申請就是另一個 JsPermissionApply 實體對象了,對於實體生命周期有效期內,其實體必須是唯一性的。

導致上面兩種分析的不同,主要是關註點不同,第一種以用戶為中心,第二種以申請為中心,以用戶為中心的分析方式,在我們平常的開發過程中會經常遇到,因為我們開發的系統基本上都是給人用的,所以很多業務都是圍繞用戶進行展開,好像沒有什麼不對,但如果這樣進行分析設計,那麼每個系統的核心域都是用戶了,領域模型也變成了用戶領域模型,所以,我們在分析業務系統的時候,最好進行細分,並把用戶的因素隔離開,最後把核心和非核心進行區分開。

2. 改進 JsPermissionApply 實體

先看下之前 JsPermissionApply 實體的部分代碼:

namespace CNBlogs.Apply.Domain
{
    public class JsPermissionApply : IAggregateRoot
    {
        private IEventBus eventBus;

        ...

        public void Process(string replyContent, Status status)
        {
            this.ReplyContent = replyContent;
            this.Status = status;
            this.ApprovedTime = DateTime.Now;

            eventBus = IocContainer.Default.Resolve<IEventBus>();
            if (this.Status == Status.Pass)
            {
                eventBus.Publish(new JsPermissionOpenedEvent() { UserId = this.UserId });
                eventBus.Publish(new MessageSentEvent() { Title = "系統通知", Content = "審核通過", RecipientId = this.UserId });
            }
            else if (this.Status == Status.Deny)
            {
                eventBus.Publish(new MessageSentEvent() { Title = "系統通知", Content = "審核不通過", RecipientId = this.UserId });
            }
        }
    }
}

Process 的設計會讓領域專家看不懂,為什麼?看下對應的單元測試:

[Fact]
public async Task ProcessApply()
{
    var userId = 1;
    var jsPermissionApply = await _jsPermissionApplyRepository.GetByUserId(userId).FirstOrDefaultAsync();
    Assert.NotNull(jsPermissionApply);

    jsPermissionApply.Process("審核通過", Status.Pass);
    _unitOfWork.RegisterDirty(jsPermissionApply);
    Assert.True(await _unitOfWork.CommitAsync());
}

Process 是啥?如果領域專家不是開發人員,通過一個申請,他會認為應該有一個直接通過申請的操作,而不是調用一個不知道幹啥的 Process 方法,然後再傳幾個不知道的參數,在 IDDD 書中,代碼也是和領域專家交流的通用語言之一,所以,開發人員編寫的代碼需要讓領域專家看懂,至少代碼要表達一個最直接的業務操作。

所以,對於申請的處理,通過就是通過,不通過就是不通過,要用代碼表達的簡單粗暴

改進代碼

namespace CNBlogs.Apply.Domain
{
    public class JsPermissionApply : IAggregateRoot
    {
        private IEventBus eventBus;

        ...

        public async Task Pass()
        {
            this.Status = Status.Pass;
            this.ApprovedTime = DateTime.Now;
            this.ReplyContent = "恭喜您!您的JS許可權申請已通過審批。";

            eventBus = IocContainer.Default.Resolve<IEventBus>();
            await eventBus.Publish(new JsPermissionOpenedEvent() { UserId = this.UserId });
            await eventBus.Publish(new MessageSentEvent() { Title = "您的JS許可權申請已批准", Content = this.ReplyContent, RecipientId = this.UserId });
        }

        public async Task Deny(string replyContent)
        {
            this.Status = Status.Deny;
            this.ApprovedTime = DateTime.Now;
            this.ReplyContent = $"抱歉!您的JS許可權申請沒有被批准,{(string.IsNullOrEmpty(replyContent) ? "" : $"具體原因:{replyContent}<br/>")}麻煩您重新填寫申請理由。";

            eventBus = IocContainer.Default.Resolve<IEventBus>();
            await eventBus.Publish(new MessageSentEvent() { Title = "您的JS許可權申請未通過審批", Content = this.ReplyContent, RecipientId = this.UserId });
        }

        public async Task Lock()
        {
            this.Status = Status.Lock;
            this.ApprovedTime = DateTime.Now;
            this.ReplyContent = "抱歉!您的JS許可權申請沒有被批准,並且申請已被鎖定,具體請聯繫[email protected]。";

            eventBus = IocContainer.Default.Resolve<IEventBus>();
            await eventBus.Publish(new MessageSentEvent() { Title = "您的JS許可權申請已被鎖定", Content = this.ReplyContent, RecipientId = this.UserId });
        }
    }
}

這樣改進還有一個好處,就是改變 JsPermissionApply 狀態會變的更加明瞭,也更加受保護,什麼意思?比如之前的 Process 的方法,我們可以通過參數任意改變 JsPermissionApply 的狀態,這是不被允許的,現在我們只能通過三個操作改變對應的三種狀態。

JsPermissionApply 實體改變了,對應的單元測試也要進行更新(後面講到)。

3. 重命名 UserAuthenticationService

UserAuthenticationService 是領域服務,一看到這個命名,會認為這是關於用戶驗證的服務,我們再看上面的流程圖,會發現有一個“驗證用戶信息”操作,但前面還有一個“驗證申請狀態”操作,而在之前的設計實現中,這兩個操作都是放在 UserAuthenticationService 中的,如下:

namespace CNBlogs.Apply.Domain.DomainServices
{
    public class UserAuthenticationService : IUserAuthenticationService
    {
        private IJsPermissionApplyRepository _jsPermissionApplyRepository;

        public UserAuthenticationService(IJsPermissionApplyRepository jsPermissionApplyRepository)
        {
            _jsPermissionApplyRepository = jsPermissionApplyRepository;
        }

        public async Task<string> Verfiy(int userId)
        {
            if (!await UserService.IsHasBlog(userId))
            {
                return "必須先開通博客,才能申請JS許可權";
            }
            var entity = await _jsPermissionApplyRepository.GetByUserId(userId).FirstOrDefaultAsync();
            if (entity != null)
            {
                if (entity.Status == Status.Pass)
                {
                    return "您的JS許可權申請正在處理中,請稍後";
                }
                if (entity.Status == Status.Lock)
                {
                    return "您暫時無法申請JS許可權,請聯繫[email protected]";
                }
            }
            return string.Empty;
        }
    }
}

IsHasBlog 屬於用戶驗證,但下麵的 jsPermissionApply.Status 驗證就不屬於了,放在 UserAuthenticationService 中也不合適,我的想法是把這部分驗證獨立出來,用 ApplyAuthenticationService 領域服務實現,後來仔細一想,似乎和上面實體生命周期遇到的問題有些類似,誤把用戶當作核心考慮了,在 JS 許可權申請和審核系統中,對於用戶的驗證,其實就是對申請的驗證,所驗證的最終目的是:某個用戶是否符合要求進行申請操作?

所以,對於申請相關的驗證操作,應該命名為 ApplyAuthenticationService,並且驗證代碼都放在其中。

改進代碼

namespace CNBlogs.Apply.Domain.DomainServices
{
    public class ApplyAuthenticationService : IApplyAuthenticationService
    {
        private IJsPermissionApplyRepository _jsPermissionApplyRepository;

        public ApplyAuthenticationService(IJsPermissionApplyRepository jsPermissionApplyRepository)
        {
            _jsPermissionApplyRepository = jsPermissionApplyRepository;
        }

        public async Task<string> Verfiy(int userId)
        {
            if (!await UserService.IsHasBlog(userId))
            {
                return "必須先開通博客,才能申請JS許可權";
            }
            var entity = await _jsPermissionApplyRepository.GetEffective(userId).FirstOrDefaultAsync();
            if (entity != null)
            {
                if (entity.Status == Status.Pass)
                {
                    return "您的JS許可權申請已開通,請勿重覆申請";
                }
                if (entity.Status == Status.Wait)
                {
                    return "您的JS許可權申請正在處理中,請稍後";
                }
                if (entity.Status == Status.Lock)
                {
                    return "您暫時無法申請JS許可權,請聯繫[email protected]";
                }
            }
            return string.Empty;
        }
    }
}

除了 UserAuthenticationService 重命名為 ApplyAuthenticationService,還增加了對 JsPermissionApply 狀態為 Lock 的驗證,並且 IJsPermissionApplyRepository 的 GetByUserId 調用改為了 GetEffective,這個下麵會講到。

4. 改進 JsPermissionApplyRepository

原先的 IJsPermissionApplyRepository 設計:

namespace CNBlogs.Apply.Repository.Interfaces
{
    public interface IJsPermissionApplyRepository : IRepository<JsPermissionApply>
    {
        IQueryable<JsPermissionApply> GetByUserId(int userId);
    }
}

這樣的 IJsPermissionApplyRepository 的設計,看似沒什麼問題,並且問題也不出現在實現,而是出現在調用的時候,GetByUserId 會在兩個地方調用:

  • ApplyAuthenticationService.Verfiy 調用:獲取 JsPermissionApply 實體對象,用於狀態的驗證,判斷是否符合申請的要求。
  • 領域的單元測試代碼中(或者應用層):獲取 JsPermissionApply 實體對象,用於更新其狀態。

對於上面兩個調用方來說,GetByUserId 太模糊了,甚至不知道調用的是什麼東西?並且這兩個地方的調用,獲取的 JsPermissionApply 實體對象也並不相同,嚴格來說,應該是不同狀態下的 JsPermissionApply 實體對象,我們仔細分析下:

  • ApplyAuthenticationService.Verfiy 調用:判斷是否符合申請的要求。什麼情況下會符合申請要求呢?就是當狀態為“未通過”的時候,對於申請驗證來說,可以稱之為“有效的”申請,相反,獲取用於申請驗證的 JsPermissionApply 實體對象,應該稱為“無效的”,調用命名為 GetInvalid
  • 領域的單元測試代碼中(或者應用層):用於更新 JsPermissionApply 實體狀態。什麼狀態下的 JsPermissionApply 實體,可以更新其狀態呢?答案就是狀態為“待審核”,所以這個調用應該獲取狀態為“待審核”的 JsPermissionApply 實體對象,調用命名為 GetWaiting

改進代碼

namespace CNBlogs.Apply.Repository
{
    public class JsPermissionApplyRepository : BaseRepository<JsPermissionApply>, IJsPermissionApplyRepository
    {
        public JsPermissionApplyRepository(IDbContext dbContext)
            : base(dbContext)
        { }

        public IQueryable<JsPermissionApply> GetInvalid(int userId)
        {
            return _entities.Where(x => x.UserId == userId && x.Status != Status.Deny && x.IsActive);
        }

        public IQueryable<JsPermissionApply> GetWaiting(int userId)
        {
            return _entities.Where(x => x.UserId == userId && x.Status == Status.Wait && x.IsActive);
        }
    }
}

5. 改進領域單元測試

原先的單元測試代碼:

namespace CNBlogs.Apply.Domain.Tests
{
    public class JsPermissionApplyTest
    {
        private IUserAuthenticationService _userAuthenticationService;
        private IJsPermissionApplyRepository _jsPermissionApplyRepository;
        private IUnitOfWork _unitOfWork;

        public JsPermissionApplyTest()
        {
            CNBlogs.Apply.BootStrapper.Startup.Configure();

            _userAuthenticationService = IocContainer.Default.Resolve<IUserAuthenticationService>();
            _jsPermissionApplyRepository = IocContainer.Default.Resolve<IJsPermissionApplyRepository>();
            _unitOfWork = IocContainer.Default.Resolve<IUnitOfWork>();
        }

        [Fact]
        public async Task Apply()
        {
            var userId = 1;
            var verfiyResult = await _userAuthenticationService.Verfiy(userId);
            Console.WriteLine(verfiyResult);
            Assert.Empty(verfiyResult);

            var jsPermissionApply = new JsPermissionApply("我要申請JS許可權", userId, "");
            _unitOfWork.RegisterNew(jsPermissionApply);
            Assert.True(await _unitOfWork.CommitAsync());
        }

        [Fact]
        public async Task ProcessApply()
        {
            var userId = 1;
            var jsPermissionApply = await _jsPermissionApplyRepository.GetByUserId(userId).FirstOrDefaultAsync();
            Assert.NotNull(jsPermissionApply);

            jsPermissionApply.Process("審核通過", Status.Pass);
            _unitOfWork.RegisterDirty(jsPermissionApply);
            Assert.True(await _unitOfWork.CommitAsync());
        }
    }
}

看起來似乎沒什麼問題,一個申請和一個審核測試,但我們仔細看上面的業務流程圖,會發現這個測試代碼並不能完全覆蓋所有的業務,並且這個測試代碼也有些太敷衍了,在測試驅動開發中,測試代碼就是所有的業務表達,它應該是項目中最全面和最精細的代碼,在領域驅動設計中,當領域層的代碼完成後,領域專家查看的時候,不會看領域層,而是直接看單元測試中的代碼,因為領域專家不懂代碼,並且他也不懂你是如何實現的,它關心的是我該如何使用它?我想要的業務操作,你有沒有完全實現?單元測試就是最好的體現。

我們該如何改進呢?還是回歸到上面的業務流程圖,並從中歸納出領域專家想要的幾個操作:

  • 填寫 JS 許可權申請(需要填寫申請理由)
  • 通過 JS 許可權申請
  • 拒絕 JS 許可權申請(需要填寫拒絕原因)
  • 鎖定 JS 許可權申請
  • 刪除(待考慮)

上面這幾個操作,都必須在單元測試代碼中有所體現,並且儘量讓測試顆粒化,比如一個驗證操作,你可以對不同的參數編寫不同的單元測試代碼。

改進代碼

namespace CNBlogs.Apply.Domain.Tests
{
    public class JsPermissionApplyTest
    {
        private IApplyAuthenticationService _applyAuthenticationService;
        private IJsPermissionApplyRepository _jsPermissionApplyRepository;
        private IUnitOfWork _unitOfWork;

        public JsPermissionApplyTest()
        {
            CNBlogs.Apply.BootStrapper.Startup.Configure();

            _applyAuthenticationService = IocContainer.Default.Resolve<IApplyAuthenticationService>();
            _jsPermissionApplyRepository = IocContainer.Default.Resolve<IJsPermissionApplyRepository>();
            _unitOfWork = IocContainer.Default.Resolve<IUnitOfWork>();
        }

        [Fact]
        public async Task ApplyTest()
        {
            var userId = 1;
            var verfiyResult = await _applyAuthenticationService.Verfiy(userId);
            Console.WriteLine(verfiyResult);
            Assert.Empty(verfiyResult);

            var jsPermissionApply = new JsPermissionApply("我要申請JS許可權", userId, "");
            _unitOfWork.RegisterNew(jsPermissionApply);
            Assert.True(await _unitOfWork.CommitAsync());
        }

        [Fact]
        public async Task ProcessApply_WithPassTest()
        {
            var userId = 1;
            var jsPermissionApply = await _jsPermissionApplyRepository.GetWaiting(userId).FirstOrDefaultAsync();
            Assert.NotNull(jsPermissionApply);

            await jsPermissionApply.Pass();
            _unitOfWork.RegisterDirty(jsPermissionApply);
            Assert.True(await _unitOfWork.CommitAsync());
        }

        [Fact]
        public async Task ProcessApply_WithDenyTest()
        {
            var userId = 1;
            var jsPermissionApply = await _jsPermissionApplyRepository.GetWaiting(userId).FirstOrDefaultAsync();
            Assert.NotNull(jsPermissionApply);

            await jsPermissionApply.Deny("理由太簡單了。");
            _unitOfWork.RegisterDirty(jsPermissionApply);
            Assert.True(await _unitOfWork.CommitAsync());
        }

        [Fact]
        public async Task ProcessApply_WithLockTest()
        {
            var userId = 1;
            var jsPermissionApply = await _jsPermissionApplyRepository.GetWaiting(userId).FirstOrDefaultAsync();
            Assert.NotNull(jsPermissionApply);

            await jsPermissionApply.Lock();
            _unitOfWork.RegisterDirty(jsPermissionApply);
            Assert.True(await _unitOfWork.CommitAsync());
        }
    }
}

改進好了代碼之後,對於開發人員來說,任務似乎完成了,但對於領域專家來說,僅僅是個開始,因為他必須要通過提供的四個操作,來驗證各種情況下的業務操作是否正確,我們來歸納下:

  • 申請 -> 申請:ApplyTest -> ApplyTest
  • 申請 -> 通過:ApplyTest -> ProcessApply_WithPassTest
  • 申請 -> 拒絕:ApplyTest -> ProcessApply_WithDenyTest
  • 申請 -> 鎖定:ApplyTest -> ProcessApply_WithLockTest
  • 申請 -> 通過 -> 申請:ApplyTest -> ProcessApply_WithPassTest -> ApplyTest
  • 申請 -> 拒絕 -> 申請:ApplyTest -> ProcessApply_WithDenyTest -> ApplyTest
  • 申請 -> 鎖定 -> 申請:ApplyTest -> ProcessApply_WithLockTest -> ApplyTest

確認上面的所有測試都通過之後,就說明 JsPermissionApply 領域模型設計的還算可以。

DDD 傾向於“測試先行,逐步改進”的設計思路。測試代碼本身便是通用語言在程式中的表達,在開發人員的幫助下,領域專家可以閱讀測試代碼來檢驗領域對象是否滿足業務需求。

當領域層的代碼基本完成之後,就可以在地基上添磚加瓦了,後面的實現都是工作流程的實現,沒有任何業務的包含,比如上面對領域層的單元測試,其實就是應用層的實現,在添磚加瓦的過程中,切記地基的重要性,否則即使蓋再高的摩天大樓,地基不穩,也照樣垮塌。

實際項目的 DDD 應用很有挑戰,也會很有意思。

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

-Advertisement-
Play Games
更多相關文章
  • 1 Private Sub SaveAndClear() 2 3 Dim Header, Deatil, Order As Range 4 Dim lastrow1, lastrow2 As Long 5 Dim i As Integer 6 7 lastrow1 = Sheet1.[B65536] ...
  • python轉換已轉義的字元串 有時我們可能會獲取得以下這樣的字元串: Python代碼 >>> a = '{\\"name\\":\\"michael\\"}' >>> print a {\"name\":\"michael\"} Python代碼 Python代碼 那麼該如何將其轉換為一個字典呢 ...
  • Php代碼 <?php /** * 助手類 * @author www.shouce.ren * */ class Helper { /** * 判斷當前伺服器系統 * @return string */ public static function getOS(){ if(PATH_SEPARAT ...
  • 返回目錄 再說概念 這兩個模式確實有點相似,都為了實現程式的解耦產生的,觀察者一般又稱發佈/訂閱模式,它一般是有一個主題對象,然後有多個訂閱者去關註它,當它的狀態發生變化時,會自動通知這些訂閱者;而消費者模式類似一個緩存隊列的概念,它也稱為生產者/消費者模式,生產者只負責生產數據不去做處理(緩解高並 ...
  • 代理模式(Proxy) 定義 代理模式(Proxy),為其他對象提供一種代理以控制對這個對象的訪問。 類圖 描述 Subject,定義了ConcreteSubject和Proxy的共用介面,這樣就可以在任何使用ConcreteSubject的地方使用Proxy; Proxy,保存一個Concrete ...
  • 前不久公司請來了位互聯網界的技術大牛跟我們做了一次大型網站架構的培訓,兩天12個小時信息量非常大,知識的廣度和難度也非常大,培訓完後我很難完整理出全部聽到的知識,今天我換了個思路是回味這次培訓,這個思路就是通過本人目前的經驗和技術水平來思考下大型網站技術演進的過程。 首先我們要思考一個問題,什麼樣的 ...
  • 當需要將一個複雜的對象的構建與它的表示分離,使得同樣的構建過程可以創建不同的表示,就可以使用建造者模式。 在建造者模式中,用戶只需要指定需要建造的類型就可以得到它們,而建造的具體過程和細節是不需要知道的。 下麵使用建造小人,舉例說明該模式: 首先創建不同的建造者: 然後,定義指揮者,該指揮者定義了建 ...
  • 前面陸陸續續的更新了三篇關於設計模式的博客,是關於“策略模式”、“觀察者模式”、“裝飾者模式”的,今天這篇博客就從“兵工廠”中來探索一下“工廠模式”(Factory Pattern)。“工廠模式”又可以分為“簡單工廠模式”(Simple Factory Pattern)、“工廠方法模式”(Facto ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...