# MASA.EShop **Repository Path**: CAPFNT/MASA.EShop ## Basic Information - **Project Name**: MASA.EShop - **Description**: Masa框架 官方示例 - **Primary Language**: C# - **License**: MIT - **Default Branch**: develop - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2022-11-08 - **Last Updated**: 2022-12-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 中 | [EN](README.md) #
Masa.EShop
# 介绍 基于 eShopOnDapr 的`.Net Core`分布式应用程序示例,由[Masa.BuildingBlocks](https://github.com/masastack/Masa.BuildingBlocks), [Masa.Contrib](https://github.com/masastack/Masa.Contrib), [Masa.Utils](https://github.com/masastack/Masa.Utils),[Dapr](https://github.com/dapr/dapr)提供支持。 ## 目录结构 ``` Masa.EShop ├── dapr │ ├── components dapr本地组件定义目录 │ │ ├── pubsub.yaml 发布订阅配置文件 │ │ └── statestore.yaml 状态管理配置文件 ├── src 源文件目录 │ ├── Api │ │ ├── Masa.EShop.Api.Caller Caller调用封装 │ │ └── Masa.EShop.Api.Open BFF层,提供接口给Web.Client │ ├── Contracts 公用元素提取,如服务间通信的Event Class │ │ ├── Masa.EShop.Contracts.Basket │ │ ├── Masa.EShop.Contracts.Catalog │ │ ├── Masa.EShop.Contracts.Ordering │ │ └── Masa.EShop.Contracts.Payment │ ├── Services 服务拆分 │ │ ├── Masa.EShop.Services.Basket │ │ ├── Masa.EShop.Services.Catalog │ │ ├── Masa.EShop.Services.Ordering │ │ └── Masa.EShop.Services.Payment │ ├── Web │ │ ├── Masa.EShop.Web.Admin │ │ └── Masa.EShop.Web.Client ├── test | └── Masa.EShop.Services.Catalog.Tests ├── docker-compose docker-compose 服务配置 │ ├── Masa.EShop.Web.Admin │ └── Masa.EShop.Web.Client ├── .gitignore git提交的忽略文件 ├── LICENSE 项目许可 ├── .dockerignore docker构建的忽略文件 └── README.md 项目说明文件 ``` ## 项目结构 ![结构图](img/eshop.png) ## 项目架构 ![架构图](img/eshop-architectureks.png) ## 快速入门 - 准备工作 - Docker - VS 2022 - .Net 6.0 - Dapr - 启动项目 - VS 2022(推荐) 设置 docker-compose 为启动项目,Ctrl + F5 启动。 ![vs-run](img/vs_run.png) 启动后可以看到容器视图的对应输出 ![vs-result](img/vs_result.png) - CLI 项目根目录下执行命令 ``` docker-compose build docker-compose up ``` 启动后效果 ![cli-result](img/cli_result.png) - VS Code (Todo) - 启动效果 Baseket Service: http://localhost:8081/swagger/index.html Catalog Service: http://localhost:8082/swagger/index.html Ordering Service: http://localhost:8083/swagger/index.html Payment Service: http://localhost:8084/swagger/index.html Admin Web: empty Client Web: http://localhost:8090/catalog ## 特性 #### MinimalAPI 项目中的服务使用 .Net 6.0 新增的 Minimal API 方式代替原有的 Web API 实现 > 更多 Minimal API 内容参考[mvc-to-minimal-apis-aspnet-6](https://benfoster.io/blog/mvc-to-minimal-apis-aspnet-6/) ```C# var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/api/v1/helloworld", ()=>"Hello World"); app.Run(); ``` `Masa.Contrib.Service.MinimalAPIs`对 Minimal API 进一步封装, 修改代码为: ```C# var builder = WebApplication.CreateBuilder(args); var app = builder.Services.AddServices(builder); app.Run(); ``` ```C# public class HelloService : ServiceBase { public HelloService(IServiceCollection services): base(services) => App.MapGet("/api/v1/helloworld", ()=>"Hello World")); } ``` > 增加了 ServiceBase 类(相当于 ControllerBase),使用时定义自己的 Service 类(相当于 Controller),在构造函数中维护路由注册。`AddServices(builder)`方法会找到所有服务类完成注册。继承 ServiceBase 类为单例模式,构造函数注入只可以注入单例,如 Repostory 等应该借助 FromService 实现方法注入。 #### Dapr 官方 Dapr 使用介绍,Masa.Contrib 封装的 Dapr 实现参考了 Event 部分 更多 Dapr 内容参考:https://docs.microsoft.com/zh-cn/dotnet/architecture/dapr-for-net-developers/ 1. 添加 Dapr ```C# builder.Services.AddDaprClient(); ... app.UseRouting(); app.UseCloudEvents(); app.UseEndpoints(endpoints => { endpoints.MapSubscribeHandler(); }); ``` 2. 发布事件 ```C# var @event = new OrderStatusChangedToValidatedIntegrationEvent(); await _daprClient.PublishEventAsync ( "pubsub", nameof(OrderStatusChangedToValidatedIntegrationEvent), @event ); ``` 3. 订阅事件 ```C# [Topic("pubsub", nameof(OrderStatusChangedToValidatedIntegrationEvent)] public async Task OrderStatusChangedToValidatedAsync( OrderStatusChangedToValidatedIntegrationEvent integrationEvent, [FromServices] ILogger logger) { logger.LogInformation("----- integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", integrationEvent.Id, Program.AppName, integrationEvent); } ``` > Topic 的第一个参数 pubsub 为配置文件 pubsub.yaml 中的 name #### Actor 1. 项目中增加 Actor 支持 ```C# app.UseEndpoints(endpoint => { ... endpoint.MapActorsHandlers(); //Actor 支持 }); ``` 2. 定义 Actor 接口,继承 IActor。 ```C# public interface IOrderingProcessActor : IActor { ``` 3. 实现`IOrderingProcessActor`,并继承`Actor`类。示例项目还实现了`IRemindable`接口,实现该接口后通过方法`RegisterReminderAsync`完成注册提醒。 ```C# public class OrderingProcessActor : Actor, IOrderingProcessActor, IRemindable { //todo } ``` 4. 注册 Actor ```C# builder.Services.AddActors(options => { options.Actors.RegisterActor(); }); ``` 5. Actor 调用代码 ```C# var actorId = new ActorId(order.Id.ToString()); var actor = ActorProxy.Create(actorId, nameof(OrderingProcessActor)); ``` #### EventBus 仅支持发送进程内事件 1. 添加 EventBus ```C# builder.Services.AddEventBus(); ``` 2. 自定义 Event ```C# public class DemoEvent : Event { //todo 自定义属性事件参数 } ``` 3. 发送 Event ```C# IEventBus eventBus; await eventBus.PublishAsync(new DemoEvent()); ``` 4. 处理事件 ```C# [EventHandler] public async Task DemoHandleAsync(DemoEvent @event) { //todo } ``` #### IntegrationEventBus 发送跨进程事件,但当同时添加 EventBus 时,也支持进程内事件 1. 添加 IntegrationEventBus ```C# builder.Services .AddDaprEventBus(); // .AddDaprEventBus(options=>{ // //todo // options.UseEventBus();//添加EventBus // }); ``` 2. 自定义 Event ```C# public class DemoIntegrationEvent : IntegrationEvent { public override string Topic { get; set; } = nameof(DemoIntegrationEvent); //todo 自定义属性事件参数 } ``` > Topic 属性值为 Dapr pub/sub 相关特性 TopicAttribute 第二个参数的值 3. 发送 Event ```C# public class DemoService { private readonly IIntegrationEventBus _eventBus; public DemoService(IIntegrationEventBus eventBus) { _eventBus = eventBus; } //todo public async Task DemoPublish() { //todo await _eventBus.PublishAsync(new DemoIntegrationEvent()); } } ``` 4. 处理事件 ```C# [Topic("pubsub", nameof(DemoIntegrationEvent))] public async Task DemoIntegrationEventHandleAsync(DemoIntegrationEvent @event) { //todo } ``` #### CQRS 更多关于 CQRS 文档请参考:https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs ##### Query 1. 定义 Query ```c# public class CatalogItemQuery : Query> { public string Name { get; set; } = default!; public override List Result { get; set; } = default!; } ``` 2. 添加 QueryHandler, 例: ```c# public class CatalogQueryHandler { private readonly ICatalogItemRepository _catalogItemRepository; public CatalogQueryHandler(ICatalogItemRepository catalogItemRepository) => _catalogItemRepository = catalogItemRepository; [EventHandler] public async Task ItemsWithNameAsync(CatalogItemQuery query) { query.Result = await _catalogItemRepository.GetListAsync(query.Name); } } ``` 3. 发送 Query ```C# IEventBus eventBus; await eventBus.PublishAsync(new CatalogItemQuery(){ Name = "Rolex" });//进程内使用IEventBus ``` ##### Command 1. 定义 Command ```c# public class CreateCatalogItemCommand : Command { public string Name { get; set; } = default!; //todo } ``` 2. 添加 CommandHandler, 例: ```c# public class CatalogCommandHandler { private readonly ICatalogItemRepository _catalogItemRepository; public CatalogCommandHandler(ICatalogItemRepository catalogItemRepository) => _catalogItemRepository = catalogItemRepository; [EventHandler] public async Task CreateCatalogItemAsync(CreateCatalogItemCommand command) { //todo } } ``` 3. 发送 Command ```C# IEventBus eventBus; await eventBus.PublishAsync(new CreateCatalogItemCommand());//进程内使用IEventBus ``` #### DDD DDD 更多内容参考:https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice 既可以可发送进程内事件、也可发送跨进程事件 1. 添加 DomainEventBus ```c# .AddDomainEventBus(options => { options.UseEventBus()//使用进程内事件 .UseUow(dbOptions => dbOptions.UseSqlServer("server=masa.eshop.services.eshop.database;uid=sa;pwd=P@ssw0rd;database=payment"))//使用工作单元 .UseDaprEventBus()//使用跨进程事件 .UseEventLog() .UseRepository();//使用Repository的EF版实现 }) ``` 2. 定义 DomainCommand( 进程内 ) ```C# //校验支付的Command, 需要继承DomainCommand, 如果是查询, 则需要继承DomainQuery<> public class OrderStatusChangedToValidatedCommand : DomainCommand { public Guid OrderId { get; set; } } ``` 3. 发送 DomainCommand ```C# IDomainEventBus domainEventBus; await domainEventBus.PublishAsync(new OrderStatusChangedToValidatedCommand() { OrderId = "OrderId" });//发送DomainCommand ``` 4. 添加 Handler ```C# [EventHandler] public async Task ValidatedHandleAsync(OrderStatusChangedToValidatedCommand command) { //todo } ``` 5. 定义 DomainEvent(跨进程) ```c# public class OrderPaymentSucceededDomainEvent : IntegrationDomainEvent { public Guid OrderId { get; init; } public override string Topic { get; set; } = nameof(OrderPaymentSucceededIntegrationEvent); private OrderPaymentSucceededDomainEvent() { } public OrderPaymentSucceededDomainEvent(Guid orderId) => OrderId = orderId; } public class OrderPaymentFailedDomainEvent : IntegrationDomainEvent { public Guid OrderId { get; init; } public override string Topic { get; set; } = nameof(OrderPaymentFailedIntegrationEvent); private OrderPaymentFailedDomainEvent() { } public OrderPaymentFailedDomainEvent(Guid orderId) => OrderId = orderId; } ``` 6. 定义领域服务并发送 IntegrationDomainEvent(跨进程) ```c# public class PaymentDomainService : DomainService { private readonly ILogger _logger; public PaymentDomainService(IDomainEventBus eventBus, ILogger logger) : base(eventBus) => _logger = logger; public async Task StatusChangedAsync(Aggregate.Payment payment) { IIntegrationDomainEvent orderPaymentDomainEvent; if (payment.Succeeded) { orderPaymentDomainEvent = new OrderPaymentSucceededDomainEvent(payment.OrderId); } else { orderPaymentDomainEvent = new OrderPaymentFailedDomainEvent(payment.OrderId); } _logger.LogInformation("----- Publishing integration event: {IntegrationEventId} from {AppName} - ({@IntegrationEvent})", orderPaymentDomainEvent.Id, Program.AppName, orderPaymentDomainEvent); await EventBus.PublishAsync(orderPaymentDomainEvent);//用于发送DomainEvent } } ``` ## 服务说明 #### Masa.EShop.Services.Basket 1. 添加[MinimalAPI](####MinimalAPI) 2. 添加、使用[Dapr](####Dapr) #### Masa.EShop.Services.Catalog 1. 添加[MinimalAPI](####MinimalAPI) 2. 添加[DaprEventBus](####IntegrationEventBus) ```c# builder.Services .AddDaprEventBus(options => { options.UseEventBus()//使用进程内事件 .UseUow(dbOptions => dbOptions.UseSqlServer("server=masa.eshop.services.eshop.database;uid=sa;pwd=P@ssw0rd;database=catalog"))//使用工作单元 .UseEventLog();//将CatalogDbContext上下文交于事件日志使用, CatalogDbContext需要继承IntegrationEventLogContext }) ``` 3. 使用[CQRS](####CQRS) #### Masa.EShop.Services.Ordering 1. 添加[MinimalAPI](####MinimalAPI) 2. 添加[DaprEventBus](####IntegrationEventBus) ```C# builder.Services     .AddMasaDbContext(dbOptions => dbOptions.UseSqlServer("Data Source=masa.eshop.services.eshop.database;uid=sa;pwd=P@ssw0rd;database=order")) .AddDaprEventBus(options => { options.UseEventBus().UseEventLog(); }) ``` 3. 使用[CQRS](####CQRS) 4. 添加[Actor](####Actor) 5. 修改 docker-compse 文件 docker-compose.yml 中增加 dapr 服务; ```yaml dapr-placement: image: "daprio/dapr:1.4.0" ``` docker-compose.override.yml 中增加具体命令和端口映射 ```yaml dapr-placement: command: ["./placement", "-port", "50000", "-log-level", "debug"] ports: - "50000:50000" ``` 对应的 ordering.dapr 服务上增加命令 ```yaml "-placement-host-address", "dapr-placement:50000" ``` #### Masa.EShop.Services.Payment 1. 添加[MinimalAPI](####MinimalAPI) 2. 添加[DomainEventBus](####DDD) ```C# builder.Services .AddDomainEventBus(options => { options.UseEventBus()//使用进程内事件 .UseUow(dbOptions => dbOptions.UseSqlServer("server=masa.eshop.services.eshop.database;uid=sa;pwd=P@ssw0rd;database=payment")) .UseDaprEventBus()///使用跨进程事件 .UseEventLog() .UseRepository();//使用Repository的EF版实现 }) ``` 3. 使用[CQRS](####CQRS) 4. 使用[DDD](####DDD) # 功能介绍 待补充 # Nuget 包介绍 ```c# Install-Package Masa.Contrib.Service.MinimalAPIs //MinimalAPI使用 ``` ```c# Install-Package Masa.Contrib.Dispatcher.Events //发送进程内消息 ``` ```c# Install-Package Masa.Contrib.Dispatcher.IntegrationEvents.Dapr //发送跨进程消息使用 Install-Package Masa.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF //记录跨进程消息日志 ``` ```c# Install-Package Masa.Contrib.Data.UoW.EF //工作单元,确保事务的一致性 ``` ```c# Install-Package Masa.Contrib.ReadWriteSpliting.Cqrs //CQRS实现 ``` ```c# Install-Package Masa.BuildingBlocks.Ddd.Domain //DDD相关实现 Install-Package Masa.Contribs.Ddd.Domain.Repository.EF //Repository实现 ``` ## 交流 | QQ group | WX public account | WX Customer Service | | ----------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | ![masa.blazor-qq](img/masa.blazor-qq-group.png) | ![masa.blazor-weixin](img/masa.blazor-wechat-public-account.png) | ![masa.blazor-weixin](img/masa.blazor-wechat-customer-service.png) |