⬆️ ⬇️

Incoding Rapid Development Framework (part 2 CQRS)

image



Pre story



My previous article was an introduction to the Incoding Framework, which began with IML (our flagship feature). IML pushed us to develop the project more than a set of utilities (this kind of goodness is completely in any development team) used in the company's projects, but this does not mean that other components are not worked out, but instead are “polished” with no less detail and I will try to prove it to you .



Silver bullet?



Previously, I have always been a supporter of the fact that every solution has its drawbacks and advantages, but the CQRS in my opinion surpasses the N-Layer, and also has no "contraindications" or "side effects", which makes it a candidate for the first cartridge, But first things first.

')

Has anyone heard of CQRS?



For those who are already using CQRS, the first sections may not be interesting, so before putting the label “bicycle”, I suggest that you familiarize yourself with the killing feature section, which can convince you otherwise. Those who use N-Layer architecture should think about switching to CQRS and in order to back up my proposal I will describe our way to CQRS





Oh, how convenient a lot of abstractions (opinion from the past)



When we were just starting to develop the application, we chose N-Layer as the server-side architecture, which divides the application into multiple layers, thereby allowing to design different parts of the project independently of each other, but in the end we got the following problems:



Note: the idea of ​​the N-Layer is also related to the substitution dll of a certain layer, but this extremely rarely claimed task is much easier solved through IoC.



The main source of "evil" in the N-Layer is most often the service layer, which is designed to hide the details of the business processes and application logic by aggregating similar tasks into one class, which over time turns it into a GOD object and leads to problems:





If not to be given in subtleties, then conditionally alteration on the CQRS will be to divide large objects into small

public interface IUserService { void Add(string name); void Delete(string id); List<User> Fetch(Criteries criteries); } 


After decomposition, we get two Command (AddUser, DeleteUser) and query (FetchUser), which increases the number of classes, but allows us to get rid of the few related methods in the class.

“Now, even more classes need to be written!” - this is the first question asked by the “true” N-Layer developers, but as a counterargument, we can single out what we get atomic (no dependencies between objects) of tasks, and this is worth it:

  1. Fewer conflicts in VCS (Version Controler System)
  2. Search by class is easier than by method.
  3. You no longer need to separate the UserService by partial, because it’s difficult to navigate in one))
  4. The issue on the bugtracker is formed from Command and Query
  5. Sprint for agile is formed from Command and Query
  6. Testing of small objects




Our implementation



For the impatient, who wants to immediately “feel” the code, you can download the source code (ibid example is the Incoding CQRS + MVD bundle) from GitHub , we will consider them as an example.

note: to start a project, you need to create an empty database and specify its ConnectionString in the web.config (main key)



Dispatcher


The key element that performs Message ( Command or Query ) in a single transaction (Unit Of Work).

Controller - in the framework of the asp.net mvc (console, wpf, owin and etc) system to use the dispatcher, you need to get an instance of it from IoC and then two methods are available:



 public ActionResult Test() { var dispatcher = IoCFactory.Instance.TryResolve<IDispatcher>(); var id = dispatcher.Query(new GetIdQuery()); dispatcher.Push(new DeleteEntityByIdCommand(id)); return something ActionResult; } 




Unit Of Work - if you look at the implementation of any Command , you can see that the main code is contained in the overloaded Execute method, which can be called without Dispatcher participation, but then the connection to the database and the transaction will not be opened.

 new DeactivateEntityCommand().Execute(); // without transaction and connection dispatcher.Push(new DeactivateEntityCommand()); // open transaction and connection 




Message


CommandBase and QueryBase are children of Message, but their behavior differs in the type of Isolation Level with which the Unit Of Work is created.



note: the restriction may seem tough, but if for some reason you need to save (delete, insert) data in Query, you should revise your script by splitting into smaller tasks.



Message has two main tools:

Repository - database interface, supports all CRUD scripts

Examples
Create


 Repository.Save(new entity()) 


Read


 Repository.GetById<TEntity>(id); Repository.Query(whereSpecification: spec, orderSpecification:spec, paginatedSpecification:spec) 


Note: Query (Paginated) is the most extensive method of the Repository, which with the help of the specification to the query describes the data to be obtained.

Update


 var entityFromDb = Repository.GetById<TEntity>(id); entityFromDb.Title = "New title"; // tracking 


note: if provider ORM does not support tracking, then you need to call the Repository.SaveOrUpdate (entity) method

Delete


 Repository.Delete<TEntity>(id); 






Event Broker - communication between Command, which allows you to aggregate re-occurring "pieces" of code and encapsulate in events and subscribers.



Task: audit of some actions

Problem: the code for saving Audit will be the same and you will have to repeat it in each Command.

Solution in the Service Layer: you can select the base class ServiceWithAuditBase, but it will be difficult to maintain with increasing complexity of the audit, and inheritance always leads to complication.

Solution with subscribers

Event code

 public class OnAuditEvent : IEvent { public string Message { get; set; } } 


note: condition for Event to implement IEvent

