📜 ⬆️ ⬇️

The impossible is possible. Stateful behavior in a stateless application!

When developing web applications, interactive communication with the user is often necessary in the process of performing some actions. Web ERP-systems, in turn, impose on such communication rather specific requirements. After commissioning several variants of such systems, I found a method that seemed to me the most acceptable. Now I want to share my solution to the problem of interactive work with the user when performing actions on the server.





')
So, when developing a complex web application, you need to combine several conflicting requirements:
  1. The application code of a business solution must contain a minimum of artifacts that are not relevant to the business problem.
  2. the business transaction must take place in one indivisible transaction
  3. during the code execution, interactive communication with the user is necessary
  4. waiting for a response from the user should not occupy server resources and block the work of other users
  5. the application code is executed on the server, communication with the client goes through the browser


A typical business task: placing an order, in the process of ordering on the server checks are made that require the operator to interactive input. For example, we want to ask the user to confirm his actions when certain conditions are met.

In accordance with the requirement of simplicity of the business code, we would like the application code to look something like this (attention, pseudocode!):

var  = ();  (.()) { (“      , ?”); } (); 


In this case, in the confirmation line, we would like magic to occur:
  1. Code suspended work.
  2. The request Yes / No has come to the unconfirmed form of the client.
  3. If he clicks "Yes" - we continue to work.
  4. If “No” - give him to re-enter the form and send again.


I propose to your court my “magical” solution, which has already been tested and successfully works in my projects.

Let's try to solve the problem of issuing a typical “Yes / No” dialogue.


The main problem is that we cannot pause the operation on the server while waiting for a response from the user for several reasons:
  1. All client-server communication occurs asynchronously and over a stateless protocol.
  2. we cannot occupy resources while waiting for a response from the user

However, the user should have the impression of continuity of action. Similarly, the programmer should not think about the fact that the execution of his code can be interrupted. Therefore, it is necessary to use some tricks.

To begin with, we introduce the concepts of request identifiers and actions.
Request ID is a unique request ID that is generated on the client. For each request to the server, its identifier is its own, except for the case when we request user input.
Action ID - unique identifier of the action. To be more precise, the unique identifier of the place in the code in which the user's response is requested.

These two identifiers allow you to organize the client-server operation scheme in such a way that you can determine exactly when and what was requested from the user and what answer he gave.

This is how the scheme looks like:


Thus, the developer has the impression that his method is executed only 1 time. The user, accordingly, also seems that he performed only 1 action.

Consider how this idea might look like for a programmer.
 public class HelloNewWorldOrder { // ,   Guid guid = new Guid("5FFD6DB4-1201-44BF-9DE0-DC199AC004D9"); public void KillAllHumans(Human[] humans) { foreach (var human in humans) { if (human.Name == Context.Current.User.Name) { //      ExceptionHelper.Interactive(guid, "     .   ?"); } human.Kill(); } } } 


In my opinion, it looks pretty friendly :)

ExceptionHelper.Interactive itself looks something like this:
 public static void Interactive(Guid id, string message) { //    var key = RequestHelper.GetRequestId(); var exists = Query.All<InteractiveException>() .Any(r => r.RequestId == key && r.ExceptionId == id); if (exists) { return; } throw new InteractiveException(message, id, key); } 


It remains to add only the record of omitted exceptions in the database. For example, this can be done in Global.asax, the base controller, or where we expect similar communication with the user.

In this simple way, we have achieved that the behavior of the desktop application that stores its state between user actions is emulated. At the same time, the real state of the application is not stored anywhere, no resources are blocked, and no restrictions on the user response time are imposed.

You can try this system in action at http://demo.oreodor.com/Parts/Main.aspx#Order:Regular . The approach described in the article is used there when placing the order.

The source code of the validation validity action is as follows:

 /// <summary> ///   /// </summary> [Icon(ExtIcon.Accept)] public class OrderCompleteAction : IAction<IFormContext<Order>> { /// <summary> ///  1 /// </summary> private Guid e1 = new Guid("84099696-2225-41F9-AF54-0BE66367CEAA"); /// <summary> ///  1 /// </summary> private Guid e2 = new Guid("26142EDB-3DC8-4B00-920F-FA33FC3ADF40"); /// <summary> ///   /// </summary> /// <param name="context"> </param> public void Execute(IFormContext<Order> context) { Assert.That(context.Item, Is.Not.Null, "   , !"); var cpus = context.Item.Items.Select(m => m.Linked) .OfType<Cpu>().Select(c => c.SocketType.SysName).ToHashSet(); var mbs = context.Item.Items.Select(m => m.Linked) .OfType<MotherBoard>().Select(c => c.SocketType.SysName).ToHashSet(); var coolers = context.Item.Items.Select(m => m.Linked) .OfType<Cooler>().SelectMany(c => c.Sockets.Select(m => m.Linked.SysName)).ToHashSet(); if (!cpus.IsSubsetOf(coolers)) { ExceptionHelper.Interactive(e1, "       ."); } if (!cpus.IsSubsetOf(mbs)) { ExceptionHelper.Interactive(e2, "       . ."); } context.Item.Status = Status.BuiltInStatuses.Work.ToEntity<Status>(); context.ShowMsgBox("   ."); } } 

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


All Articles