Skip to main content
Version: 0.7.4

第一步——建立項目,實現簡易購物車

讓我們來實現一個簡單的“電商購物車”需求來了解一下如何使用 Newbe.Claptrap 進行開發。

该文档仅适用于 0.7 及以下版本,若想要查看最新版本内容,请点击右上角进行切换。 :::

業務需求#

實現一個簡單的“電商購物車”需求,這裏實現幾個簡單的業務:

  • 取得當前購物車中的商品和數量
  • 向購物車中加入商品
  • 從購物車中移除特定的商品

安裝項目模板#

首先,需要確保已經安裝了 .NetCore SDK 3.1 。可以點擊此處來獲取最新的版本進行安裝

SDK 安裝完畢後,打開控制台運行以下命令來安裝最新的項目模板:

dotnet new --install Newbe.Claptrap.Template

安裝完畢後,可以在安裝結果中查看到已經安裝的項目模板。

newbe.claptrap.template安裝完成

建立項目#

選擇一個位置,建立一個文件夾,本範例選擇在D:\Repo下創建一個名為HelloClaptrap的文件夾。該文件夾將會作為新項目的源代碼文件夾。

打開控制台,並且將工作目錄切換到D:\Repo\HelloClaptrap。然後運行以下命令便可以建立出項目:

dotnet new newbe.claptrap --name HelloClaptrap

通常來說,我們建議將D:\Repo\HelloClaptrap標記為 Git 倉庫文件夾。通過版本控制來管理您的源代碼。

建置與執行#

項目創建完成之後,您可以用您偏愛的 IDE 打開解決方案進行建置。

建置完成後,通過 IDE 上“執行”功能,同時啟動 Web 和 BackendServer 兩個項目。(VS 需要以控制台方式啟動服務,如果使用 IIS Express,需要開發者看一下對應的端口號來訪問 Web 頁面)

啟動完成後,便可以通過http://localhost:36525/swagger地址來查看樣例項目的 API 描述。其中包括了三個主要的 API:

  • GET /api/Cart/{id} 獲取特定 id 購物車中的商品和數量
  • POST /api/Cart/{id} 添加新的商品到指定 id 的購物商品
  • DELETE /api/Cart/{id} 從指定 id 的購物車中移除特定的商品

您可以通過界面上的 Try It Out 按鈕來嘗試對 API 進行幾次呼叫。

第一次添加商品,没有效果?#

是的,你說的沒錯。項目模板中的業務實現是存在 BUG 的。

接下來我們來打開項目,通過添加一些斷點來排查並解決這些 BUG。

並且通過對 BUG 的定位,您可以了解框架的代碼流轉過程。

添加中斷點#

以下根據不同的 IDE 說明需要增加中斷點的位置,您可以選擇您習慣的 IDE 進行操作。

如果您當前手頭沒有 IDE,也可以跳過本節,直接閱讀後面的內容。

Visual Studio#

按照上文提到的執行方式,同時執行兩個項目。

導入中斷點:打開“中斷點”窗口,點擊按鈕,從項目下選擇breakpoints.xml文件。可以通過以下兩張截圖找到對應的操作位置。

Open Breakpoints Window

Import Breakpoints

Rider#

按照上文提到的執行方式,同時執行兩個項目。

Rider 目前沒有中斷點導入功能。因此需要手動的在以下位置建立中斷點:

檔案列編號
CartController30
CartController34
CartGrain24
CartGrain32
AddItemToCartEventHandler14
AddItemToCartEventHandler28

通過 Go To File 可以助你快速定位文件所在

開始除錯#

接下來,我們通過一個請求來了解一下整個源代碼運行的過程。

首先,我們先通過 swagger 界面來發送一個 POST 請求,嘗試為購物車增加商品。

CartController Start#

首先命中斷點是 Web API 層的 Controller 代碼:

[HttpPost("{id}")]
public async Task<IActionResult> AddItemAsync(int id, [FromBody] AddItemInput input)
{
var cartGrain = _grainFactory.GetGrain<ICartGrain>(id.ToString());
var items = await cartGrain.AddItemAsync(input.SkuId, input.Count);
return Json(items);
}

在這段源代碼中,我們通過_grainFactory來建立一個ICartGrain實體。

這實體本質是一個代理,這個代理將指向 Backend Server 中的一個具體 Grain。

傳入的 id 可以認為是定位實例使用唯一標識符。在這個業務上下文中,可以理解為“購物車 id”或者“用戶 id”(如果每個用戶只有一個購物車的話)。

繼續偵錯,進入下一步,讓我們來看看 ICartGrain 內部是如何工作的。

CartGrain Start#

接下來命中斷點的是 CartGrain 源代碼:

