中 | 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
ServiceBase
class (like ControllerBase) provided byMASA.BuildingBlocks
is 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);
}
Topic
first parameterpubsub
is thename
field in thepubsub.yaml
file.
- Add Actor
app.UseEndpoints(endpoint =>
{
...
endpoint.MapActorsHandlers();
});
- Define actor interface and inherit IActor.
public interface IOrderingProcessActor : IActor
{
- Implement
IOrderingProcessActor
and inherit theActor
class. The sample project also implements theIRemindable
interface, 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
}
Topic
property is the value of the daprTopicAttribute
second 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 //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实现