Preface
After continuous persistence and efforts, the first real case of Enode 2.0 is finally available. This case is a simple forum. The original intention of this forum is to verify the feasibility of using the Enode framework to develop a real project. For more information about Enode, see this article. This article focuses on how Enode helps us develop an application based on the DDD + cqrs + Event sourcing architecture. This forum uses the Enode and equeue frameworks. equeue is a distributed Message Queue component. The main idea of this component is to refer to Alibaba's rocketmq. When we use equeue, we are not targeting queue, but topics. Equeue is fully implemented in C #. For details about equeue, you can refer to this article.
Enode, equeue, forum open source project address
- Enode Source Address: https://github.com/tangxuehua/enode
- Equeue Open Source Address: https://github.com/tangxuehua/equeue
- Ecommon Open Source Address: https://github.com/tangxuehua/ecommon
- Forum Open Source Address: https://github.com/tangxuehua/forum
- Forum Online address (temporary domain name, will be changed to Enode. Me Later): http://enode.cloudapp.net
- Forum's equeue message data monitoring statistics page: http://enode.cloudapp.net/equeueadmin
In addition, if you want to develop a reference assembly in the project, you can obtain it through nuget. Enter the keyword Enode to view all the related packages, as shown in:
Forum overall Architecture Analysis
Forum adopts the DDD + cqrs + Event sourcing architecture. With the help of Enode, the Forum itself does not need to be designed in terms of technical architecture. Enode can be used directly to complete this architecture. So as long as we understand the Enode architecture, we will know what the Forum architecture is like. The following is the Enode architecture diagram (skip this section if you have understood this diagram ):
It is a data flow diagram of a cqrs architecture. There are two types of UI requests: command and query.
Command is used to write data, and query is used to read data. Writing data and reading data are implemented in different architectures. Data Writing supports synchronous and asynchronous data writing. Reading data is completely implemented using simple and efficient methods. When we want to write data to the system, if you use ASP. net mvc to develop the site, you can create and send a command in the controller. The command will be sent to the Message Queue (in equeue). Then, the subscriber of the message queue, that is, the process that processes the command, will pull these commands and then call the command handler to process the command; when the command handler processes the command, it calls the domain method to complete related business logic operations. Domain is the domain layer in DDD and is responsible for implementing the business logic of the entire system. Then, because it is the event sourcing architecture, any modification to the aggregate root in the domain will generate the corresponding domain event, and the domain event will be first persisted to the eventstore, if no concurrency conflict occurs during persistence, the message queue will be published to the Message Queue (equeue) (publish) and then the consumer of the message queue, that is, the process processing domain events will pull these domain events, and then call the relevant event handler for related updates. For example, some event handler will update the read database (read dB ), some of them generate new commands, which I call Process Manager (SAGA ). When we sometimes need to modify multiple aggregation root for a business scenario, we need Process Manager. Process Manager is responsible for modeling the process. Its principle is to implement an event-driven process. Process Manager processes events and generates the response command to complete interaction between the aggregation root. Generally, we design a process aggregation root and other aggregation Root involved in the process. Process Manager is used to coordinate the interaction between these aggregation root. For more information, see banktransfersample in Enode source code.
Queries are used to display data on the ui or provide data for third-party interfaces. queries have no side effects on the system. We can implement the query end in any way we like. The query targets read dB. As mentioned above, data in read dB is updated through event handler (denormalizer.
Therefore, we can see that the data sources at the command end and query end are completely separated throughout the architecture. The final result of the command end is domain event, and the domain event is persistent in the event store; the data source of the query end is read dB, which can be stored as a relational database. Data Synchronization at both ends of CQ is implemented through domain event.
The biggest benefit of the cqrs architecture is the separation of read and write at the architecture level and data storage level. In this way, we can easily optimize reading or writing separately. In addition, because the event sourcing architecture is used, as long as the domain event is persisted on our command end, all the statuses of this domain are saved. This feature makes our framework have a lot of room for design. For example, we don't have to consider the issue of strong consistency between domain events and business data, because domain events itself is the business data, we can use domain event to restore the domain status at any time. When we want to query the latest domain data, we can use the query end. Of course, because the query end is asynchronously updated, the data on the query end may have a little latency. This is the final consistency we have always talked about (the data at both ends of CQ will eventually be consistent ).
Through the preceding architecture diagram, we know that a command is processed in two phases after it is sent: 1) first processed by a command Service (domain is called to complete the business logic to generate domain events); 2) it is processed by Event Service (responding to domain event, completing read dB updates or generating new commands); understanding these two stages is very useful for understanding the following Forum project structure.
Forum project Structure Analysis
The above is the Forum project structure. The project contains four host projects:
Forum. brokerservice:
This project is used to host the equeue broker. All the command and domain event messages in the forum will be placed on the broker. For example, the command sent by the Controller will be sent to the broker, and the domain event generated by the domain will also be sent to the broker. Then, the consumer will pull messages from the brokerservice when consuming messages. Because the host project does not need to interact with users, I deployed it as a Windows service.
Forum. commandservice:
This project is used to process commands and is also deployed as a Windows service.
Forum. eventservice:
This project is used to process domain events and is also deployed as a Windows service.
Forum. Web:
This is the web site of the Forum. You don't need to talk about it more. The website is used to send commands or call the query service on the query end to query data. The web site only needs to rely on Forum. commands and forum. queryservices, because it only needs to send command and query data.
Forum. commandhandlers:
All command Handler are in this project. The responsibility of command handler is to process command and call the domain method to complete the business logic;
Forum. commands:
All commands are in this project. Each Command is a DTO and will be encapsulated as a message sent to the equeue.
Forum. domain:
It is the domain layer of the Forum. All aggregation, factories, domain services, and domain events are in this project. This project is the most valuable part of the entire forum and the project where the business logic is located.
Forum. domain. dapper:
Because some interfaces may be defined in the domain, the persistence behind these interfaces must be implemented externally. If the persistence of these interfaces is based on the classic DDD architecture, such as the storage interface defined at the domain layer, the implementation is in the base layer (infrastructure. From the layered architecture diagram of classic DDD, the domain layer depends on the infrastructure layer, but some warehousing implementation classes in the infrastructure layer depend on the domain layer; although I can understand this two-way dependency, it is easy to confuse many people who learn DDD, so I prefer to regard domain as the core of the architecture, and everything else is outside the domain. In fact, this idea is similar to the hexagonal architecture. In terms of architecture, the upper layer is not dependent on the lower layer, but the outer layer is dependent on the inner layer. The inner layer defines the interface and the outer layer implements the interface. The inner layer only needs to define the interface for itself. So based on this idea, I will. if dapper is used to implement the interface defined in domain, I will define a forum. domain. A dapper project is called forum. domain. dapper depends on the inner forum. domain. If we have an implementation based on entityframework in the future, we only need to create another project like forum. domain. entityframework. So we can see that forum. domain. dapper is a forum engineer. domain external adapter, Forum. define the adaptation interface in domain, Forum. domain. the Dapper project implements these adaptive interfaces. Based on this idea, our architecture does not have the concept that the upper layer depends on the lower layer, but instead replaces it with the relationship between the internal and external layers. The inner layer does not depend on the outer layer, and the outer layer depends on the inner layer, the inner layer interacts with the outer layer directly through the adapter interface, or through domain event. In this way, we don't have to worry about the seemingly two-way dependency in the classic DDD.
Forum. domain. tests:
This project is a test project for Forum. domain. Each test routine simulates the Controller to initiate the command, and finally checks whether the status in the domain is correctly modified.
Forum. queryservices:
This project defines all query interfaces on the query end. The Forum. Web site depends on the query service interfaces in this project. The implementation of these Query Interfaces is placed in Forum. queryservices. dapper. The relationship between Forum. queryservices and Forum. queryservices. dapper is similar to that between Forum. domain and Forum. domain. dapper.
Forum. denormalizers. dapper:
All denormalizer is involved in this project, and denormalizer is responsible for processing domain events and updating the reader library. Since dapper is currently used for data persistence, the project name ends with dapper.
Forum. Infrastructure:
This is a basic project that stores all basic public things, such as service-independent services, configuration information, and global variables;It should be emphasized that:Here, Forum. Infrastructure is different from the infrastructure in the classic DDD. The infrastructure in DDD is a logical layer, and all the technical support implementations in the domain layer are in the infrastructure. The infrastructure here is only a common basic public thing, infrastructure is not intended for any other layer of service. It can be used by any other project;
Well, the above briefly introduces the functions and design objectives of each project. Let's take a look at the design of the Forum domain model!
Forum Domain Model Design
- Core Function Requirement Analysis:
- Provides three functions: User Registration, logon, and cancellation. When registering a user, you must verify that the user name is unique;
- Provides basic core functions such as posting, replying, modifying posts, modifying replies, and replying;
- The system administrator can maintain Forum sections;
- Aggregation recognition: the identified aggregation methods include Forum account, post, reply, and Forum.
- Next, we will analyze each aggregated information we care about: the minimum account information should be: Account name + password; the Forum should have a name; the post should have the title, content, publisher, posting time, and Forum; the reply should have the reply content, reply time, respondent, and Forum, and the parent reply (which can be blank );
- Scenario lookup: registration is to create an account (the account uniqueness design is analyzed in detail later); the essence of logon is to call the query service on the query end to find whether the account exists, so domain processing is not required, logout is the same; posting is the creation of a post; replying is the creation of a reply; modifying a post is the modification of the post aggregation root; modifying a reply is the modification of the reply aggregation root; adding a forum is to create a forum aggregation root;
- Key business rule Identification: 1) the account name cannot be repeated; 2) the post must have a forum and publisher; 3) the reply must have a corresponding post and responder;
- Implementation of key business rules:
- How can I achieve repeated account names? First, it is a business rule, so it must be implemented in domain instead of in command handler. Then, due to the event sourcing architecture, the inherent defect is that the requirement for uniqueness constraints cannot be realized. Therefore, we need to explicitly design the things that can express the aggregate root index in the domain. I call them indexstore, indicating that they are a storage of the aggregate root index. This idea is very similar to the classic DDD. We have the repository concept, and the warehousing maintains all the aggregated root; here, indexstore maintains the index information of the aggregated root. With this index information, we can design a domain service such as registeraccountservice in the domain when registering a new account. The domain service uses accountindexstore to check whether the account name is repeated, if there are no duplicates, the current account name is added to accountindexstore. If there are duplicates, an exception is reported. Another non-business point needs to be considered, that is, how to implement concurrent user registration processing. We can implement DB-level locks in command handler (instead of locking the entire account table, but locking a record in another table) to ensure that at the same time, no two account names will be added to accountindexstore. We use registeraccountservice to explicitly express the business rule "account names cannot be repeated, thus, this business rule is implemented in the code-level embodiment field. In the past, if event sourcing was not used, we may rely on the unique index of dB to implement this uniqueness, although it can also be implemented in functions, but in fact, this business rule that the account name cannot be repeated is not reflected in the domain. This is also the point I think of when I implement uniqueness Verification Based on Event sourcing.
- The post must have its own forum and publisher. This business rule is easily guaranteed. You only need to judge whether the forum and post are blank on the post aggregation root;
- The reply must have a corresponding post and reply person. Likewise, you only need to judge whether it is empty in the constructor;
Take registering a new user as an example to demonstrate the code to implement client js to submit registration information through angularjs:
$ Scope. Submit = function () {If (isstringempty ($ scope. newaccount. accountname) {$ scope. errormsg = 'enter the account. '; Return false;} If (isstringempty ($ scope. newaccount. Password) {$ scope. errormsg =' enter the password. '; Return false;} If (isstringempty ($ scope. newaccount. confirmpassword) {$ scope. errormsg =' enter the password to confirm. '; Return false;} if ($ scope. newaccount. Password! = $ Scope. newaccount. confirmpassword) {$ scope. errormsg = 'inconsistent password input. '; Return false;} $ HTTP ({method: 'post', URL:'/account/register ', data: $ scope. newaccount }). success (function (result, status, headers, config) {If (result. success) {$ window. location. href = '/home/Index';} else {$ scope. errormsg = result. errormsg ;}}). error (function (result, status, headers, config) {$ scope. errormsg = result. errormsg ;});};
The controller processes the request:
[Httppost] [ajaxvalidateantiforgerytoken] [asynctimeout (5000)] public async task <actionresult> Register (registermodel model, cancellationtoken token) {var command = new registernewaccountcommand (model. accountname, model. password); var result = await _ commandservice. execute (command, commandreturntype. eventhandled); If (result. status = commandstatus. failed) {If (result. predictiontypename = typeof (dupl Icateaccountexception). Name) {return JSON (New {success = false, errormsg = "this account has been registered. Please register with another account. "});} Return JSON (New {success = false, errormsg = result. errormessage});} _ authenticationservice. signin (result. aggregaterootid, model. accountname, false); Return JSON (New {success = true });}
Commandhandler processes command:
[Component(LifeStyle.Singleton)]public class AccountCommandHandler : ICommandHandler<RegisterNewAccountCommand>{ private readonly ILockService _lockService; private readonly RegisterAccountService _registerAccountService; public AccountCommandHandler(ILockService lockService, RegisterAccountService registerAccountService) { _lockService = lockService; _registerAccountService = registerAccountService; } public void Handle(ICommandContext context, RegisterNewAccountCommand command) { _lockService.ExecuteInLock(typeof(Account).Name, () => { context.Add(_registerAccountService.RegisterNewAccount(command.Id, command.Name, command.Password)); }); }}
Registeraccountservice:
/// <Summary> provides domain services for account registration and encapsulates business rules for account registration, such as account uniqueness check /// </Summary> [component (lifestyle. singleton)] public class extends {private readonly iidentitygenerator _ identitygenerator; private readonly iaccountindexstore _ accountindexstore; private readonly temporary _ factory; Public registeraccountservice (iidentitygenerator identitygenerator, registrfactory, iaccountin Dexstore accountindexstore) {_ identitygenerator = identitygenerator; _ factory = factory; _ accountindexstore = accountindexstore ;} /// <summary> register a new account /// </Summary> /// <Param name = "accountindexid"> </param> /// <Param name = "accountname ""> </param> /// <Param name =" accountpassword "> </param> /// <returns> </returns> Public Account registernewaccount (string accountindexid, string accountname, string AC Countpassword) {// create a new account var account = _ factory. createaccount (accountname, accountpassword); // first, judge whether the account has var accountindex = _ accountindexstore. findbyaccountname (account. name); If (accountindex = NULL) {// if not, add it to account index _ accountindexstore. add (New accountindex (accountindexid, account. ID, account. name);} else if (accountindex. indexid! = Accountindexid) {// if it exists but is different from the current index ID, it is assumed that the account has repeated throw new duplicateaccountexception (accountname);} return account ;}}
Eventhandler processes domain events:
[Component(LifeStyle.Singleton)]public class AccountEventHandler : BaseEventHandler, IEventHandler<NewAccountRegisteredEvent>{ public void Handle(IEventContext context, NewAccountRegisteredEvent evnt) { using (var connection = GetConnection()) { connection.Insert( new { Id = evnt.AggregateRootId, Name = evnt.Name, Password = evnt.Password, CreatedOn = evnt.Timestamp, UpdatedOn = evnt.Timestamp, Version = evnt.Version }, Constants.AccountTable); } }}
Conclusion
Okay, that's all. I haven't written an article for a long time, so I don't know how to write it. Next, I am going to share with you some of the technical issues that I have encountered in Enode's constant improvement over the past few months. By the way, you can experience the functions of this forum. Although it is very simple, there are still some basic functions. Http://enode.cloudapp.net/