ไธญ | EN
Masa.EShop
Introduction
A sample .NET Core
distributed application based on eShopOnDapr, powered by MASA.Framework, 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
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
.
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);
}
Topic
first parameterpubsub
is thename
field in thepubsub.yaml
file.
Actor
- 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));
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
}
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
}
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ๅฎ็ฐ
Interactive
QQ group | WX public account | WX Customer Service |
---|---|---|