TU Tran

Technologies should serve for business purpose.

NAVIGATION - SEARCH

[TinyERP]Using CQRS (basic)

Overview about CQRS

CQRS stands for Command Query Responsibility Segregation that was described by Grey Young.
For more information about this, you can search from the internet easily, so in this article, we will not spend time for reinventing the wheel.

In web application, client sends request to server/api to perform specified action. Such as: create new order, create new user, get user information...

Those actions can be categorized into 2 types (Command and Query) in CQRS:

  1. Query: client wants to retrieve data from server/api. This action can be completed in a short period of time.
  2. Command: client wants to create new resource or modify the state of current resource in system. This action may require a long time to complete and use expensive resource (such as: database connection, ...)

For easier to understand, resource can be data/ entity of system, such as: user information, order information, ....

(click to see the full size)

As shown in photo above, Command and Query were handled separately. You may come up with the list of questions:

  1. Why do we need Command and Query being handled separately?
  2. Why do we need to maintenance "Write Database" and "Read Database" at the sample time?
  3. Why do we need to raise event(s) for each command handled (step 4)?

Why do we need Command and Query being handled separately?

  • Command Action usually takes long time to handle as we need to verify the business rules. For example: we need to check customer balance before depositing from their account, this makes take long.
  • Query action usually handles quickly as data in "Read database" was already in denormalized form as needed.
  • Most of requests to our application are for retrieving data and display on the UI. So we can add more servers handling query action if need...
  • User can wait longer when updating data than getting data from the system.

By separate Read and Write, We can optimize performance or improve security of our system better.

 Why do we need to maintenance "Write Database" and "Read Database" at the same time?

Let see the sample UI in the application that we want to get data for as below:

(Please do not spend your time on minor issues, such as: price unit or date-time format, ...)

For this function, we may have database diagram for this as below:

And T-SQL statement for getting data:

SELECT 
	Orders.Id, 
	OrderCustomerDetails.[Name], 
	Orders.CreatedDate,
	SUM(OrderLines.Quantity) as "TotalItems",
	SUM(OrderLines.Quantity*OrderLines.Price) as "TotalPrice"
FROM OrderAggregates as Orders
LEFT OUTER JOIN OrderLines ON Orders.id=OrderLines.OrderId
LEFT OUTER JOIN Products ON Products.Id=OrderLines.ProductId
LEFT OUTER JOIN OrderCustomerDetails ON Orders.CustomerDetailId=OrderCustomerDetails.Id
GROUP BY Orders.Id, OrderCustomerDetails.[Name], Orders.CreatedDate


This T-SQL for getting data is rather simple and works well for 100 of orders or 1000. But what was about 1M or 10M (M= millions) of orders.

This usually was the root cause for performance problem in enterprise application. This can be solved if we have necessary data beforehand, then just query and return to the client side.

You can see that how did "Read Database" improve performance of our system. And "Write Database" can be used for validation, ....

Why do we need to raise event(s) for each command handled (step 4)?

As above question, we have "Read Database" along with "Write Database" and need to synchronize data between them.

For example, Product was ordered and out of stock in "Write Database", this was not updated on "Read Database".

Then manager looks at the inventory report (get data from Read Database) and found that we have that product in stock, but our client can not order that product (validation fail as no data in Write Database) as it was already out of stock.

Above, providing you the basic knowledge about CQRS, We will learn how to implement this in TinyERP.

How was CQRS used in TinyERP?

Let see the example how can we create new Order and get back the list of created Orders as sample above.

Analyzing the requirement, we have the following steps as below:

  1. For creating new Order:
    • Define OrderHandler: This will receive create new order request from client side. This acts the same as Controller in WebAPI and why not OrderController, we will not discuss here.
    • Implement OrderCommandHandler, this will handle logic of creating new Order, including validation, update number of available product in Write Database, .....
    • Raise appropriated events for creating order Order, such as: OnOrderCreated, ..., so we can listen those event and update appropriated information into Read Database.
    • Implement EventHandler for Order, this will update appropriated data into "Read Database"
  2. For getting list of Orders
    • Define OrderHandler: This will receive request from client side
    • Implement OrderQuery and get the list of orders from "Read Database".