Subscriber code

 public class AuditSubscriber : IEventSubscriber<OnAuditEvent> { readonly IRepository repository; public AuditSubscriber(IRepository repository) { this.repository = repository; } public void Subscribe(OnAuditEvent @event) { this.repository.Save(new Audit { Message = @event.Message }); } public void Dispose() { } } 


Note: Subscriber is created through IoCFactory and therefore you can inject in ctor (constructor) or use IoCFactory.Instance.TryResolve ()

Command Code

 EventBroker.Publish(new OnAuditEvent { Message = "New product {0} by {1}".F(Title, Price) }); 




Query


To create a custom Query, you need to inherit QueryBase, where you specify the expected data return and override the ExecuteResult method

 public class GetProductsQuery : QueryBase<List<GetProductsQuery.Response>> { public class Response { public string Title { get; set; } public string Price { get; set; } } public string Title { get; set; } public decimal? From { get; set; } public decimal? To { get; set; } protected override List<Response> ExecuteResult() { return Repository.Query(whereSpecification: new ProductByTitleWhere(this.Title) .And(new ProductBetweenPriceWhere(this.From, this.To))) .Select(product => new Response { Title = product.Title, Price = product.Price.ToString("C") }) .ToList(); } } 


It can be noted that the nested class is used as the Result, but why not ...

Return immediately an object from the database (Entity) - this method has a problem associated with the work area of ​​the session connecting to the database, consider with an example.

Example
Query code

 return Repository.Query<Product>(); 


Controller Code

 dispatcher.Query(new GetProductsQuery()) .Select(r=> new { Amount = r.Orders.Sum(r=>r.Price)) 


The error will be in the runtime, if you do not turn off Lazy Load (relevant only for OLAP objects) at the ORM mapping level, because after the Query is completed, the session is closed, and when you access the Orders field, a query is sent to the database.



ViewModel is the same as the nested class, but with the ability to reuse in other Query, which is an extremely rare scenario.



Command


Our first implementations of Command for CQRS were with the description (AddUserCommand) from the executor (UserCommandHandler), which made the development process more complicated, so these parts were later combined.

note: the main reason for separation was the support of the DTO (Data Transfer Object) model for SOAP systems, but with the advent of asp.net mvc, it just became irrelevant



To create a custom Command, you need to inherit CommandBase and override the Execute method.

 public class AddProductCommand : CommandBase { public string Title { get; set; } public decimal Price { get; set; } public override void Execute() { var product = new Product { Title = Title, Price = Price } Repository.Save(product); Result = product.Id; } } 


note: there are scenarios where the Command should return data, then you can set Result in the Command method



Killing feature



Composite


CQRS helps to break up complex tasks into smaller ones, but the problem of a general transaction occurs.

Task : save the object in 3 stages

Solution : divided into three commands (Step1Command, Step2Command, Step3Command)

Condition : Transaction

 public ActionResult Save(Step1Command step1,Step2Command step2,Step3Command step3) { dispatcher.Push(composite => { composite.Quote(step1); composite.Quote(step2); composite.Quote(step3); }); return IncodingResult.Success(); } 


In addition to grouping the Command in one package, Composite allows you to manipulate the results of execution. Let us complicate the task and set a condition for Step1 to transmit the Id of a new element after Step 1 and Step 2 and 3.

 public ActionResult Save(Step1Command step1,Step2Command step2,Step3Command step3) { dispatcher.Push(composite => { composite.Quote(step1,new MessageExecuteSetting { OnAfter = () => { step2.Id = step1.Result; step3.Id = step1.Result; } }); composite.Quote(step2); composite.Quote(step3);}); } 




“Hot” change connection string


If the application receives the connection string not at the start, but during the operation, for example, after logging into the system, the third-party service provides the address for the current session, then you should be able to change the path specified earlier.

 dispatcher.Query(query, new MessageExecuteSetting { Connection = new SqlConnection(currentConnectionString) }); 


Inherited system


Wrapping over the “old ″ base is not a problem in the Incoding Framework. There are tools that allow you to avoid creating additional infrastructure for working with different configurations and develop Command and Query without taking this detail into account.

 dispatcher.Query(query, new MessageExecuteSetting { DataBaseInstance = "Instance 2" }); 


note: each key has its own ORM configuration



Conclusion



The article focuses primarily on the review of the implementation of Incoding CQRS, so the review of the CQRS methodology itself is brief, and it is already well described in other sources. Incoding CQRS is one of the parts of our framework, but it is completely self-contained and is used without other components (IML, MVD, Unit Test).



In the comments to the first article about IML, there were questions about the possibility of using alternative (OWIN) platforms for asp.net mvc, so I’ll immediately notice that Incoding CQRS was used in WPF projects, and as for IML, this month will be an article about integration .



PS Glad to hear feedback and comments, as well as questions on the work of the framework. The next green hero Incoding MVD who fights against copy & paste will be in a couple of weeks)))

Source: https://habr.com/ru/post/211206/



All Articles