summary
The publish subscribe mode is used to decouple different architecture levels, and can also be used to isolate the interaction between businesses
advantage:
- loose coupling
- crosscutting concern
- Testability
- event driven
<!-- more -->
Publish subscribe mode
The publisher sends the message to the subscriber through the dispatching center. The dispatching center solves the relationship between the publisher and the subscriber to ensure that the message can be delivered to the subscriber.
Publishers and subscribers do not know each other. Publishers only publish messages to the dispatching center, while subscribers only care about the types of messages they subscribe to
Multi subscriber order preserving execution
It is true that similar statements are rarely seen in common publish subscribe patterns. However, in the actual business, we will have similar requirements. For a message, the scheduling center coordinates multiple subscribers to execute the message in order, and can also pass the message processed by the previous subscriber to the next subscriber. In this way, we can not only retain the characteristics of publish subscribe mode, but also have the characteristics of sequential execution logic.
A little thought: if the configuration of EventBus supports dynamic adjustment, can the execution order of business be dynamically arranged and combined?
In other words, it may provide a possibility for in-process workflow
Event Sourcing
An event driven architecture pattern that can be used for auditing and traceability
- Event driven architecture
- Take events as facts
- The view of business data generated by event calculation can be persistent or non persistent
CQRS (separation of responsibilities for command query)
CQRS is an architecture pattern that can separate the implementation of change model and query model
Event Sourcing & CQRS
Event traceability can cooperate well with CQRS
- While persisting events to the Event Store in the Command Handler, a final view is calculated in real time to the View DB for query and display
- In Query, you can get the latest status through the View DB, replay events through the Event Store to verify the View or use it for more rigorous business
Saga
Saga is a long-lived transaction, which is decomposed into a set of sub transactions that can be interleaved. Each of these sub transactions is a real transaction that maintains database consistency
- Each Saga consists of a series of sub transaction ti
- Each Ti has a corresponding compensation action Ci, which is used to cancel the result caused by Ti
Two execution sequences
- T1, T2, T3...[Tx retry]...,Tn
- T1, T2, ..., Tj, Cj,..., C2, C1
Two recovery strategies
- backward recovery, which compensates all completed transactions if any sub transaction fails. That is, the second execution sequence mentioned above, where j is the wrong sub transaction. The effect of this method is to undo all previous successful sub transactions and undo the execution results of the whole Saga
- Forward recovery, forward recovery, retry failed transactions, assuming that each sub transaction will succeed in the end. For scenarios that must succeed, the execution sequence is similar to this: T1, T2, TJ (failure), TJ (retry), Tn, where j is the sub transaction where the error occurred. Ci is not required in this case
Class view of BuildingBlocks
As an interface standard, BuildingBlocks does not have too many interference implementation methods. It only retains the most basic function flow restrictions to achieve the minimum function set of EventBus. Whether to implement the subscription relationship based on interface or feature is left to Contrib to decide.
event
Publish / subscribe for local events
- IEvent: event interface. IEvent < tresult > is the basic event interface with return value
- Ieventhandler < tevent >: event handler interface. Isagaeventhandler < tevent > provides basic interface requirements for the implementation of Saga
- Imidleware < tevent >: a middleware interface that allows pre-processing actions and closing actions after time execution to be mounted before event execution
- IEventBus: event bus interface, which is used to send events and provide subscription relationship maintenance and additional function execution
Integration event
Publish / subscribe for cross process events
- Integration eventlog: the integration event log is used to implement the message model of the local message table
- Iiintegrationeventlogservice: integrated event log service interface
- ITopic: publish / subscribe topics
- Iiintegrationevent: integration event interface
- Iiintegrationeventbus: integrated event bus, which is used for the event bus of cross process calls
CQRS
Used to separate the implementation of the change model from the query model
- Iquery < tresult >: query interface
- Iqueryhandler < tcommand, tresult >: query processor interface
- ICommand: an interface that can be used to add, delete, modify and other instructions
- Icommandhandler < tcommand >: instruction processor interface
Event Bus
To complete the above functions, we need to use EventBus, which needs the following basic functions
- Receive event
- Maintain subscription relationship
- Forwarding events
Receive and forward events
In fact, these two functions can be combined into one interface. The publisher calls Publish, and then the Event Bus forwards it according to the subscription relationship
Maintain subscription relationship
Yes Net project, our common methods for scanning automatic registration are interfaces and features
MediatR supports the interface to scan the event subscription relationship, for example: irequesthandler <, >
public class PingHandler : IRequestHandler<Ping, string> { public Task<string> Handle(Ping request, CancellationToken cancellationToken) { return Task.FromResult("Pong"); } }
If your code cleanliness is not too high, maybe you hope so
public class NetHandler : IRequestHandler<Ping, string>, IRequestHandler<Telnet, string> { public Task<string> Handle(Ping request, CancellationToken cancellationToken) { return Task.FromResult("Pong"); } public Task<string> Handle(Telnet request, CancellationToken cancellationToken) { return Task.FromResult("Success"); } }
Looks okay? What if a lot?
Is there any way to solve this problem?
characteristic! Let's take an example
public class NetHandler { [EventHandler] public Task PingAsync(PingEvent @event) { //TODO } [EventHandler] public Task TelnetAsync(TelnetEvent @event) { //TODO } }
It seems that we have found a way out
Multi subscriber order preserving execution
It is true that sequential execution scenarios can be satisfied by pushing events layer by layer, but if you are surrounded by a large number of infinite dolls, you may need another way out. See the following example:
public class NetHandler { [EventHandler(0)] public Task PingAsync(PingEvent @event) { //TODO } [EventHandler(1)] public Task LogAsync(PingEvent @event) { //TODO } }
As long as the parameter is the same Event, it will be executed according to the Order of EventHandler.
Saga
What if the execution fails? If one of the two methods cannot be combined with the local transaction because it needs to call the remote service, can you rollback it for me?
Come on, SAGA, let's do another cancellation for you. It also supports retry mechanism and whether to ignore the cancellation of the current step.
Let's preset the scene first:
- Call CheckBalanceAsync to check the balance
- Call WithdrawAsync and throw exception
- Retry WithdrawAsync 3 times
- Call CancelWithdrawAsync
The code is as follows:
public class TransferHandler { [EventHandler(1)] public Task CheckBalanceAsync(TransferEvent @event) { //TODO } [EventHandler(2, FailureLevels.ThrowAndCancel, enableRetry: true, retryTimes: 3)] public Task WithdrawAsync(TransferEvent @event) { //TODO throw new Exception(); } [EventHandler(2, FailureLevels.Ignore, enableRetry: false, isCancel: true)] public Task CancelWithdrawAsync(TransferEvent @event) { //TODO } }
AOP
For a business scenario, add a parameter validation to all commands before execution
We provide middleware, which allows you to do things related to crosscutting concerns like Russian Dolls (. Net Middleware)
public class LoggingMiddleware<TEvent> : IMiddleware<TEvent> where TEvent : notnull, IEvent { private readonly ILogger<LoggingMiddleware<TEvent>> _logger; public LoggingMiddleware(ILogger<LoggingMiddleware<TEvent>> logger) => _logger = logger; public async Task HandleAsync(TEvent @event, EventHandlerDelegate next) { _logger.LogInformation("----- Handling command {EventName} ({@Event})", typeof(TEvent).FullName, @event); await next(); } }
Register DI
builder.Services.AddTransient(typeof(IMiddleware<>), typeof(LoggingMiddleware<>))
Full function list of MASA EventBus
- Receive event
- Maintain subscription relationship - Interface
- Maintain subscription relationships - properties
- Multi subscriber sequential execution
- Forwarding events
- Saga
- AOP
- UoW
- Automatically turn transactions on and off
Integration Event Bus
Event Bus for cross service, supporting final consistency and local message table
Pub/Sub
It provides a Pub Sub interface and provides a default implementation based on Dapr Pub/Sub
Local message table
It provides local message saving and UoW linkage interfaces, and provides default implementation based on EF Core
usage method
Enable Dapr Event Bus
builder.Services .AddDaprEventBus<IntegrationEventLogService>(options=> { options.UseUoW<CatalogDbContext>(dbOptions => dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=Password;database=test")) .UseEventLog<CatalogDbContext>(); ) });
Define Integration Event
public class DemoIntegrationEvent : IntegrationEvent { public override string Topic { get; set; } = nameof(DemoIntegrationEvent);//dapr topic name //todo other properties }
Define DbContext (not required. Defining DbContext can link local message tables with business transactions)
public class CustomDbContext : IntegrationEventLogContext { public DbSet<User> Users { get; set; } = null!; public CustomDbContext(MasaDbContextOptions<CustomDbContext> options) : base(options) { } }
Send Event
IIntegrationEventBus eventBus; // from DI await eventBus.PublishAsync(new DemoIntegrationEvent());
Subscribe to Event (based on the version of Dapr Pub/Sub)
[Topic("pubsub", nameof(DomeIntegrationEvent))] public async Task DomeIntegrationEventHandleAsync(DomeIntegrationEvent @event) { //todo }
Domain Event Bus
The ability to provide both Event Bus and Integration Event Bus in the field allows the event to be sent in real time or triggered at one time when saving
Domain Event Bus is the most complete capability, so using domain Event Bus is equivalent to enabling Event Bus and Integration Event Bus. Within domain Event Bus, event classification will be automatically coordinated to divert to Event Bus and Integration Event Bus
Enable Domain Event Bus
builder.Services .AddDomainEventBus(options => { options.UseEventBus()//Use in-process events .UseUoW<CustomDbContext>(dbOptions => dbOptions.UseSqlServer("server=localhost;uid=sa;pwd=P@ssw0rd;database=idientity")) .UseDaprEventBus<IntegrationEventLogService>()///Use cross-process events .UseEventLog<LocalMessageDbContext>() .UseRepository<CustomDbContext>(); })
Add DomainCommand
Domain Event is an in-process event and integration domainevent is a cross process event
public class RegisterUserSucceededIntegrationEvent : IntegrationDomainEvent { public override string Topic { get; set; } = nameof(RegisterUserSucceededIntegrationEvent); public string Account { get; set; } = default!; } public class RegisterUserSucceededEvent : DomainEvent { public string Account { get; set; } = default!; }
In process event subscription
[EventHandler] public Task RegisterUserHandlerAsync(RegisterUserDomainCommand command) { //TODO }
Cross process event subscription
[Topic("pubsub", nameof(RegisterUserSucceededIntegrationEvent))] public async Task RegisterUserSucceededHandlerAsync(RegisterUserSucceededIntegrationEvent @event) { //todo }
Send DomainCommand
IDomainEventBus eventBus;//from DI await eventBus.PublishAsync(new RegisterUserDomainCommand());
Usage scenario
- Consider legacy system docking
- Walking in clouds and non clouds
- Flow calculation
- Micro service decoupling and cross cluster communication (it is not difficult to change Dapr Pub/Sub to Dapr Binding)
- Some AOP scenarios
summary
Event driven can solve the problems of some specific scenarios. Everything has two sides. Using such a complex pattern in an already simple business scenario will bring a lot of burden.
There is no end to learning.
Open source address
MASA.BuildingBlocks: https://github.com/masastack/MASA.BuildingBlocks
MASA.Contrib: https://github.com/masastack/MASA.Contrib
MASA.Utils: https://github.com/masastack/MASA.Utils
MASA.EShop: https://github.com/masalabs/MASA.EShop
If you are interested in our MASA Framework, whether it is code contribution, use or Issue, please contact us