DDD combat (3)

Table of contents

1. Realize domain events with MediatR

Second, the basic use of RabbitMQ


1. Realize domain events with MediatR

        Domain events can cut off the strong dependencies between domain models. After the event release is completed, the handler of the event decides how to respond to the event, so that we can realize the decoupling between event release and event processing. When implementing domain events in .NET, we can use C# event syntax, but the event syntax requires the event handler to be explicitly registered in the event publisher object, which is highly coupled. The author recommends using MediatR to implement domain events .

        MediatR is an open source library that implements in-process event delivery in .NET, and it can realize the decoupling between event publishing and event processing. MediatR supports two modes: "one publisher corresponds to one processor" and "one publisher corresponds to multiple processors". The latter is more widely used, so this section will explain this usage.

        Step 1, create an ASP.NET Core project, and then install MediatR.Extensions.Microsoft DependencyInjection via NuGet.

        Step 2: Call the AddMediatR method in the project's Programcs to register the services related to MediatR into the dependency injection container. The parameters of the AddMediatR method generally specify several assemblies where the event handler is located. Register MediatR as shown in the code below.

builder.Services.AddMediatR(Assembly.Load("领域事件 1"));

        Step 3: Define a class TestEvent that transmits data between the event publisher and handler. This class needs to implement the Iotification interface, as shown in the following code.

public record TestEvent(string UserName) : INotification;

        The UserName property in TestEvent represents the username of the logged-in user. Events are generally passed from the publisher to the handler, and there is rarely a need to directly notify the publisher of the event at the handler of the event. Therefore, the properties of the TestEvent class that implements INotification are generally immutable. We use record syntax to declare this class.

        Step 4: The event handler must implement the NotificationHandler<TNotification> interface, where the generic parameter TNotification represents the type of message that the event handler will handle. All events of type TNotification are handled by event handlers. We write two event handlers, as shown in Code 9-24, which respectively output the received events to the console and write them to files.

public class TestEventHandler1 : INotificationHandler<TestEvent>
{
public Task Handle(TestEvent notification, CancellationToken cancellationToken)
{
Console.writeLine($"我收到了(notification.UserName)");
return Task.CompletedTask;
}
}
public class TestEventHandler2 : INotificationHandler<TestEvent>
{
public async Task Handle(TestEvent notification, CancellationToken cancellationToken)
{
await File.WriteAllTextAsync("d:/1.txt",$"来了{notification.UserName}");
}}

        Step 5: Inject the service of IMediator type into the class that needs to publish the event, and then we use the Publish method to publish. Be careful not to call the Send method to publish events by mistake, because the Send method is used to publish one-to-one events, and the Publish method is used to publish one-to-many events. We need to publish the event in the login method of the controller, here we omit the actual login code, as shown in the 9th line of code in the following code.

public class TestController : ControllerBase
{
private readonly IMediator mediator;
public TestController(IMediator mediator)
{
this.mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> Login(LoginRequest req)
{
await mediator.Publish (new TestEvent(req.UserName));
return Ok("ok");
}}

        Run the above program, and then call the login interface, we can see that there are code output messages in the console and in the file, which means that both event handlers have been executed.

        If we use await to call the Publish method, then the program will wait for all event handlers' Handle methods to complete before continuing to execute backwards, so the code of the event publisher and event handler runs in the same call stack , so that we can easily implement strongly consistent transactions. If the event publisher does not need to wait for the execution of the event handler, then we can call the Publish method without the await method; even if we need to use the await method to call the Publish method to publish the event, if the code execution of an event handler is too time-consuming, In order to avoid affecting the user experience, we can also execute the event processing logic asynchronously in the Handle method of the event handler. If we choose not to wait for event handlers, we have to deal with eventual consistency of transactions.

Second, the basic use of RabbitMQ