Create new Order

  1. Define OrderHandler:

    Let define new OrderHandler in "App.Api/Features/Order" folder:

    namespace App.Api.Features.Order
    {
        [RoutePrefix("api/orders")]
        public class OrderHandler : CommandHandlerController<OrderAggregate>
        {
            [Route("")]
            [HttpPost()]
            [ResponseWrapper()]
            public void CreateOrder(CreateOrderRequest request)
            {
                this.Execute(request);
            }
        }
    }

    and CreateOrderRequest class as DTO for receiving data from client side in "App.Command/Order":

    namespace App.Command.Order
    {
        public class CreateOrderRequest : BaseCommand
        {
            public CustomerDetail CustomerDetail { get; set; }
            public IList<OrderLine> OrderLines { get; set; }
        }
    }
    

    For more detail about CustomerDetail and OrderLine classes, Please check out the code from Github (https://github.com/techcoaching/TinyERP/tree/develop).

  2. Implement OrderCommandHandler
    We need to define OrderCommandHandler class in "App.Command.Impl/Order", this class will handle the main logic for creating new Order, including validation, update available items in Write Database, ...:
    namespace App.Command.Impl.Order
    {
        public class OrderCommandHandler : IOrderCommandHandler
        {
            public void Handle(CreateOrderRequest command)
            {
    			/*This is logic of creating new order*/
                OrderAggregate order = AggregateFactory.Create<OrderAggregate>();
                order.AddCustomerDetail(command.CustomerDetail);
                order.AddOrderLineItems(command.OrderLines);
                using (IUnitOfWork uow = new UnitOfWork(new AppDbContext(IOMode.Write)))
                {
                    IOrderRepository repository = IoC.Container.Resolve<IOrderRepository>(uow);
                    repository.Add(order);
                    uow.Commit();
    				/*Add OnOrderCreated event. So EventHandler can receive this event and update into ReadDatabase */
                    order.AddEvent(new OnOrderCreated(order.Id));
                }
    			/*And raise appropriated events for creating new Order*/
                order.PublishEvents();
            }
        }
    }
    

    In this sample, we assume that request is valid and does not need to validate. Look at AddCustomerDetail and AddOrderLineItems methods, we also have 2 other events were created (OnCustomerDetailChanged and OnOrderLineItemAdded)

  3. Raise appropriated events for creating order Order

    As we can see that, Creating  new Order, we have the following events need to be handled in EventHandler:

    - OnOrderCreated: this was raised when new Order was created and contained the information of created order, such as: Id.

    - OnOrderLineItemAdded: this was raised for each order item was added into current order. This event also contains information of added order item. such as: Product information, price, quantity, ...

    - OnCustomerDetailChanged: this was raised when we attach or update customer information of order.

    In Above command handler, we have this line of code for raising those event:

    order.PublishEvents();

     For raising events, the listener can be InApp (existing in the sample deployed application) or Remote (in other server or domain). We will check this more detail in next part (How to scale your architecture)

  4. Implement EventHandler for Order, this will update appropriated data into "Read Database"

    For handling event of Order, we need to define IOrderEventHandler interface which implement appropriated IEventHandler<EventType>. This will listen on "EventType"  and Execute (EventType ev) method will be called if EventType was raised in the app.

    namespace App.EventHandler.Order
    {
        public interface IOrderEventHandler: 
    		IEventHandler<OnOrderCreated>,
            IEventHandler<OnCustomerDetailChanged>,
            IEventHandler<OnOrderLineItemAdded>,
        {
        }
    }
    

    In this case, IOrderEventHandler interface listens on 3 events: OnOrderCreated, OnOrderLineItemAdded, OnCustomerDetailChanged.

     Then, we define OrderEventHandler class which implement IOrderEventHandler interface. This class will implement the logic for specified event.

    namespace App.EventHandler.Impl.Order
    {
        public class OrderEventHandler : IOrderEventHandler
        {
            public void Execute(OnOrderCreated ev)
            {
                /*Handling OnOrderCreated logic goes here*/
            }
    
            public void Execute(OnOrderLineItemAdded ev)
            {
                /*Handling OnOrderLineItemAdded logic goes here*/
            }
    
            public void Execute(OnCustomerDetailChanged ev)
            {
                /*Handling OnCustomerDetailChanged logic goes here*/
            }
        }
    }
    

    We have 3 Execute method, with appropriated type of event as parameter. "Execute(OnOrderCreated ev)" method will be called for handling "OnOrderCreated" event.

    Let try to implement OrderEventHandler class as below:

    namespace App.EventHandler.Impl.Order
    {
        public class OrderEventHandler : IOrderEventHandler
        {
            public void Execute(OnOrderActivated ev)
            {
                IOrderQuery query = IoC.Container.Resolve<IOrderQuery>();
                App.Query.Entity.Order.Order order = query.GetByOrderId(ev.OrderId);
                order.IsActivated = true;
                query.Update(order);
            }
            public void Execute(OnOrderCreated ev)
            {
                IOrderQuery query = IoC.Container.Resolve<IOrderQuery>();
                query.Add(new App.Query.Entity.Order.Order(ev.OrderId));
            }
            public void Execute(OnOrderLineItemAdded ev)
            {
                IOrderQuery query = IoC.Container.Resolve<IOrderQuery>();
                App.Query.Entity.Order.Order order = query.GetByOrderId(ev.OrderId);
                order.OrderLines.Add(new OrderLine(ev.ProductId, ev.ProductName, ev.Quantity, ev.Price));
                order.TotalItems += ev.Quantity;
                order.TotalPrice += ev.Price * (decimal)ev.Quantity;
                query.Update(order);
            }
            public void Execute(OnCustomerDetailChanged ev)
            {
                IOrderQuery query = IoC.Container.Resolve<IOrderQuery>();
                App.Query.Entity.Order.Order order = query.GetByOrderId(ev.OrderId);
                order.Name = ev.CustomerName;
                query.Update(order);
            }
        }
    }

    What we are doing is simple, just update directly into "Read Database", for example, with "OnOrderCreated" event. We call to:

    IOrderQuery query = IoC.Container.Resolve<IOrderQuery>();
    query.Add(new App.Query.Entity.Order.Order(ev.OrderId));

    Ok, To this point, new Order was created in "Write Database" (MSSQL database) and "Read Database" (MongoDB).

    How data can be inserted into MongoDb as we did not mention about this before.

Currently, IOrderQuery get data from/ insert into MongoDB. You can find out more interesting in next topic "Using Multiple Data Stores in TinyERP".

As in the photo below is how the data was stored in "Read Database":

 In next section, We will get this data and return to client, without potential performance issues as in traditional.

Get list of Orders

  1. Define OrderHandler

    This was the same as traditional WebApi controller, let append new method into OrderHandler

    [HttpGet()]
    [Route("")]
    [ResponseWrapper()]
    public IList<OrderSummaryItem> GetOrders()
    {
    	IOrderQuery query = IoC.Container.Resolve<IOrderQuery>();
    	return query.GetOrders<OrderSummaryItem>();
    }

    In this method, We just get the list of orders from database and return to client side. no more.

  2. Implement OrderQuery and get the list of orders from "Read Database"

    Look at implementation of IOrderQuery interface, we have OrderQuery class with a simple method:

    namespace App.Query.Impl.Order
    {
        public class OrderQuery : BaseQueryRepository<Order>, IOrderQuery
        {
            public IList<TEntity> GetOrders<TEntity>() where TEntity : IMappedFrom<Order>
            {
                return this.GetItems<TEntity>();
            }
        }
    }
    

    Note: this was inherit from BaseQueryRepository<Order> which will get from/ update to MongoDB. We will discuss more about this in "Using Multiple Data Stores in TinyERP" topic.

    Calling to "<base Uri>/api/orders" from REST client:

    And from the UI:

    You can see that, we can get the list of orders in simple way, WITHOUT HEAVY JOIN which usually raise potential performance issues.

 

 For more information about other articles in this series

Thank you for reading,

Note: Please like and share to your friends if you think this is usefull article, I really appreciate

Add comment