Design and Implementation of domain events targeting the classic layered architecture in the field-driven design

Source: Internet
Author: User
Tags msmq

In the byteart retail case I developed, the implementation of domain events has been introduced. For more information, see my previous article: in-depth analysis of byteart retail case: domain events. After a period of study and thinking, we have a new understanding of the design and implementation of domain events. In this article, let's first take a look at the drawbacks of the implementation of domain events in byteart retail cases, and then discuss the design and implementation of domain events in the field-driven design. Because many of the articles are from byteart retail cases, this article can still be seen as an "out-of-box" in "in-depth analysis of byteart retail cases ". At the time of writing this article, I have reconstructed the implementation of domain events in byteart retail. Therefore, readers can still read the latest code of the case from GitHub.

Review the implementation of domain events in byteart retail Cases

In the article "in-depth analysis of byteart retail case: domain events", I have introduced in detail how domain events are implemented. Therefore, it is not intended to be described in more detail here, especially the definition of domain events. The first thing to note is that the content described in this article may be different from the content in this article, but it doesn't matter. If you have not read this article, you can also take a retrospective look, it is helpful to have a deeper understanding of the main ideas in this article.

The question comes from "What's the problem?" in this article ?" Description of this section. This section introduces an Application Scenario for sending emails. We recommend that you use another event type different from the domain event type for email sending: application events, which distributes an application event to the event bus while performing persistence operations on domain objects, this completes the email sending task in the Application Event processor. Of course, the problems we encounter are the same: we want Object Persistence and email sending to be completed in the same transaction to ensure that object persistence and email sending can succeed or fail at the same time.

However, in fact, the introduction and implementation of this application event is not very reasonable. It needs to aggregate the root event to record the events that have occurred when a domain event occurs, then, when the warehousing completes Object Persistence transaction commit, these domain events are converted into application events and then sent to the event bus. There are two problems: first, it violates the "single responsibility Principal" principle of object-oriented design. warehousing is responsible for object lifecycle management, however, submitting an event to the event bus is not within the scope of its responsibility. This will also have a certain impact on the warehouse design: the warehouse has to rely on the event bus, even if dependency injection is adopted, the storage has to perceive the existence of the event bus. Here, we need to differentiate the design of domain warehousing in the cqrs architecture, in the cqrs architecture, domain warehousing is responsible for the persistence of domain events to event store, and is also responsible for distributing events to the event bus, however, event storage and distribution are the responsibilities of domain warehousing. Because domain warehousing has degraded to no longer directly responsible for the persistence tasks of domain objects, there is no such problem in cqrs; the second problem is to let the aggregation root be responsible for saving domain objects. This is not only an extra step for application oriented to the domain-driven layered architecture (the object state has been saved in the private field), Cross-thread operations also bring about data inconsistency. Even if the locking mechanism is adopted, it will also have a certain impact on the performance. Similarly, the cqrs architecture, in cqrs, the state of the object is described by event tracing. Therefore, the aggregation root must maintain existing events and event snapshots.

It seems that we really need to reconstruct the byteart retail case. The purpose of reconstruction is: without introducing "application events, implement the required functions directly in the domain event processor. This is more in line with the original concepts and definitions of "Domain events.

Redesign domain events

For the interface definition and abstract type implementation of domain events, I have already introduced the byteart retail case: domain events. Here we will focus on the distribution and processing logic of domain events.

First, domain events are handled by domain Event Handlers. Domain event handlers are one type of event handlers (events in applications are not just domain events ), the difference is that it is only responsible for processing domain events. Therefore, its interface is defined as follows:

public interface IDomainEventHandler<TDomainEvent> : IEventHandler<TDomainEvent>    where TDomainEvent : class, IDomainEvent { }

Second, implement this interface to complete event processing in the implementation class:

Public class orderdispatchedeventhandler: idomaineventhandler <orderdispatchedevent> {public void handle (orderdispatchedevent evnt) {// process event }}

Then, modify the domainevent class and add the publish static method in the class to distribute domain events:

public static void Publish<TDomainEvent>(TDomainEvent domainEvent)    where TDomainEvent : class, IDomainEvent{    IEnumerable<IDomainEventHandler<TDomainEvent>> handlers = ServiceLocator        .Instance        .ResolveAll<IDomainEventHandler<TDomainEvent>>();    foreach (var handler in handlers)    {        if (handler.GetType().IsDefined(typeof(HandlesAsynchronouslyAttribute), false))            Task.Factory.StartNew(() => handler.Handle(domainEvent));        else            handler.Handle(domainEvent);    }}

OK. The distribution and handling of domain events have been completed. That's easy! Note the following points: 1. The handlesasynchronously feature is introduced. Applying this feature on the domain event processor enables the processor to process events asynchronously Based on TPL, the processor can select its own processing mode based on the actual situation: Some processing processes may take a long time, and the execution of the business logic does not need to know the processing result, this feature can be applied to the event processor, for example, sending an email when an event occurs; 2. Using the service locator (service locator) in the publish Method) to resolve all registered event processors for a certain domain, and then call them one by one to complete event processing. Readers who have in-depth research on "control reversal/dependency injection" will certainly not agree with this practice. It is not advisable to directly use service locator in applications, service locator can only be used for type registration to the IOC container. service locator cannot be used directly to parse a type of object. This objection makes sense, because if the resolve method of service locator is used everywhere in the program, the dependency between the program on external components is not obvious, and worse: if I have not registered a type in the IOC container, this program cannot be run at all. Therefore, directly using the servicelocator. Resolve method not only increases the program's dependency on external components, but also makes the dependency less apparent. This is a very bad design.

Here, I want to share my views on this 2nd point. Although theoretically speaking, this design is very poor, but for our current application scenarios, this design is the most concise, because Note: first of all: publish is a static method, in a program, you cannot apply dependency injection based on static types or static methods. Even if you do not directly use service locator, you use a design similar to event aggregator, you also have to "inject" the event aggregator instance to the publish method, but this is powerless for static methods. Maybe you still think, can we inject this dependency through the domainevent constructor? The answer is no: domainevent is a message. It is just a carrier of data and should not be concerned about how it is distributed, in this way, only the coupling between domainevent and event dispatching mechanism can be strengthened, and this coupling will even be brought to the domain model! Weigh the drawbacks of servicelocator. Resolve. This design is even worse, it can be said to be terrible! Second, we define publish as a static method because domain events come from the domain model and we cannot or inject instances of the event dispatch mechanism into the domain model. Therefore, A domain object cannot receive any distribution mechanism instance, and then initiates a domain event. It can only distribute domain events in a similar way as follows:

public void Confirm(){    DomainEvent.Publish<OrderConfirmedEvent>(new OrderConfirmedEvent(this)    {        ConfirmedDate = DateTime.Now,        OrderID = this.ID,        UserEmailAddress = this.User.Email    });}

In addition, servicelocator is directly used here. another benefit of resolveall is that all event processors can be directly registered as perresolve lifecycles as long as they use external components (such as warehousing and event Bus) with a reasonable life cycle manager, instances of these components can be directly used in the event processor, and these instances can be consistent in a certain execution context. This is very important and will be discussed later in this article.

To sum up, it is reasonable to directly use service locator to obtain all event processor instances. This also provides us with some architectural design inspiration: whether everything is correct or not, and whether it is reasonable or not. The architecture process is a trade-off process, the purpose of the architecture is to find a solution that best fits the current application scenario.

Finally, register the domain event processor in the IOC container. byteart retail uses the unity IOC container. Therefore, I wrote the relevant registration information in the web. config OF THE byteartretail. Services Project:

<unity xmlns="http://schemas.microsoft.com/practices/2010/unity"><container>  <!--Domain Event Handlers-->  <register     type="ByteartRetail.Domain.Events.IDomainEventHandler`1          [[ByteartRetail.Domain.Events.OrderDispatchedEvent, ByteartRetail.Domain]],           ByteartRetail.Domain"     mapTo="ByteartRetail.Domain.Events.Handlers.OrderDispatchedEventHandler, ByteartRetail.Domain"     name="OrderDispatchedEventHandler" /></container></unity>

So far, the generation, distribution, and processing logic of events in the entire domain have been clear. Next, let's dive into the domain event processor to learn how to perform operations related to warehousing or other third-party infrastructure components in the processor, and ensure that these operations are transactional.

Domain event handlers)

Because domain models use service locator to obtain all event processors when distributing domain events, we can directly declare the infrastructure component interfaces we need in the event processor constructor, and then call these components in the handle method. For example, if we need to use the sales order repository in the event processor that the order has been delivered, we can do this:

public class OrderDispatchedEventHandler : IDomainEventHandler<OrderDispatchedEvent>{    private readonly ISalesOrderRepository salesOrderRepository;    public OrderDispatchedEventHandler(ISalesOrderRepository salesOrderRepository)    {        this.salesOrderRepository = salesOrderRepository;    }    public void Handle(OrderDispatchedEvent evnt)    {        // this.salesOrderRepository.Find(xxxx);    }}

Furthermore, if we still need to use the event bus in the handle method to distribute the event to the event bus after processing orderdispatchedevent, the same applies, add ieventbus to the constructor of the orderdispatchedeventhandler interface.

In the Layered Architecture Application of DDD, the application layer is responsible for coordinating various tasks. Therefore, from the byteart retail case, we can also see that in the WCF Service implementation code at the application layer, it obtains the warehousing and event bus instances, obtains the domain model objects through warehousing, and then performs business operations through these objects. The entire task is completed in a WCF operation. Based on the design experience, we should minimize the object lifecycle to reduce the probability of errors. Therefore, in the byteart retail case, the storage context (repository context) both event bus and event bus are registered to the unity IOC container with the lifecycle of WCF per operation, that is, as long as they are in the same WCF operation context, these objects are unique. Because domain objects are calling domainevent. the publish method also exists in the WCF operation context when sending messages. Therefore, the repository context used by the warehouse in the domain event processor) it will be consistent with the context used in the application-layer WCF method. This is very important: this ensures that the changes and storage of domain objects in the domain event processor can be submitted in the WCF method at the application layer, because the two use the same repository context. This process can be roughly described as follows:

Similarly, on the premise that ieventbus registers as the per WCF operation context lifecycle in the Unity IOC container, we can also reference the ieventbus instance in the event processor, then use ieventbus in the application layer code. the COMMIT () method submits a dispatch event once. The complete code of orderdispatchedeventhandler is as follows:

Public class metadata: idomaineventhandler <orderdispatchedevent> {private readonly isalesorderrepository salesorderrepository; private readonly ieventbus bus; Public metadata (inclusalesorderrepository, ieventbus) {This. salesorderrepository = salesorderrepository; this. bus = bus;} public void handle (orderdispatchedevent evnt) {salesorder = evnt. source as salesorder; salesorder. datedispatched = evnt. dispatcheddate; salesorder. status = salesorderstatus. dispatched; bus. publish <orderdispatchedevent> (evnt) ;}// Application Layer: Public void dispatch (guid orderid) {var salesorder = salesorderrepository. getbykey (orderid); salesorder. dispatch (); salesorderrepository. update (salesorder); context. commit (); bus. commit ();}

In the dispatch method at the application layer above, we use the context. Commit () method and the bus. Commit () method to commit transactions for warehouse operations and event bus operations respectively. In the current two-phase commit (two phase commit, 2 PC), there is no transaction between the two. Of course, not all application scenarios must ensure the transaction between the two. For example, our email sending function, after the data has been submitted to the database, whether the email is successfully sent does not affect the data consistency of the system. At best, the customer cannot receive the email. At this time, the cause of the failed email sending can be recorded in the log, then, the system maintenance personnel manually solve the problem. There are also some application scenarios where the data consistency requirement is far greater than the performance or other requirements. At this time, we must ensure the transaction between the two. In the byteart retail case, I introduced the concept of Transaction Coordinator.

Transaction Coordinator)

The transaction coordinator is used to coordinate the transaction operations of multiple components. It is an implementation of the Distributed Transaction architecture. In byteart retail, two Transaction Coordinator implementations are provided: one is implemented based on Microsoft MSDTC (Microsoft Distributed Transaction Coordinator), and the other is to ignore the implementation of any distributed transaction processing. The following are the interfaces and classes related to the Transaction Coordinator:

It can be seen that byteart retail implements two types of Transaction Coordinator: distributedtransactioncoordinator and suppressedtransactioncoordinator. The transactioncoordinatorfactory class creates the required Transaction Coordinator instance based on the passed iunitofwork object. The iunitofwork interface provides a property: distributedtransactionsupported, which indicates whether the current unit of work supports Microsoft's Distributed Transaction coordinatorfactory to create a Transaction Coordinator, all units of work are poll to check whether all units support MSDTC. If all units support MSDTC, the instance of distributedtransactioncoordinator is returned. Otherwise, the instance of suppressedtransactioncoordinator is returned, indicating that the Distributed Transaction processing function is ignored. Distributedtransactioncoordinator encapsulates system. transactions. when the Commit () method is called, the implementation of transactionscope will first call the Commit () method in the base class to submit the unit of work one by one, and then use transactionscope. the complete () method completes distributed transactions. Therefore, the use of distributedtransactioncoordinator ensures that all infrastructure components that support MSDTC (ms SQL Server, Oracle, MSMQ, etc.) are transactional.

Review the code at the above application layer. After the Transaction Coordinator is introduced, the dispatch method can be modified:

public void Dispatch(Guid orderID){    using (ITransactionCoordinator coordinator = TransactionCoordinatorFactory.Create(Context, bus))    {        var salesOrder = salesOrderRepository.GetByKey(orderID);        salesOrder.Dispatch();        salesOrderRepository.Update(salesOrder);        coordinator.Commit();    }}

Because the Entity Framework and SQL local dB are used as the data storage mechanism, context itself supports MSDTC. Whether the Coordinator is distributedtransactioncoordinator depends on whether the bus supports MSDTC. To verify the work of the Transaction Coordinator here, I added another event bus implementation in byteart retail: MSMQ-based event bus (for code, see byteartretail. events. bus. msmqbus class), when the bus. when publish is called, msmqbus distributes the event to MSMQ. After testing, the data storage operation and the operation to send events to MSMQ are indeed completed in the same transaction. After these operations are successfully completed, we can view the message content in MSMQ:

If the selected event bus does not support MSDTC, coordinator will be suppressedtransactioncoordinator, which means there is no guarantee for any distributed transactions. For example, the byteartretail. Events. Bus. eventbus class uses event aggregator to send emails. "Email sending" itself does not support MSDTC, so the transaction here cannot be guaranteed. However, when suppressedtransactioncoordinator performs commit, the database transaction will be committed first. If an exception occurs, the commit to event bus will not be performed afterwards, for the Application Scenario of "email sending", it is sufficient (because no data is changed, but emails are sent out ).

If you are a patient with obsessive-compulsive disorder (and I am also in fact), you will feel that it is still not safe to do so: Because the mail fails to be sent, there will be no other remedial measures to resend the mail. In fact, it is very simple: you can use msmqbus to ensure that the event distribution and database persistence are completed at the same time. When the event is distributed to MSMQ, you can get a background service program to read the event information from MSMQ, then try to send the email. If the email is sent successfully, the event will be removed from MSMQ. Otherwise, try again when the next round-robin occurs.

Finally, let's say a few wordings: MSDTC may cause performance problems. Therefore, when the data consistency requirements are not high, try not to use MSDTC, just like in our mail sending scenario. The Resource Manager type that supports MSDTC is also very limited. Therefore, you should make a proper technical selection in practical applications and do not draw conclusions blindly (msdn should have MSDTC development documentation, but I guess no one will have too much energy to develop this project ). To use MSDTC, you must start the Distributed Transaction Coordinator service on the server:

Domain event significance event-driven solutions

Domain events bring an event-driven solution to enterprise applications, greatly reducing the dependency between application components and applications: When a business operation starts or completes, when an event is generated and distributed to the event bus, the event subscriber can process the event, or even forward it to other receivers. This not only improves the performance of applications (because events can be processed asynchronously), but also does not need to know how events are routed to other places, how to deal with these events in these areas has brought huge benefits to business analysis, development and testing. I will not discuss the advantages of Event Driven Architecture (EDA) in detail. There are too many articles about this on the Internet. If you are interested, please take a look.

Rich Domain Models

This statement may not be appropriate, but it is true in practice. For example, there are two types of aggregation in byteart retail: "user" and "order". "user" itself should not aggregate "orders". From the perspective of the domain model, the existence of "user" does not depend on "order" ("order" is not a component of "user ), therefore, the relationship between it and "car" and "Wheel" is different.

Of course, we have a normal requirement: Maybe all the order information of a user. Now that "user" does not aggregate "orders", it is impossible to navigate from user aggregation to all the order objects under it. What should we do at this time? Before a domain event exists, you can only obtain the user ID at the application layer, then use the user warehouse to obtain the user entity, and then use the order warehouse to find all the orders of the user. Now let's take a look at how this part is implemented after byteart retail introduces domain events.

First, define a getuserordersevent domain event and define an attribute in the "user" Object (because in code writing, the user is used. salesorders is more intuitive), write the following code in the getter of the attribute:

public IEnumerable<SalesOrder> SalesOrders{    get    {        IEnumerable<SalesOrder> result = null;        DomainEvent.Publish<GetUserOrdersEvent>(new GetUserOrdersEvent(this),            (e, ret, exc) =>            {                result = e.SalesOrders;            });        return result;    }}

Then, create an event processor:

public class GetUserOrdersEventHandler : IDomainEventHandler<GetUserOrdersEvent>{    private readonly ISalesOrderRepository salesOrderRepository;    public GetUserOrdersEventHandler(ISalesOrderRepository salesOrderRepository)    {        this.salesOrderRepository = salesOrderRepository;    }    public void Handle(GetUserOrdersEvent evnt)    {        var user = evnt.Source as User;        evnt.SalesOrders = this.salesOrderRepository.FindSalesOrdersByUser(user);    }}

After the event processor completes processing, the domainevent. Publish static method calls back the lambda statement given in the salesorders attribute to return the obtained order.

The meaning here is far from simply modifying the writing method based on the original one. As you can see, this decouples the domain model and warehouse operations. In the domain model, events are distributed in the domain, and all warehousing operations are completed in the event processor. The domain model does not know what will happen after the event is distributed. It only waits for the processing result. Many readers have asked me: how do I access warehousing in a domain model? I think this is the answer.

Improve domain model performance

Assume that an aggregation attribute in the domain model contains a large object, which consumes a lot of time each time the data is read from the warehouse for aggregation, in this case, domain events can be used to solve the problem. Using a method similar to the above, you can simply dispatch a domain event instead of directly reading the attribute from the database, and then return it directly (the application layer can be directly returned ), after the event processor completes Data Reading, it notifies the caller (such as the application layer) with the event model in C #. After the application layer collects all the data, it returns to the presentation layer.

The implementation here can use the async/await programming model of C #5.0. I have not yet had time to practice it. Here is just an idea, but it should not be difficult to implement it. I have waited for a specific case, then perform a detailed analysis.

Summary

This article first puts forward the disadvantages of byteart retail's original domain event model implementation, then provides a solution after reconstruction, and briefly discusses the transaction of event processing, the article also discusses the significance of domain events. Byteart retail is a demo. Of course there will be a lot of incomplete considerations, and I don't have much time to analyze the pros and cons in depth. If a friend can discover the problems and discuss them with you, I think this is not only for myself, but also for others. I sincerely hope this article will give you some inspiration and help you solve the difficulties encountered in practical application.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.