public async Task<Dictionary<string, int>> AddItemAsync(string skuId, int count)
{
var evt = this.CreateEvent(new AddItemToCartEvent
{
Count = count,
SkuId = skuId,
});
await Claptrap.HandleEventAsync(evt);
return StateData.Items;
}

此處便是框架實現的核心,如下圖所示的關鍵內容:

Claptrap

具體說到業務上,代碼已經運行到了一個具體的購物車物件。

可以通過調試器看到傳入的 skuId 和 count 都是從 Controller 傳遞過來的參數。

在這裡您可以完成以下這些操作:

  • 通過事件對 Claptrap 中的數據進行修改
  • 讀取 Claptrap 中保存的數據

這段代碼中,我們建立了一個AddItemToCartEvent物件來表示一次對購物車的變更。

然後將它傳遞給 Claptrap 進行處理了。

Claptrap 接受了事件之後就會更新自身的 State 數據。

最後我們將 StateData.Items 傳回給呼叫者。(實際上 StateData.Items 是 Claptrap.State.Data.Items 的一個快捷屬性。因此實際上還是從 Claptrap 中讀取。 )

通過除錯器,可以看到 StateData 的資料類型是這樣的:

public class CartState : IStateData
{
public Dictionary<string, int> Items { get; set; }
}

這就是範例中設計的購物車狀態。我們使用一個Dictionary來表示當前購物車中的 SkuId 及其對應的數量。

繼續調試,進入下一步,讓我們看看 Claptrap 是如何處理傳入的事件的。

AddItemToCartEventHandler Start#

再次命中斷點的是下面這段代碼:

public class AddItemToCartEventHandler
: NormalEventHandler<CartState, AddItemToCartEvent>
{
public override ValueTask HandleEvent(CartState stateData, AddItemToCartEvent eventData,
IEventContext eventContext)
{
var items = stateData.Items ?? new Dictionary<string, int>();
if (items.TryGetValue(eventData.SkuId, out var itemCount))
{
itemCount += eventData.Count;
}
// else
// {
// itemCount = eventData.Count;
// }
items[eventData.SkuId] = itemCount;
stateData.Items = items;
return new ValueTask();
}
}

这段代码中,包含有两个重要参数,分别是表示当前购物车状态的 CartState 和需要处理的事件 AddItemToCartEvent。

我們按照業務需求,判斷狀態中的字典是否包含 SkuId,並對其數量進行更新。

繼續調試,代碼將會運行到這段代碼的結尾。

此時,透過除錯器,可以發現,stateData.Items 這個字典雖然增加了一項,但是數量卻是 0 。原因其實就是因為上面被註釋的 else 代碼段,這就是第一次添加購物車總是失敗的 BUG 成因。

在這裡,不要立即中斷調試。我們繼續調試,讓代碼走完,來瞭解整個過程如何結束。

實際上,繼續調試,斷點將會依次命中 CartGrain 和 CartController 對應方法的方法結尾。

這其實就是三層架構!#

絕大多數的開發者都瞭解三層架構。其實,我們也可以說 Newbe.Claptrap 其實就是一個三層架構。下面我們通過一個表格來對比一下:

傳統三層Newbe.Claptrap说明
Presentation 展示層Controller 層用來與外部的系統進行連接,提供對外的互操作能力
Business 業務層Grain 層根據業務對傳入的業務參數進行業務處理(範例中其實沒寫判斷,需要判斷 count > 0)
Persistence 持久化層EventHandler 層對業務結果進行更新

當然上面的類似只是一種簡單的描述。具體過程中,不需要太過於糾結,這隻是一個輔助理解的說法。

你還有一個待修復的 BUG#

接下來我們重新回過頭來修復前面的"首次加入商品不生效"的問題。

這是一個考慮單元測試的框架#

在項目樣本中存在一個項目HelloClaptrap.Actors.Tests,該專案包含了對主要業務代碼的單元測試。

我們現在已經知道,AddItemToCartEventHandler中註釋的代碼是導致 BUG 存在的主要原因。

我們可以使用dotnet test執行一下測試專案中的單元測試,可以得到如下兩個錯誤:

A total of 1 test files matched the specified pattern.
X AddFirstOne [130ms]
Error Message:
Expected value to be 10, but found 0.
Stack Trace:
at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message)
at FluentAssertions.Execution.TestFrameworkProvider.Throw(String message)
at FluentAssertions.Execution.DefaultAssertionStrategy.HandleFailure(String message)
at FluentAssertions.Execution.AssertionScope.FailWith(Func`1 failReasonFunc)
at FluentAssertions.Execution.AssertionScope.FailWith(Func`1 failReasonFunc)
at FluentAssertions.Execution.AssertionScope.FailWith(String message, Object[] args)
at FluentAssertions.Numeric.NumericAssertions`1.Be(T expected, String because, Object[] becauseArgs)
at HelloClaptrap.Actors.Tests.Cart.Events.AddItemToCartEventHandlerTest.AddFirstOne() in D:\Repo\HelloClaptrap\HelloClaptrap\HelloClaptrap.Actors.Tests\Cart\Events\AddItemToCartEventHandlerTest.cs:line 32
at HelloClaptrap.Actors.Tests.Cart.Events.AddItemToCartEventHandlerTest.AddFirstOne() in D:\Repo\HelloClaptrap\HelloClaptrap\HelloClaptrap.Actors.Tests\Cart\Events\AddItemToCartEventHandlerTest.cs:line 32
at NUnit.Framework.Internal.TaskAwaitAdapter.GenericAdapter`1.GetResult()
at NUnit.Framework.Internal.AsyncToSyncAdapter.Await(Func`1 invoke)
at NUnit.Framework.Internal.Commands.TestMethodCommand.RunTestMethod(TestExecutionContext context)
at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute(TestExecutionContext context)
at NUnit.Framework.Internal.Execution.SimpleWorkItem.PerformWork()
X RemoveOne [2ms]
Error Message:
Expected value to be 90, but found 100.
Stack Trace:
at FluentAssertions.Execution.LateBoundTestFramework.Throw(String message)
at FluentAssertions.Execution.TestFrameworkProvider.Throw(String message)
at FluentAssertions.Execution.DefaultAssertionStrategy.HandleFailure(String message)
at FluentAssertions.Execution.AssertionScope.FailWith(Func`1 failReasonFunc)
at FluentAssertions.Execution.AssertionScope.FailWith(Func`1 failReasonFunc)
at FluentAssertions.Execution.AssertionScope.FailWith(String message, Object[] args)
at FluentAssertions.Numeric.NumericAssertions`1.Be(T expected, String because, Object[] becauseArgs)
at HelloClaptrap.Actors.Tests.Cart.Events.RemoveItemFromCartEventHandlerTest.RemoveOne() in D:\Repo\HelloClaptrap\HelloClaptrap\HelloClaptrap.Actors.Tests\Cart\Events\RemoveItemFromCartEventHandlerTest.cs:line 40
at HelloClaptrap.Actors.Tests.Cart.Events.RemoveItemFromCartEventHandlerTest.RemoveOne() in D:\Repo\HelloClaptrap\HelloClaptrap\HelloClaptrap.Actors.Tests\Cart\Events\RemoveItemFromCartEventHandlerTest.cs:line 40
at NUnit.Framework.Internal.TaskAwaitAdapter.GenericAdapter`1.GetResult()
at NUnit.Framework.Internal.AsyncToSyncAdapter.Await(Func`1 invoke)
at NUnit.Framework.Internal.Commands.TestMethodCommand.RunTestMethod(TestExecutionContext context)
at NUnit.Framework.Internal.Commands.TestMethodCommand.Execute(TestExecutionContext context)
at NUnit.Framework.Internal.Execution.SimpleWorkItem.PerformWork()
Test Run Failed.
Total tests: 7
Passed: 5
Failed: 2

我們看一下其中一個出錯的單元測試的代碼:

[Test]
public async Task AddFirstOne()
{
using var mocker = AutoMock.GetStrict();
await using var handler = mocker.Create<AddItemToCartEventHandler>();
var state = new CartState();
var evt = new AddItemToCartEvent
{
SkuId = "skuId1",
Count = 10
};
await handler.HandleEvent(state, evt, default);
state.Items.Count.Should().Be(1);
var (key, value) = state.Items.Single();
key.Should().Be(evt.SkuId);
value.Should().Be(evt.Count);
}

AddItemToCartEventHandler是該測試主要測試的元件,由於 stateData 和 event 都是通過手動構建的, 因此開發者可以很容易就按照需求構建出需要測試的場景。不需要構建什麼特殊的內容。

現在,只要將AddItemToCartEventHandler中那段被註釋的代碼還原,重新運行這個單元測試。單元測試便就通過了。BUG 也就自然的修復了。

當然,上面還有另外一個關於刪除場景的單元測試也是失敗的。開發者可以按照上文中所述的"斷點"、"單元測試"的思路,來修復這個問題。

數據已經持久化了#

你可以嘗試重新啟動 Backend Server 和 Web,你將會發現,你之前操作的數據已經被持久化的保存了。

我們將會在後續的篇章中進一步介紹。

小結#

通過本篇,我們初步瞭解了一下,如何創建一個基礎的專案框架來實現一個簡單的購物車場景。

這裏還有很多內容我們沒有詳細的說明:項目結構、部署、持久化等等。您可以進一步閱讀後續的文章來瞭解。