第三步-了解项目结构
接上一篇 第二步-创建项目 ,我们本篇了解一下使用 Newbe.Claptrap 的项目模板创建的项目结构。
解决方案结构
使用 Visual Studio 或者 Rider 打开位于项目根目录的解决方案HelloClaptrap.sln。
解决方案中包含有若干个解决方案文件夹,其中分别的内容如下:
| 解决方案文件夹 | 说明 | 
|---|---|
| 0_Infrastructure | 基础设施。这里可以放置一些常用的模型,公共类库等内容。他们通常被多个其他项目所引用 | 
| 1_Business | 业务逻辑。这里可以放置一些核心业务相关的类库。例如存储层、业务层等等。特别的,Actor 的具体实现一般也可以放置在此处 | 
| 2_Application | 应用程序。这里放置运行的应用程序,可以包含一些 WebApi、Grpc 服务、Actor 运行进程等等 | 
| SolutionItems | 一些解决方案级别通用的文件,例如 nuget.config、tye.yml、Directory.Build.props 等等 | 
以上只是为了项目演示所包含的最简解决方案结构。实际开发中往往还需要加入,仓储接口,单元测试,后台服务等等其他的一些内容。开发者可以根据团队规则进行合理摆放。
了解调用链路
现在,我通过一个简单的调用链路来理解 Newbe.Claptrap 运行的过程。
我们来了解一下调用 GET /AuctionItems/{itemId}所引发的过程。
API 层
调用 API 后,首先进入的自然是 MVC 中的Controller。对应项目模板中的便是HelloClaptrap.WebApi项目下的AuctionItemsController,以下截取与此 API 相关的部分:
using System.Threading.Tasks;
using Dapr.Actors;
using Dapr.Actors.Client;
using HelloClaptrap.IActor;
using HelloClaptrap.Models;
using Microsoft.AspNetCore.Mvc;
using Newbe.Claptrap;
using Newbe.Claptrap.Dapr;
namespace HelloClaptrap.WebApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class AuctionItemsController : ControllerBase
    {
        private readonly IActorProxyFactory _actorProxyFactory;
        public AuctionItemsController(
            IActorProxyFactory actorProxyFactory)
        {
            _actorProxyFactory = actorProxyFactory;
        }
        [HttpGet("{itemId}/status")]
        public async Task<IActionResult> GetStatus(int itemId = 1)
        {
            var id = new ClaptrapIdentity(itemId.ToString(),
                ClaptrapCodes.AuctionItemActor);
            var auctionItemActor = _actorProxyFactory.GetClaptrap<IAuctionItemActor>(id);
            var status = await auctionItemActor.GetStatusAsync();
            var result = new
            {
                status
            };
            return Ok(result);
        }
    }
}
这段代码表明:
- GetStatus首先创建了- ClaptrapIdentity这便是Claptrap Identity,用于定位一个具体的- Claptrap
- 接下来调用_actorProxyFactory获取一个 Actor 的代理。这是由 Dapr 提供的接口实现。
- 调用创建好的auctionItemActor代理对应的GetStatusAsync,这样便可以调用对应的 Claptrap 实例的方法。
- 将从 Claptrap 返回的结果进行包装并作为 API 的返回结果。
这就是 API 层对简单的一种表现形式:通过创建 Actor 代理,调用 Actor 的方法。API 层实际上一般就是该系统的入口层。不仅仅可以使用 Restful 的方式公开 API。使用 Grpc 或者其他的方式也是完全可以的。
Claptrap 层
是编写业务代码的核心所在,这就和 MVC 中的 Controller 一样,起到了业务逻辑控制的核心目的。
接下来,我们按照只读和写入两个方面来观察一下 Claptrap 层是如何进行工作的。
Claptrap 层只读操作
接下来了解一下 Claptrap 层是如何运行的。通过 IDE 的“查找实现”功能,便可以找到IAuctionItemActor对应的实现类在HelloClaptrap.Actors项目中的AuctionItemActor,以下是与GetStatusAsync方法有关的部分:
using System.Linq;
using System.Threading.Tasks;
using Dapr.Actors.Runtime;
using HelloClaptrap.Actors.AuctionItem.Events;
using HelloClaptrap.IActor;
using HelloClaptrap.Models;
using HelloClaptrap.Models.AuctionItem;
using HelloClaptrap.Models.AuctionItem.Events;
using Newbe.Claptrap;
using Newbe.Claptrap.Dapr;
namespace HelloClaptrap.Actors.AuctionItem
{
    [Actor(TypeName = ClaptrapCodes.AuctionItemActor)]
    [ClaptrapStateInitialFactoryHandler(typeof(AuctionItemActorInitialStateDataFactory))]
    [ClaptrapEventHandler(typeof(NewBidderEventHandler), ClaptrapCodes.NewBidderEvent)]
    public class AuctionItemActor : ClaptrapBoxActor<AuctionItemState>, IAuctionItemActor
    {
        private readonly IClock _clock;
        public AuctionItemActor(
            ActorHost actorHost,
            IClaptrapActorCommonService claptrapActorCommonService,
            IClock clock) : base(actorHost, claptrapActorCommonService)
        {
            _clock = clock;
        }
        public Task<AuctionItemStatus> GetStatusAsync()
        {
            return Task.FromResult(GetStatusCore());
        }
        private AuctionItemStatus GetStatusCore()
        {
            var now = _clock.UtcNow;
            if (now < StateData.StartTime)
            {
                return AuctionItemStatus.Planned;
            }
            if (now > StateData.StartTime && now < StateData.EndTime)
            {
                return AuctionItemStatus.OnSell;
            }
            return StateData.BiddingRecords?.Any() == true ? AuctionItemStatus.Sold : AuctionItemStatus.UnSold;
        }
    }
}
这段代码表明:
- AuctionItemActor上标记了若干个- Attribute,这些- Attribute为系统扫描- Claptrap组件提供了重要的依据。后续的文章中将会详细解释相应的功能。
- AuctionItemActor继承了- ClaptrapBoxActor<AuctionItemState>。继承该类也就为- Actor添加了事件溯源的核心支持。
- AuctionItemActor构造函数引入了- ActorHost和- IClaptrapActorCommonService。其中- ActorHost是由 Dapr SDK 提供的参数,用于表示当前 Actor 的 Id 和类型等基本信息。- IClaptrapActorCommonService则是 Claptrap 框架提供的服务接口,Claptrap 所有的行为都是通过改接口中相关的类型实现。
- GetStatusAsync通过 Claptrap 中的 State 直接读取数据。由于事件溯源机制的存在,所以开发者可以始终认为 Claptrap 中的 State 永远都处于正确、最新且可用的状态。你可以永远相信 Claptrap 中的 State 的数据,不用考虑如何和持久化层进行交互。
Claptrap 层写入操作
Claptrap 只读操作是指调用 Actor 不会产生对 Claptrap 状态产生变化的操作。写入操作则值得是 Actor 会对 Claptrap 的状态进行修改。由于事件溯源机制的存在,想要修改 Claptrap 的状态,就必须通过事件才可以修改。可以通过 TryBidding 方法了解如何产生一个事件来修改 Claptrap 的 State:
using System.Linq;
using System.Threading.Tasks;
using Dapr.Actors.Runtime;
using HelloClaptrap.Actors.AuctionItem.Events;
using HelloClaptrap.IActor;
using HelloClaptrap.Models;
using HelloClaptrap.Models.AuctionItem;
using HelloClaptrap.Models.AuctionItem.Events;
using Newbe.Claptrap;
using Newbe.Claptrap.Dapr;
namespace HelloClaptrap.Actors.AuctionItem
{
    [Actor(TypeName = ClaptrapCodes.AuctionItemActor)]
    [ClaptrapStateInitialFactoryHandler(typeof(AuctionItemActorInitialStateDataFactory))]
    [ClaptrapEventHandler(typeof(NewBidderEventHandler), ClaptrapCodes.NewBidderEvent)]
    public class AuctionItemActor : ClaptrapBoxActor<AuctionItemState>, IAuctionItemActor
    {
        private readonly IClock _clock;
        public AuctionItemActor(
            ActorHost actorHost,
            IClaptrapActorCommonService claptrapActorCommonService,
            IClock clock) : base(actorHost, claptrapActorCommonService)
        {
            _clock = clock;
        }
        public Task<TryBiddingResult> TryBidding(TryBiddingInput input)
        {
            var status = GetStatusCore();
            if (status != AuctionItemStatus.OnSell)
            {
                return Task.FromResult(CreateResult(false));
            }
            if (input.Price <= GetTopPrice())
            {
                return Task.FromResult(CreateResult(false));
            }
            return HandleCoreAsync();
            async Task<TryBiddingResult> HandleCoreAsync()
            {
                var dataEvent = this.CreateEvent(new NewBidderEvent
                {
                    Price = input.Price,
                    UserId = input.UserId
                });
                await Claptrap.HandleEventAsync(dataEvent);
                return CreateResult(true);
            }
            TryBiddingResult CreateResult(bool success)
            {
                return new()
                {
                    Success = success,
                    NowPrice = GetTopPrice(),
                    UserId = input.UserId,
                    AuctionItemStatus = status
                };
            }
            decimal GetTopPrice()
            {
                return StateData.BiddingRecords?.Any() == true
                    ? StateData.BiddingRecords.First().Key
                    : StateData.BasePrice;
            }
        }
    }
}
这段代码表明:
- 在生成事件之前可以通过 Claptrap State 对数据进行验证,以决定要不要产生下一步的事件。这是非常有必要的,因为这样可以将没必要产生的事件拒之门外。不论从运行逻辑、持久化空间还是执行效率方面都是非常必要的。
- 经过了必要的验证后,便可以通过 this.CreateEvent创建一个事件。这是一个扩展方法,其中对 Event 的一些基础信息进行了构建。而开发者只需要关心自定义的业务数据部分即可。例如NewBidderEvent就是开发者需要关心的业务数据。
- 事件创建完成之后,便可以通过 Claptrap 对象的 HandleEventAsync方法保存并执行这个方法。在这个方法当中 Claptrap 将会把事件进行持久化,并且调用 Handler 来更新 Claptrap 的 State。下文将会描述如何编写 Handler
- 调用过 HandleEventAsync之后,如果没有任何错误,则表明事件已经成功持久化了。并且可以认为 Claptrap 中的 State 已经正确更新。故而,此时可以从 State 中读取最新的数据返回给调用方。
Handler 层
Handler 层负责执行事件的业务逻辑,并且将数据更新到 State 中。由于 Event 和 State 都是内存中的对象,因此。Handler 的代码实现一般非常的简单。下面就是当触发 NewBidderEvent 时所调用的 Handler。
using System.Threading.Tasks;
using HelloClaptrap.Models.AuctionItem;
using HelloClaptrap.Models.AuctionItem.Events;
using Newbe.Claptrap;
namespace HelloClaptrap.Actors.AuctionItem.Events
{
    public class NewBidderEventHandler
        : NormalEventHandler<AuctionItemState, NewBidderEvent>
    {
        private readonly IClock _clock;
        public NewBidderEventHandler(
            IClock clock)
        {
            _clock = clock;
        }
        public override ValueTask HandleEvent(AuctionItemState stateData,
            NewBidderEvent eventData,
            IEventContext eventContext)
        {
            if (stateData.BiddingRecords == null)
            {
                stateData.InitBiddingRecords();
            }
            var records = stateData.BiddingRecords;
            records.Add(eventData.Price, new BiddingRecord
            {
                Price = eventData.Price,
                BiddingTime = _clock.UtcNow,
                UserId = eventData.UserId
            });
            stateData.BiddingRecords = records;
            return ValueTask.CompletedTask;
        }
    }
}
这段代码表明:
- NewBidderEventHandler继承了- NormalEventHandler作为基类,这主要是为了简化 Handler 的实现而添加的辅助类。其泛型参数分别是对应 Claptrap 的 State 类型和 Event 的 EventData 类型。
- Handler 实现了继承自基类 NormalEventHandler的HandleEvent方法。在这个方法中主要是为了对 State 进行更新。
除了以上显而易见的代码内容之外,还有一些关于 Handler 重要的运行机制必须在此处说明:
- Handler 需要对应的 Actor 类型上标记才会被使用。AuctionItemActor 中 [ClaptrapEventHandler(typeof(NewBidderEventHandler), ClaptrapCodes.NewBidderEvent)]就起到了这个作用。
- Handler 实现了 IDispose和IAsyncDispose接口。这表明,Handler 将会在处理事件时按需创建。您可以参见《TODO Claptrap 系统中各对象生命周期》中的说明。
- 由于事件溯源机制的存在,开发者在编写 Handler 时要充分考虑 HandleEvent方法中逻辑的幂等性。换句话说,您必须确保相同的参数传入HandleEvent方法后得到的结果应该完全一样。否则,当进行实践溯源时将会发生意想不到的结果。您可以参见《TODO 事件与状态的工作原理》中的说明。
有了 Handler 层,便可以通过事件实现对 State 的更新操作。
小结
本篇,我们介绍了 Claptrap 项目中主要的项目结构层次和关键组件。通过对这些组件的了解,开发者已经能够掌握如何公开 API、生成事件和更新状态。这也就是最简单的使用 Claptrap 的必要步骤。
下一步,我们将介绍如何使用 Minion。