中 | EN
A sample .NET Core distributed application based on eShopOnDapr, powered by MASA.BuildingBlocks, MASA.Contrib, MASA.Utils,Dapr.
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.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
-
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
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.
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.
- 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));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
}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
}More CQRS content reference:https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs
- 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"
});- 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());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);
}
}- Add MinimalAPI
- Add and use Dapr
- 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
- Add MinimalAPI
- Add DaprEventBus
builder.Services
.AddDaprEventBus<IntegrationEventLogService>(options =>
{
options.UseEventBus()
.UseUoW<OrderingContext>(dbOptions => dbOptions.UseSqlServer("Data Source=masa.eshop.services.eshop.database;uid=sa;pwd=P@ssw0rd;database=order"))
.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"- 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>();
})Update later
Install-Package MASA.Contrib.Service.MinimalAPIs //MinimalAPIInstall-Package MASA.Contrib.Dispatcher.Events //In-Process eventInstall-Package MASA.Contrib.Dispatcher.IntegrationEvents.Dapr //Cross-Process event
Install-Package MASA.Contrib.Dispatcher.IntegrationEvents.EventLogs.EF //Local message tableInstall-Package MASA.Contrib.Data.UoW.EF //EF UoWInstall-Package MASA.Contrib.ReadWriteSpliting.CQRS //CQRSInstall-Package MASA.BuildingBlocks.DDD.Domain //DDD相关实现
Install-Package MASA.Contrib.DDD.Domain.Repository.EF //Repository实现