The commands in cqrs are discussed in the previous articles, and the domain events in cqrs are discussed in this article ).
Concept
First, review the execution process of a UI operation in cqrs:
First, the user clicks a button in the UI, and then the UI Layer constructs a corresponding command object and puts it in commandbus for execution. During the Command Execution Process, classes and methods in the domain model are called, and domain events are generated at this time. They are called "Domain" events because they are generated in the domain model. This can be illustrated in the following figure (unitofworkcontext is ignored first ):
It can also be seen that the calling of the domain model is "wrapped" in the context of command execution. Therefore, all operations on the UI Layer are just creating commands, and then throwing the command to commandbus, instead of calling the classes and methods in the domain model directly.
Basic implementation
The implementation of domain events is somewhat similar to the implementation of command, but note that each command can only correspond to one commandexecutor, while a domain event can be bound to multiple event processors (eventhandler ). The following is an implementation of an initial version.Code:
View code
/// <Summary> /// Mark interface (Marker Interface), which must be implemented for all domain events. /// </Summary> Public Interface Ievent {} /// <Summary> /// Event processor interface, which must be implemented by all event processors. /// </Summary> Public Interface Ieventhandler < In Tevent> Where Tevent: ievent { /// <Summary> /// Process events. /// </Summary> Void Handle (tevent evnt );} /// <Summary> /// /// </Summary> Public Static Class Eventbus { Public Static Void Publish <tevent> (Tevent evnt) Where Tevent: ievent { // Obtain all the event processors bound to the input event type, traverse and execute }}
Suppose there is a demand: an online bookstore has a new book. We want to send a promotion email for the new book to the registered user after adding the book to the database.
Then we will have an addbookcommand during implementation. When this command is executed, bookaddedevent will be triggered, and an eventhandler in the system will be bound to this event, it sends the information of the new book to the registered user by email (other event handler may be available, such as the event handler used to update the website statistics ).
The addbookcommand will not be repeated. For details, refer to the "command implementation" article. The Code of bookaddedevent is as follows, which implements the ievent interface:
View code
Public ClassBookaddedevent: ievent {Public StringBookisbn {Get;Set;}Public DecimalPrice {Get;Set;}}
The corresponding bookaddedeventhandler code is as follows:
View code
Public ClassBookaddedeventhandler: ieventhandler <bookaddedevent>{Public VoidHandle (bookaddedevent evnt ){VaRMSG ="New book! ISBN:"+ Evnt. bookisbn +", Price:"+Evnt. price;//Send emails to users}}
Different from commands, commands express an operation to be executed, and events express an event that has occurred. Therefore, the naming of event classes is in the past. Like commands, the class names of domain events have clear semantics. Therefore, the principle of "never reuse at will" in "command implementation" applies to domain events.
Improvement implementation
The above implementation ignores a very important issue: the bookaddedeventhandler sent an email to the user, and domain events were generated during the call of the domain model, this means that the database transaction has not been committed when sending the mail. What if the database transaction commit fails? The new book is not added, but the email is sent out, which is unacceptable. Therefore, for event handler such as sending emails, we must ensure that they are executed only after the transaction is committed successfully.
However, we cannot put all event handler into execution after the event is submitted successfully. For example, after adding a new book, we need to add one to the total number of books in the website statistics (assuming that the statistics are stored in a table, this statistical information can be considered as readmodel in cqrs ), at this time, the increase in the total number of books should be in the same database transaction as the addition of books. For such eventhandler, it should be executed immediately when domain events are triggered.
Therefore, we can make some changes to the above domain events: eventhandler is divided into two types, one is normal direct execution of eventhandler, one is postcommiteventhandler that is executed only after the database transaction is committed successfully. We will introduce unitofworkcontext. When the command starts to be executed, we will create a new unitofworkcontext object which will always exist during the command execution. During command execution, once a domain event is triggered, we immediately execute all the common eventhandler bound to the event, and then add the event to the current unitofworkcontext, after unitofwork is submitted successfully, traverse all the domain events added to the current unitofworkcontext and execute the corresponding postcommiteventhandler one by one. After the command is executed, close the current unitofworkcontext. The related code is as follows (for the complete code, see the taro project mentioned at the end of this article ):
First, we need to divide the eventhandler interface into two types: ieventhandler <tevent> and ipostcommiteventhandler <tevent>:
View code
Public Interface Ieventhandler < In Tevent> Where Tevent: ievent { /// <Summary> /// Process events. /// </Summary> Void Handle (tevent evnt );} Public Interface Ipostcommiteventhandler < In Tevent> Where Tevent: ievent { Void Handle (tevent evnt );}
Then there is unitofwork (the unimportant code has been removed ):
View code
Public Abstract Class Abstractunitofwork: iunitofwork { // Ieventhandlerfinder is used to obtain all eventhandler bound to the event. Private Ieventhandlerfinder _ eventhandlerfinder; Public Icollection <ievent> uncommittedevents { Get ; Private Set ;} Public Void Commit () {commitchanges (); // After the database transaction is committed, execute all ipostcommiteventhandler Invokepostcommithandlers ();} Protected Abstract Void Commitchanges (); Protected Virtual Void Invokepostcommithandlers (){ // Traverse domain events and execute the corresponding ipostcommiteventhandler Foreach ( VaR Evnt In Uncommittedevents ){ Foreach ( VaR Handler In _ Eventhandlerfinder. findpostcommithandlers (evnt) {eventhandlerinvoker. Invoke (handler, evnt) ;}} uncommittedevents. Clear ();}}
Then there is unitofworkcontext, which is very simple and implemented using threadstatic:
View code
Public Static Class Unitofworkcontext {[threadstatic] Private Static Iunitofwork _ current; Public Static Iunitofwork current { Get { Return _ Current ;}} Public Static Void Open (iunitofwork unitofwork) {_ current = Unitofwork ;} Public Static Void Close () {_ current = Null ;}}
We also need a abstractcommandexecutor abstract base class to control the enabling and disabling of unitofworkcontext (all commandexecutor must inherit abstractcommandexecutor ):
View code
Public Abstract Class Abstractcommandexecutor <tcommand>: icommandexecutor <tcommand> Where Tcommand: icommand { Protected Func <iunitofwork> getunitofwork { Get ; Private Set ;} Protected Abstractcommandexecutor (func <iunitofwork> Getunitofwork) {require. notnull (getunitofwork, " Getunitofwork " ); Getunitofwork = Getunitofwork ;} Public Void Execute (tcommand cmd ){ Using ( VaR Uow = Getunitofwork () {unitofworkcontext. Open (uow ); Try {Execute (uow, CMD );} Finally {Unitofworkcontext. Close ();}}} Protected Abstract Void Execute (iunitofwork unitofwork, tcommand cmd );}
To enable the domain model to easily trigger domain events, we add a static domainevent class. In its apply method, we first execute the ordinary eventhandler and then add the event to the current unitofworkcontext:
View code
Public Static Class Domainevent { Public Static Void Apply <tevent> (Tevent evnt) Where Tevent: ievent { VaR Handlerfinder = Eventhandlerfinders. Current; // Locate all common eventhandler bound to the event and execute Foreach ( VaR Handler In Handlerfinder. findprecommithandlers (evnt) {eventhandlerinvoker. Invoke (handler, evnt );} VaR Unitofwork = Unitofworkcontext. Current; If (Unitofwork = Null ) Throw New Invalidoperationexception ( " Current unit of work context is null. Domain events can only be applied inside a unit of work context. " ); // Add domain events to the current unitofworkcontext Unitofwork. uncommittedevents. Add (evnt );}}
Finally, we can call the domain model as follows:
//Library repository, pseudocodePublic ClassBookwarehouse {PublicIlist <book> books {Get;Private Set;}Public VoidAddbook (Book) {books. Add (book );//Triggering domain eventsDomainevent. Apply (NewBookaddedevent (book ));}}
Summary
This article discusses the basic concepts and implementation of domain events, and makes further improvements to solve the problem that some eventhandler can execute after the database transaction is committed successfully.
The introduction of domain events is extremely important. It makes the domain model more pure and decouples different logics. If there is no domain event, the code for receiving the new book into the database and the Code for sending the promotion email and changing the website statistics must be coupled. Domain events are not private products of cqrs. They can be separated from cqrs and exist independently as lightweight components.
So far, the main components of cqrs have been discussed. Other advanced topics, such as event sourcing and asynchronous event distribution, will not be discussed in this series. In the next article, we will use a mini cqrs framework (Taro) and a running online bookstore (bookstore) Sample project to demonstrate how to apply cqrs in actual projects.
Welcome to the discussion.