中 | EN
A sample .NET Core
distributed application based on eShopOnDapr, powered by MASA.Framework, 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.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
Preparation
Startup
VS 2022(Recommended)
Set docker-compose as start project, press Ctrl + F5
to start.
After startup, you can see the container view.
CLI
Run the command in the project root directory.
docker-compose build
docker-compose up
After startup, the output is as follows.
VS Code (Todo)
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
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/
builder.Services.AddDaprClient();
...
app.UseRouting();
app.UseCloudEvents();
app.UseEndpoints(endpoints =>
{
endpoints.MapSubscribeHandler();
});
var @event = new OrderStatusChangedToValidatedIntegrationEvent();
await _daprClient.PublishEventAsync
(
"pubsub",
nameof(OrderStatusChangedToValidatedIntegrationEvent),
@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.
app.UseEndpoints(endpoint =>
{
...
endpoint.MapActorsHandlers();
});
public interface IOrderingProcessActor : IActor
{
IOrderingProcessActor
and inherit the Actor
class. The sample project also implements the IRemindable
interface, and 'RegisterReminderAsync' method.public class OrderingProcessActor : Actor, IOrderingProcessActor, IRemindable
{
//todo
}
builder.Services.AddActors(options =>
{
options.Actors.RegisterActor<OrderingProcessActor>();
});
var actorId = new ActorId(order.Id.ToString());
var actor = ActorProxy.Create<IOrderingProcessActor>(actorId, nameof(OrderingProcessActor));
Only In-Process events.
builder.Services.AddEventBus();
public class DemoEvent : Event
{
//todo 自定义属性事件参数
}
IEventBus eventBus;
await eventBus.PublishAsync(new DemoEvent());
[EventHandler]
public async Task DemoHandleAsync(DemoEvent @event)
{
//todo
}
Cross-Process event, In-Process event also supported when EventBus
is added.
builder.Services
.AddDaprEventBus<IntegrationEventLogService>();
// .AddDaprEventBus<IntegrationEventLogService>(options=>{
// //todo
// options.UseEventBus();//Add EventBus
// });
public class DemoIntegrationEvent : IntegrationEvent
{
public override string Topic { get; set; } = nameof(DemoIntegrationEvent);
//todo
}
Topic
property is the value of the daprTopicAttribute
second parameter.
public class DemoService
{
private readonly IIntegrationEventBus _eventBus;
public DemoService(IIntegrationEventBus eventBus)
{
_eventBus = eventBus;
}
//todo
public async Task DemoPublish()
{
//todo
await _eventBus.PublishAsync(new DemoIntegrationEvent());
}
}
[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
public class CatalogItemQuery : Query<List<CatalogItem>>
{
public string Name { get; set; } = default!;
public override List<CatalogItem> Result { get; set; } = default!;
}
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);
}
}
IEventBus eventBus;// DI is recommended
await eventBus.PublishAsync(new CatalogItemQuery(){
Name = "Rolex"
});
public class CreateCatalogItemCommand : Command
{
public string Name { get; set; } = default!;
//todo
}
public class CatalogCommandHandler
{
private readonly ICatalogItemRepository _catalogItemRepository;
public CatalogCommandHandler(ICatalogItemRepository catalogItemRepository) => _catalogItemRepository = catalogItemRepository;
[EventHandler]
public async Task CreateCatalogItemAsync(CreateCatalogItemCommand command)
{
//todo
}
}
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.
.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版实现
})
To verify payment command, you need to inherit DomainCommand or DomainQuery<>
public class OrderStatusChangedToValidatedCommand : DomainCommand
{
public Guid OrderId { get; set; }
}
IDomainEventBus domainEventBus;
await domainEventBus.PublishAsync(new OrderStatusChangedToValidatedCommand()
{
OrderId = "OrderId"
});
[EventHandler]
public async Task ValidatedHandleAsync(OrderStatusChangedToValidatedCommand command)
{
//todo
}
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;
}
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);
}
}
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>();
})
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"
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实现
QQ group | WX public account | WX Customer Service |
---|---|---|