        Different from domain events, integration events are used to transfer events between microservices. Because this is communication between servers, a third-party server must be used as an event bus. We generally use message middleware as the event bus. Currently, commonly used message middleware include Redis, RabbitMQ, Kafka, ActiveMO, etc. This project uses RabbitMQ.

        Let's first understand a few basic concepts in RabbitMQ.

        (1) Channel: A channel is a virtual connection between message producers, consumers and servers. Why is it called a "virtual connection"? Because the establishment of a TCP connection is very resource-intensive, so RabbitMo uses the TCP connection A virtual channel is constructed on the basis of We try to reuse TCP connections as much as possible, and channels are closed when they are exhausted.

        (2) Queue (queue): The queue is used to send and receive messages. The producer puts the message in the queue, and the consumer gets the message from the queue.

        (3) Exchange (exchange): The exchange is used to route messages to one or more queues.

        RabbitMO has a lot of usage modes, readers can refer to related documents. Here we introduce the mode used in the integration event
, that is, the routing mode.

        In this mode, the producer publishes the message to the switch, the message will carry the routingKey attribute, and the switch will send the message to one or more queues according to the value of routingKey; the consumer will get the message from the queue; both the switch and the queue Located inside the RabbitMO server. The advantage of this mode is that even if the consumer is not online, the consumer-related messages will be saved in the queue. When the consumer is online, the missed messages during the offline period can be obtained. We know that in a software system, neither the producer nor the consumer of the message can be online 24 hours a day. This mode can ensure that the consumer receives the missed message due to server restart and other reasons.

        The installation of RabbitMO service is relatively simple. Here we mainly explain how to connect RabbitMQ in NET to send and receive messages.

        First, we create a project for sending messages and a console project for receiving messages, both of which need to install the NuGet package RabbitMO.Client.

        Next, we send the message as shown in the code below.

var factory = new ConnectionFactory();
factory.HostName = "127.0.0.1";        //RabbitMQ 服务器地址
factory.DispatchConsumersAsync = true;
string exchangeName = "exchangel";     //交换机的名字
string eventName = "myEvent";          //routingKey 的值
using var conn = factory.CreateConnection();
while(true)
{
string msg = DateTime.Now.TimeOfDay.ToString(); //待发送消息
using (var channel = conn.CreateModel())        //创建虚似信道
{
var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2;
channel.ExchangeDeclare(exchange: exchangeName, type:"direct");//声明交换
byte[] body = Encoding.UTE8.GetBytes (msg);
channel.BasicPublish(exchange: exchangeName,routingKey: eventName,mandatory: true,basicProperties: properties,body: body); //发布消息
}
Console.WriteLine("发布了消息:” + msg);
Thread.Sleep(1000);
}

        The sixth line of code creates a TCP connection from the client to RabbitMQ. We try to reuse this TCP connection; the ninth line of code uses the current time as the message to be sent; the tenth line of code creates a virtual channel, which can be It is closed when it is used up. It should be noted that the message will not be sent until the channel is closed; the code on line 14 uses the ExchangeDeclare method to declare an exchange with the specified name. If the exchange with the specified name already exists, create it again. The type parameter If it is set to direct, it means that the switch will perform equality matching according to the value of the routingKey of the message, and the message will be published to the queue bound to its routingKey; because the messages in RabbitMo are all transmitted according to the byte type, so we In line 15, the message of string type is converted to byte] type; for the convenience of demonstration, an infinite loop is used here to send a message every 1s.

