For a long time, I've been thinking about how to implement a complete set of event-driven architectures in the framework of ASP. The problem looks a bit big, but the main goal is to implement an ASP. NET core-based microservices that can easily subscribe to event messages from a channel and process the received messages, and at the same time, it can send event messages to that channel. So that consumers subscribing to the event message can do further processing of the message data. Let's review several ways of communication between microservices, which are divided into two types: synchronous and asynchronous. Synchronous communication The most common is the restful API, and very simple and lightweight, a request/response loopback is over; asynchronous communication is most commonly transmitted through message channels, which send event messages containing special meaning data to the message channel, while consumers interested in a type of message , you can get the information contained in the message and perform the appropriate action, which is a representation of the event-driven architecture that we are more familiar with. Although the event-driven architecture seems to be very complex, from the implementation of the micro-service seems a bit onerous, but it is a very wide range of applications, but also provides a new way of communication between services. Friends who understand DDD believe they must know the CQRS architecture pattern, which is an event-driven architecture. In fact, the implementation of a complete, secure, stable, and correct event-driven architecture is not simple, due to the consistency of the asynchronous characteristics of the problem can be very difficult, and even need some infrastructure layer tools (such as relational database, good!). Only a relational database) to solve some special problems. This article intends to lead you to explore together, based on the ASP. NET Core Web API to achieve a relatively simple event-driven architecture, and then draw some questions to be considered in depth, stay in future articles continue to discuss. Perhaps the source code introduced in this article cannot be used directly in the production environment, but I hope that this article will give the reader some inspiration and can help solve the actual problems encountered.
Terminology conventions
This article will cover a number of relevant terminology, which is the first agreement:
- An event that occurs at a particular point in time, for example: when I wrote this article, the phone rang.
- Message: The entity that hosts the event data. The serialization/deserialization and transmission of events are in the form of messages
- Message communication channel: a data transmission mechanism with message routing capability to transmit data between the dispatcher and subscribers of a message
Note: In order to cater for the needs of the description, the two concepts of events and messages may be mixed in later.
A simple design
Starting with a simple design, basically event-driven architectures have event messages (events), event Subscriber, event Publisher, event-handler Handler) and the main components such as event bus, the relationship between them is as follows:
First, the IEvent interface defines the basic structure of the event message (more specifically, the data), with almost all events having a unique identifier (ID) and the time (Timestamp) at which an event occurs, usually using UTC time as the standard. Ieventhandler defines the event handler interface, and it is obvious that it contains two methods: The Canhandle method, which determines whether the incoming event object can be handled by the current processor, and the handle method, which defines the process of the event. IEvent and Ieventhandler constitute the basic elements of event handling.
Then there is the Ieventsubscriber and Ieventpublisher interface. The former indicates that the type that implements the interface is an event subscriber, which is responsible for registering the event handler and listening for messages from the event communication channel, and once the obtained message can be processed by a processor, it assigns the processor to the received message for processing. As a result, Ieventsubscriber maintains a reference to the event handler, and for an event dispatcher that implements the Ieventpublisher interface, its primary task is to send the event message to the message communication channel so that the subscriber can get the message and process it.
The Ieventbus interface represents the message communication channel, which is known as the concept of the message bus. It not only has the function of the message subscription, but also has the ability to distribute the message, so it inherits both the Ieventsubscriber and the Ieventpublisher interface. In the above design, it is necessary to separate the role of the subscriber and dispatcher of the message bus through the interface, because the roles have different responsibilities, and the design satisfies the SRP and ISP two guidelines in solid.
Based on the underlying model above, we can quickly convert this object relational model into C # code:
public interface ievent{ Guid Id {get;} DateTime Timestamp {get;}} public interface ieventhandler{ task<bool> handleasync (IEvent @event, CancellationToken cancellationtoken = default); BOOL Canhandle (IEvent @event);} Public interface Ieventhandler<in t>: Ieventhandler where t:ievent{ task<bool> handleasync (T @ event, CancellationToken cancellationtoken = default);} public interface ieventpublisher:idisposable{ Task publishasync<tevent> (tevent @event, CancellationToken CancellationToken = default) where tevent:ievent;} public interface ieventsubscriber:idisposable{ void Subscribe ();} Public interface Ieventbus:ieventpublisher, Ieventsubscriber {}
With just 30 lines of code, we have a clear description of our basic object relationship. For the above code, we need to pay attention to the following points:
- This code uses the new features of C # 7.1 (Default keyword)
- The publish and handle methods are replaced with Publishasync and Handleasync methods that support asynchronous calls, and they return task objects, which makes it easy to use the Async/await programming model in C #
- Since our model can be used as a generic model for implementing messaging systems and will require the use of ASP. NET core projects, it is recommended that the definitions of these interfaces be placed in a separate Netstandard class library for future reuse and expansion
OK, the interface is well defined. Achieve it? Below, we implement a very simple message bus: Passthrougheventbus. In future articles, I'll also show you how to implement a different message bus based on RABBITMQ and Azure Service bus.
Passthrougheventbus
As the name implies, Passthrougheventbus indicates that when a message is distributed to the message bus, the message bus does not do any processing and routing, but instead pushes the message directly to the Subscriber. In the subscriber's event listener function, the received message is processed through the registered event handler. The entire process does not depend on any external components, it does not need to reference an additional development library, but only leverages the existing ones. NET data structures to simulate the dispatch and subscription process of a message. Therefore, Passthrougheventbus does not have fault tolerance and message re-send function, does not have the message store and the routing function, we first implement such a simple message bus, to experience the event-driven architecture design process.
We can use it. NET, the basic data structures such as queue or Concurrentqueue are implemented as Message Queuing, and the message queue itself has its own responsibility when the message is pushed into the queue, and it needs to be notified to the caller. Of course, Passthrougheventbus does not need to rely on queue or concurrentqueue, it is to do is to simulate a message queue, when the message is pushed in, immediately notify subscribers to process. Similarly, in order to separate responsibilities, we can introduce a EventQueue implementation (see below), separating the message push and routing responsibilities (the responsibilities of the infrastructure layer) from the message bus.
Internal sealed class Eventqueue{public event system.eventhandler<eventprocessedeventargs> eventpushed; Public EventQueue () {} public void Push (IEvent @event) { onmessagepushed (new Eventprocessedeventargs (@ event)); } private void onmessagepushed (Eventprocessedeventargs e) = this. Eventpushed?. Invoke (this, e);}
The most important method in EventQueue is the push method, which can be seen from the code above, when EventQueue's push method is called, it immediately triggers the eventpushed event, which is one. NET event to notify subscribers of the EventQueue object that the message has been distributed. The implementation of the entire eventqueue is very simple, we focus only on the routing of events, without considering any additional things at all.
The next step is to use EventQueue to achieve passthrougheventbus. No suspense, Passthrougheventbus need to implement the Ieventbus interface, its two basic operations are publish and subscribe respectively. In the Publish method, the incoming event message is forwarded to the EventQueue, The Subscribe method subscribes to the Eventqueue.eventpushed event (. Net event), and during the Eventpushed event processing, from all registered event handlers Handlers) is found to be able to handle the received event and process it. The whole process is still very clear. Here is the implementation code for Passthrougheventbus:
public sealed class passthrougheventbus:ieventbus{private readonly eventqueue eventqueue = new EventQueue (); Private ReadOnly ienumerable<ieventhandler> eventhandlers; Public Passthrougheventbus (ienumerable<ieventhandler> eventhandlers) {this.eventhandlers = eventHandlers; private void Eventqueue_eventpushed (object sender, Eventprocessedeventargs e) = = (from eh in this.event Handlers where eh. Canhandle (e.event) Select eh). ToList (). ForEach (Async eh = await eh. Handleasync (e.event)); Public Task publishasync<tevent> (tevent @event, CancellationToken cancellationtoken = default) where Tevent: IEvent = Task.Factory.StartNew (() = Eventqueue.push (@event)); public void Subscribe () = eventqueue.eventpushed + = eventqueue_eventpushed; #region IDisposable Support private bool Disposedvalue = false; To detect redundant calls void Dispose (bool disposing) { if (!disposedvalue) {if (disposing) {this.eventQueue.EventPushed-= even tqueue_eventpushed; } Disposedvalue = true; }} public void Dispose () = Dispose (true); #endregion}
The implementation process is very simple, and of course, it is clear from the code that Passthrougheventbus does not do any route processing, and does not rely on an infrastructure facility (such as a message queue that implements AMQP), so do not expect to be able to use it in a production environment. But for now, it will be helpful for what we're going to talk about next, at least until we introduce a message bus based on RABBITMQ and other implementations.
Similarly, implement Passthrougheventbus in another Netstandard class library, although it does not require additional dependencies, but it is, after all, one of many message buses, stripping it from an interface-defined assembly, with the benefit of two points: first , which guarantees the purity of the assembly that defines the interface, so that the Assembly does not need to rely on any external components, and ensures that the Assembly has a single function, which is to provide the base Class library for the implementation of the message system, and secondly, to place the Passthrougheventbus in a separate assembly. Facilitates the invocation of the Ieventbus technology choice, for example, if the developer chooses to use an RABBITMQ-based implementation, then it is only necessary to refer to the assembly that implements the Ieventbus interface based on RABBITMQ. Without referencing the assembly that contains the Passthrougheventbus. This, I think, can be summed up as a guideline for "isolating dependencies (Dependency segregation)" in framework design.
Well, the basic components are all defined, and next, let's make a restful service based on the ASP. NET Core Web API, and access the above message bus mechanism for message dispatch and subscription.
Customer RESTful API
We still take the customer-managed RESTful API as an example, but we don't talk too much about how to implement restful services that manage customer information, which is not the focus of this article. As a case, I built this service using the ASP. NET Core 2.0 Web API, developed using visual Studio 2017 15.5, and used dapper in Customerscontroller to crud the customer information. Background based on SQL Server Express Edition, using SQL Server Management Studio allows me to easily see the results of database operations.
Implementation of RESTful APIs
Assuming our customer information contains only the customer ID and name, the following Customerscontroller code shows how our restful services store and read customer information. Of course, I have the code of this article through GitHub Open source, open source protocol for MIT, although business-friendly, but after all, the case code has not been tested, so please use with caution. The use of the source code in this article will be introduced at the end of the text.
[Route ("Api/[controller]")]public class customerscontroller:controller{private readonly iconfiguration Configuratio N Private readonly string connectionString; Public Customerscontroller (IConfiguration configuration) {this.configuration = Configuration; this.connectionstring = configuration["mssql:connectionstring"]; }//Get customer information for the specified ID [HttpGet ("{ID}")] public async task<iactionresult> get (Guid ID) {Const string s QL = "SELECT [CustomerId] as Id, [CustomerName] as Name from [dbo]. [Customers] WHERE [customerid][email protected] "; using (var connection = new SqlConnection (connectionString)) {var customer = await connection. queryfirstordefaultasync<model.customer> (SQL, new {ID}); if (customer = = null) {return NotFound (); } return Ok (customer); }}//Create new customer information [HttpPost] public async task<iactionresult> Create ([FRombody] Dynamic model) {var name = (string) model. Name; if (string. IsNullOrEmpty (name)) {return badrequest (); } const String sql = "INSERT into [dbo]. [Customers] ([CustomerId], [CustomerName]) VALUES (@Id, @Name) "; using (var connection = new SqlConnection (connectionString)) {var customer = new Model.customer (name); await connection. Executeasync (SQL, customer); Return Created (Url.action ("Get", new {id = customer. ID}), customer. ID); } }}
As always, the code is simple, and the WEB API controller simply implements the creation and return of customer information through dapper. We may as well test, use the following Invoke-restmethod PowerShell instructions, send a POST request, create a user by using the Create method above:
As you can see, the ID number of the new customer has been returned in response. Next, continue to use Invoke-restmethod to get more information about the new customer:
OK,API debugging is no problem at all. Below, we will expand this case, we hope that this API in the completion of customer information creation, while the external send a "customer information has been created" event, and set up an event handler, responsible for the event to save the details of the database.
Join the event bus and message processing mechanism
First, we add a reference to the above two assemblies on the ASP. NET Core Web API project, and then, as a general practice, in the Configureservices method, add Passthrougheventbus to the IOC container:
public void Configureservices (iservicecollection services) { services. Addmvc (); Services. Addsingleton<ieventbus, passthrougheventbus> ();}
In this case, the event bus is registered as a singleton (Singleton) service because it does not save the state. In theory, when using a singleton service, you need to pay particular attention to the life cycle management of the service instance object, because its lifecycle is the entire application level, and the object resources referenced by it will not be freed during the program's run, so when the program finishes running, you need to dispose of the resources reasonably. Fortunately, the dependency Injection framework for ASP. NET core has already handled it for us, so we don't need to worry too much about the above Passthrougheventbus single-instance registration, and when the program executes and exits normally, The dependency injection framework automatically helps us dispose of a singleton instance of Passthrougheventbus. So, for a singleton instance, do we just need to register with the Addsingleton method, without having to pay attention to whether it is actually being dispose of? The answer is no, interested readers can refer to Microsoft's official documents, in the next article I will do some introduction to this part of the content.
Next, we need to define a Customercreatedevent object that says "customer information has been created" and, at the same time, define a Customercreatedeventhandler event handler, Used to process event messages received from Passthrougheventbus. The code below, of course, is simple:
public class Customercreatedevent:ievent{public customercreatedevent (string CustomerName) {this. Id = Guid.NewGuid (); This. Timestamp = Datetime.utcnow; This. CustomerName = CustomerName; } public Guid Id {get;} Public DateTime Timestamp {get;} public string CustomerName {get;}} public class customercreatedeventhandler:ieventhandler<customercreatedevent>{public bool Canhandle (IEvent @ev ENT) = @event. GetType (). Equals (typeof (Customercreatedevent)); Public task<bool> Handleasync (customercreatedevent @event, CancellationToken cancellationtoken = default) { Return Task.fromresult (TRUE); Public task<bool> Handleasync (IEvent @event, CancellationToken cancellationtoken = default) = Canhandle (@ Event)? Handleasync ((customercreatedevent) @event, CancellationToken): Task.fromresult (false);}
The two have implemented the IEvent and Ieventhandler interfaces that we have defined at the very beginning. In the first Handleasync overloaded method of the Customercreatedeventhandler class, let's leave it simply to return a true value, indicating that the event processing succeeded. The next thing to do is to send the Customercreatedevent event to the event bus after the customer information is created, and to register the Customercreatedeventhandler instance when the ASP. NET Core Web API program starts and invokes the Subscribe method of the event bus so that it begins to listen for the event's dispatch behavior.
Therefore, Customercontroller needs to rely on Ieventbus, and in the Customercontroller.create method, it is necessary to send the event by invoking the Publish method of Ieventbus. Now the implementation of the Customercontroller to make some adjustments, adjusted after the code as follows:
[Route ("Api/[controller]")]public class customerscontroller:controller{private readonly iconfiguration Configuratio N Private readonly string connectionString; Private ReadOnly Ieventbus Eventbus; Public Customerscontroller (iconfiguration configuration, Ieventbus eventbus) {this.configuration = Confi guration; this.connectionstring = configuration["mssql:connectionstring"]; This.eventbus = Eventbus; }//Create new customer information [HttpPost] public async task<iactionresult> Create ([frombody] dynamic model) {var Name = (string) model. Name; if (string. IsNullOrEmpty (name)) {return badrequest (); } const String sql = "INSERT into [dbo]. [Customers] ([CustomerId], [CustomerName]) VALUES (@Id, @Name) "; using (var connection = new SqlConnection (connectionString)) {var customer = new Model.customer (name); await connection. Executeasync (SQL, customer); AwaIt This.eventBus.PublishAsync (new Customercreatedevent (name)); Return Created (Url.action ("Get", new {id = customer. ID}), customer. ID); }}//Get method omitted temporarily}
Then, modify the Configureservices method in Startup.cs to register the Customercreatedeventhandler in:
public void Configureservices (iservicecollection services) { services. Addmvc (); Services. Addtransient<ieventhandler, customercreatedeventhandler> (); Services. Addsingleton<ieventbus, passthrougheventbus> ();}
and call the Subscribe method to start listening on the message bus:
public void Configure (Iapplicationbuilder app, Ihostingenvironment env) { var eventbus = app. Applicationservices.getrequiredservice<ieventbus> (); Eventbus.subscribe (); if (env. Isdevelopment ()) { app. Usedeveloperexceptionpage (); } App. Usemvc ();}
OK, now let's set a breakpoint on Customercreatedeventhandler's Handleasync method, press F5 to enable visual Studio 2017 debugging, Then re-use the Invoke-restmethod command to send a POST request, you can see that the breakpoint on the Handleasync method is hit, and the event has been distributed correctly:
The data in the database is also updated correctly:
The last small step, in Handleasync, is to serialize and save the data from the Customercreatedevent object to the database. Of course this is not difficult, you can also consider the use of dapper, or direct use of ADO, even using the more heavyweight entity Framework Core, can be achieved. Then leave this question to the interested readers and friends.
Summary
Here basically the content of this article will be over, review, this article at the beginning of this paper proposed a relatively simple message system and event-driven architecture design model, and implemented a simplest event bus: Passthrougheventbus. Then, in conjunction with an actual ASP. NET Core Web API case, we learned about the process of implementing event message dispatch and subscription in the RESTful API, and implemented the processing of the resulting event messages in the event handler.
However, we still have a lot of questions to think about in more depth, such as:
- How are dependencies managed if the event handler needs to rely on infrastructure layer components? How is the component life cycle managed?
- How do I implement an event bus based on RABBITMQ or azure Service bus?
- What if the event fails to send after the database update is successful?
- How do I guarantee the order of event handling?
Wait a minute... In the following article, I will try to do a more detailed introduction.
Use of source code
The source code for this series of articles is in Https://github.com/daxnet/edasample, the GitHub repo, which distinguishes between different chapters by different release tags. The source code of this article please refer to chapter_1 this tag, as follows:
Next there will be chapter_2, chapter_3 and other such tags, corresponding to the second part of this series of articles, the third part and so on. Please look forward to it.
Implementation of the event-driven architecture under the
ASP. NET Core Web API (i): a simple implementation