中 | EN
Introduction
A sample .NET Core distributed application based on eShopOnDapr, powered by MASA.BuildingBlocks, MASA.Contrib, MASA.Utils,Dapr.
Directory Structure
MASA.EShop
├── dapr
│ ├── components dapr local components directory
│ │ ├── pubsub.yaml pub/sub config file
│ │ └── statestore.yaml state management config file
├── src
│ ├── Api
│ │ ├── MASA.EShop.Api.Caller Caller package
│ │ └── MASA.EShop.Api.Open BFF Layer, provide API to Web.Client
│ ├── Contracts Common contracts,like 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
│ ├── MASA.EShop.Web.Admin
│ └── MASA.EShop.Web.Client
├── .gitignore
├── LICENSE
├── .dockerignore
└── README.md
Project Structure
Project Architecture
Getting started
-
Preparation
- Docker
- VS 2022
- .Net 6.0
- Dapr
-
Startup
-
Display after startup(Update later)
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
Features
MinimalAPI
The service in the project uses the Minimal API added in .NET 6 instead of the Web API.
For more Minimal API content reference mvc-to-minimal-apis-aspnet-6
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/api/v1/helloworld", ()=>"Hello World"); app.Run();
MASA.Contrib.Service.MinimalAPIs based on MASA.BuildingBlocks:
Program.cs
var builder = WebApplication.CreateBuilder(args); var app = builder.Services.AddServices(builder); app.Run();
HelloService.cs
public class HelloService : ServiceBase { public HelloService(IServiceCollection services): base(services) => App.MapGet("/api/v1/helloworld", ()=>"Hello World")); }
The
ServiceBaseclass (like ControllerBase) provided byMASA.BuildingBlocksis used to define Service class (like Controller), maintains the route registry in the constructor. TheAddServices(builder)method will auto register all the service classes to DI. Service inherited from ServiceBase issimilar to singleton pattern. Such asRepostory, should be injected with theFromService.
Dapr
The official Dapr implementation, MASA.Contrib references the Event section.
More Dapr content reference: https://docs.microsoft.com/zh-cn/dotnet/architecture/dapr-for-net-developers/
- Add Dapr
builder.Services.AddDaprClient(); ... app.UseRouting(); app.UseCloudEvents(); app.UseEndpoints(endpoints => { endpoints.MapSubscribeHandler(); });
- Publish event
var @event = new OrderStatusChangedToValidatedIntegrationEvent(); await _daprClient.PublishEventAsync ( "pubsub", nameof(OrderStatusChangedToValidatedIntegrationEvent), @event );
- Sub event
[Topic("pubsub", nameof(OrderStatusChangedToValidatedIntegrationEvent)] public async Task OrderStatusChangedToValidatedAsync( OrderStatusChangedToValidatedIntegrationEvent integrationEvent, [FromServices] ILogger<IntegrationEventService> logger) { logger.LogInformation("----- integration event: {IntegrationEventId} at {AppName} - ({@IntegrationEvent})", integrationEvent.Id, Program.AppName, integrationEvent); }
Topicfirst parameterpubsubis thenamefield in thepubsub.yamlfile.
Actor
- Add Actor
app.UseEndpoints(endpoint => { ... endpoint.MapActorsHandlers(); });
- Define actor interface and inherit IActor.
public interface IOrderingProcessActor : IActor {
- Implement
IOrderingProcessActorand inherit theActorclass. The sample project also implements theIRemindableinterface, and 'RegisterReminderAsync' method.
public class OrderingProcessActor : Actor, IOrderingProcessActor, IRemindable { //todo }
- Register Actor
builder.Services.AddActors(options => { options.Actors.RegisterActor<OrderingProcessActor>(); });
- Invoke actor
var actorId = new ActorId(order.Id.ToString()); var actor = ActorProxy.Create<IOrderingProcessActor>(actorId, nameof(OrderingProcessActor));
EventBus
Only In-Process events.
- Add EventBus
builder.Services.AddEventBus();
- Define Event
public class DemoEvent : Event { //todo 自定义属性事件参数 }
- Send Event
IEventBus eventBus; await eventBus.PublishAsync(new DemoEvent());
- Hanle Event
[EventHandler] public async Task DemoHandleAsync(DemoEvent @event) { //todo }
IntegrationEventBus
Cross-Process event, In-Process event also supported when EventBus is added.
- Add IntegrationEventBus
builder.Services .AddDaprEventBus<IntegrationEventLogService>(); // .AddDaprEventBus<IntegrationEventLogService>(options=>{ // //todo // options.UseEventBus();//Add EventBus // });
- Define Event
public class DemoIntegrationEvent : IntegrationEvent { public override string Topic { get; set; } = nameof(DemoIntegrationEvent); //todo }
Topicproperty is the value of the daprTopicAttributesecond parameter.
- Send Event
public class DemoService { private readonly IIntegrationEventBus _eventBus; public DemoService(IIntegrationEventBus eventBus) { _eventBus = eventBus; } //todo public async Task DemoPublish() { //todo await _eventBus.PublishAsync(new DemoIntegrationEvent()); } }
- Handle Event
[Topic("pubsub", nameof(DemoIntegrationEvent))] public async Task DemoIntegrationEventHandleAsync(DemoIntegrationEvent @event) { //todo }
CQRS
More CQRS content reference:https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs
Query
- Define Query
public class CatalogItemQuery : Query<List<CatalogItem>> { public string Name { get; set; } = default!; public override List<CatalogItem> Result { get; set; } = default!; }
- Add QueryHandler:
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); } }
- Send Query
IEventBus eventBus;// DI is recommended await eventBus.PublishAsync(new CatalogItemQuery(){ Name = "Rolex" });
Command
- Define Command
public class CreateCatalogItemCommand : Command { public string Name { get; set; } = default!; //todo }
- Add CommandHandler:
public class CatalogCommandHandler { private readonly ICatalogItemRepository _catalogItemRepository; public CatalogCommandHandler(ICatalogItemRepository catalogItemRepository) => _catalogItemRepository = catalogItemRepository; [EventHandler] public async Task CreateCatalogItemAsync(CreateCatalogItemCommand command) { //todo } }
- 发送 Command
IEventBus eventBus; await eventBus.PublishAsync(new CreateCatalogItemCommand());
DDD
More DDD content reference:https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/ddd-oriented-microservice
Both In-Process and Cross-Process events are supported.
- Add DomainEventBus
.AddDomainEventBus(options => { options.UseEventBus() .UseUow<PaymentDbContext>(dbOptions => dbOptions.UseSqlServer("server=masa.eshop.services.eshop.database;uid=sa;pwd=P@ssw0rd;database=payment")) .UseDaprEventBus<IntegrationEventLogService>() .UseEventLog<PaymentDbContext>() .UseRepository<PaymentDbContext>();//使用Repository的EF版实现 })
- Define DomainCommand(In-Process)
To verify payment command, you need to inherit DomainCommand or DomainQuery<>
public class OrderStatusChangedToValidatedCommand : DomainCommand { public Guid OrderId { get; set; } }
- Send DomainCommand
IDomainEventBus domainEventBus; await domainEventBus.PublishAsync(new OrderStatusChangedToValidatedCommand() { OrderId = "OrderId" });
- Add Handler
[EventHandler] public async Task ValidatedHandleAsync(OrderStatusChangedToValidatedCommand command) { //todo }
- Define DomainEvent(Cross-Process))
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; }
- Define domain service and send IntegrationDomainEvent(Cross-Process)
public class PaymentDomainService : DomainService { private readonly ILogger<PaymentDomainService> _logger; public PaymentDomainService(IDomainEventBus eventBus, ILogger<PaymentDomainService> 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); } }
Service Description
MASA.EShop.Services.Basket
- Add MinimalAPI
- Add and use Dapr
MASA.EShop.Services.Catalog
- Add MinimalAPI
- Add DaprEventBus
builder.Services .AddDaprEventBus<IntegrationEventLogService>(options => { options.UseEventBus() .UseUow<CatalogDbContext>(dbOptions => dbOptions.UseSqlServer("server=masa.eshop.services.eshop.database;uid=sa;pwd=P@ssw0rd;database=catalog")) .UseEventLog<CatalogDbContext>(); })
- Use CQRS
MASA.EShop.Services.Ordering
- Add MinimalAPI
- Add DaprEventBus
builder.Services .AddMasaDbContext<OrderingContext>(dbOptions => dbOptions.UseSqlServer("Data Source=masa.eshop.services.eshop.database;uid=sa;pwd=P@ssw0rd;database=order")) .AddDaprEventBus<IntegrationEventLogService>(options => { options.UseEventBus().UseEventLog<OrderingContext>(); })
docker-compose.yml add dapr service;
dapr-placement: image: "daprio/dapr:1.4.0"
docker-compose.override.yml add command and port mapping.
dapr-placement: command: ["./placement", "-port", "50000", "-log-level", "debug"] ports: - "50000:50000"
ordering.dapr service add command
"-placement-host-address", "dapr-placement:50000"
MASA.EShop.Services.Payment
- Add MinimalAPI
- Add DomainEventBus
builder.Services .AddDomainEventBus(options => { options.UseEventBus() .UseUow<PaymentDbContext>(dbOptions => dbOptions.UseSqlServer("server=masa.eshop.services.eshop.database;uid=sa;pwd=P@ssw0rd;database=payment")) .UseDaprEventBus<IntegrationEventLogService>() .UseEventLog<PaymentDbContext>() .UseRepository<PaymentDbContext>(); })
Function Introduction
Update later
Nuget Package Introduction
Install-Package MASA.Contrib.Service.MinimalAPIs //MinimalAPI
Install-Package MASA.Contrib.Dispatcher.Events //In-Process event
Install-Package MASA.Contrib.Dispatcher.IntegrationEvents.Dapr //Cross-Process event Install-Package MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF //Local message table
Install-Package MASA.Contrib.Data.UoW.EF //EF UoW
Install-Package MASA.Contrib.ReadWriteSpliting.CQRS //CQRS
Install-Package MASA.BuildingBlocks.DDD.Domain //DDD相关实现 Install-Package MASA.Contrib.DDD.Domain.Repository.EF //Repository实现