        Finally, let's write the code of the message consumer project, as shown in the following code.

var factory = new ConnectionFactory();
factory.HostName = "127.0.0.1";
factory.DispatchConsumersAsync = true;
string exchangeName = "exchangel";
string eventName = "myEvent";
using var conn = factory.CreateConnection();
using var channel = conn.CreateModel();
string queueName = "queuel";
channel.ExchangeDeclare (exchange: exchangeName,type: "direct");
channel.QueueDeclare(queue: queueName,durable: true,
exclusive: false,autoDelete: false,arguments: nul1);
channel.QueueBind(queue: queueName,
exchange: exchangeName,routingKey: eventName);
var consumer = new AsyncEventingBasicConsumer(channel);
consumer.Received += Consumer Received;
channel.BasicConsume (queue: queueName, autoAck: false,consumer: consumer);
Console.ReadLine();
async Task Consumer_Received(object sender, BasicDeliverEventArgs args)
{
try
{
var bytes = args.Body.ToArray();
string msg = Encoding.UTF8.GetString(bytes);
Console.WriteLine(DateTime.Now “收到了消息”+msg);
channel.BasicAck(args.DeliveryTag, multiple: false);
await Task.Delay(800);
}
catch (Exception ex)
{
channel.BasicReject(args.DeliveryTag, true);
Console.writeLine("处理收到的消息出错"+ex);
}
}

        In line 9, we declare an exchange with the same name as the message sender. If the message sender has already declared an exchange with the same name, the call here will be ignored. But the ninth line of code is still indispensable, because it is possible that the consumer of the message starts before the sender of the message.

        In the 10th line of code, we declare a queue to receive messages forwarded by the switch. If a queue with the specified name already exists, the call to the QueueDeclare method is ignored. Because after the message is fetched, the message is removed from the queue, so the naming of the queue is very important. If we have two programs A and B that both want to read the same message, then the queues declared by them cannot have the same name, that is, we need to create two queues; if the queues declared by the two programs have the same name, that is, they share the same queue , then a message can only be received by one of the programs on a "first come, first served" basis.

        In lines 12 and 13, we bind the queue to the exchange and set the routingKey parameter. In this way, when the switch receives a message that the value of routingKey is the same as the set value, it will forward the message to the column we specified. Two switches can bind multiple queues. If the routingKey values ​​of these queues are the same, when the switch receives a message with the same routingKey value, it will forward the message to these queues at the same time, that is, the same message Can be received by multiple consumers. This is very important in a microservice environment, because an integration event sent by a microservice may be received by multiple microservices. For example, the "order creation complete" event issued by the order system may be received by the logistics system, payment system, log Received by multiple microservices such as the system.

        In the 14th to 17th line of code, we create a received message. When a message is received, the Received event will be triggered, and we process the received message in the Consumer_Received method.

        The BasicConsume call is not used to block execution. When we test, we create a console program. In order to avoid the program exiting after executing the BasicConsume method, we wait for the user to press the Enter key in the 17th line of code before exiting the program.

        In lines 22~24, we convert the received message into a string type, and then output it to the console. In order to be able to display the received time of the message, we also output the time when the message was received.

        Messages in RabbitMo support "failure resend", that is, during the process of receiving and processing a message by a consumer, if an error occurs during the processing of the message and the message is not completely processed, the queue will try to send the message to the consumer again . The consumer needs to call BasicAck to notify the queue "message processing complete" after the message processing is successful, and call BasicReject to notify the queue "message processing error" if the message processing error occurs. Therefore, we call BasicAck in the 25th line of code to confirm the message, and wrap the message processing process with a ty catch code block. When an exception occurs in the code block, BasicReject will be called to arrange for the message to be resent . Since the same message may be delivered repeatedly, we must ensure that the message processing code is idempotent.

        The Received of AsyncEventingBasicConsumer is used to block execution, that is, the Received event of the next message will be triggered only after the execution of the Received callback method triggered by a message is completed. In order to demonstrate the effect of the Received event blocking execution, we added a delay operation to the 26th line of code.

        After completing the above code, we run the programs on the message sending end and the consumer end respectively. From the running results, we can see that the two programs can get the expected running effect. Even if the consumer end is closed, after the consumer end restarts, it can still receive the missed messages during the offline period.

Guess you like

Origin blog.csdn.net/xxxcAxx/article/details/128486391
ddd