Writing Distributable .NET Application With x2net

Introduction

 
Writing distributed applications, especially deployed across a network, tends to be a challenge. This is not only due to the trickiness of the network programming but more so because of your code and business logic getting messed up with communication details. That makes it probably not flexible but hard to reuse.
 
Meanwhile, most programmers already know how to make their code flexible, reusable, and testable. Yes, reducing code coupling, often achieved by introducing an additional level of indirection, is the definite way to go. Then why don't we apply the same technique to overall application architecture? Simply, decoupling communication details from application logic will help us to build a flexibly distributable, fully testable application consisting of reusable modules.
 
In this article, we will get through a few simple examples of x2net application and see how distribution works in an x2 way.
 
Background 
 

x2 

 
x2 is a set of concepts and specifications that facilitates the development of highly flexible cross-platform, cross-language distributed systems. Before further going on, it is recommended to look at its README.md and concepts.md
 

x2net

 
x2net is the reference port of x2 written in C# targeting universal .NET environments. 
 

Original

 
In order to focus on the structural aspect, we begin with an extremely simple application: 
 
C#
  1. public class Hello  
  2. {  
  3.     public static void Main()  
  4.     {  
  5.         while (true)  
  6.         {  
  7.             var input = Console.ReadLine();  
  8.             if (input == "bye")  
  9.             {  
  10.                 break;  
  11.             }  
  12.             var greeting = String.Format("Hello, {0}!", input);  
  13.             Console.WriteLine(greeting);  
  14.         }  
  15.     }  
  16. }  

Defining Events

 
An x2 application is composed of logic cases (or flows) that communicate only with  one another. So defining a shared event hierarchy is the key activity at design time. In this simple example, we can grab the key feature that makes up a greeting sentence out of the name input. We define a request/response event pair for this feature as follows,
 
XML
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <x2 namespace="hello">  
  3.   <definitions>  
  4.     <!-- Hello request event. -->  
  5.     <event name="HelloReq" id="1">  
  6.       <!-- Input name. -->  
  7.       <property name="Name" type="string"/>  
  8.     </event>  
  9.     <!-- Hello response event. -->  
  10.     <event name="HelloResp" id="2">  
  11.       <!-- Resultant greeting sentence. -->  
  12.       <property name="Greeting" type="string"/>  
  13.     </event>  
  14.   </definitions>  
  15. </x2> 
Running x2net.xpiler on this XML definition file will yield a corresponding C# source file which we can include into our project.
 

Preparing Logic Module

 
Once we define events, we can write the application logic cases to handle those events. Here, we write a simple case which creates the greeting sentence.
 
C#
  1. using x2net;  
  2.   
  3. public class HelloCase : Case  
  4. {  
  5.     protected override void Setup()  
  6.     {  
  7.         // Bind the HelloReq event handler.  
  8.         Bind(new HelloReq(), OnHelloReq);  
  9.     }  
  10.   
  11.     void OnHelloReq(HelloReq req)  
  12.     {  
  13.         // Create a new HelloResp event.  
  14.         new HelloResp {  
  15.             // Set its Greeting property as a generated sentence.  
  16.             Greeting = String.Format("Hello, {0}!", req.Name)  
  17.         }  
  18.             .InResponseOf(req)  // Copy the req._Handle builtin property.  
  19.             .Post();            // And post it to the hub.  
  20.     }  
  21. }  
Please note that logic cases react to their interesting events by posting another event in return. They know nothing about the communication details: where request events come from or where response events are headed. Consequently, these logic cases may be freely located, without any change, at any point of the entire distributed application. And they can also be easily tested in isolation.
 

First x2net Application

 
Having relevant events and cases, now we are ready to set up our first x2net application with these constructs.
 
C#
  1. using x2net;  
  2.   
  3. public class HelloStandalone  
  4. {  
  5.     class LocalCase : Case  
  6.     {  
  7.         protected override void Setup()  
  8.         {  
  9.             // To print the content of HelloResp to the console output.  
  10.             Bind(new HelloResp(), (e) => {  
  11.                 Console.WriteLine(e.Greeting);  
  12.             });  
  13.         }  
  14.     }  
  15.   
  16.     public static void Main()  
  17.     {  
  18.         Hub.Instance  
  19.             .Attach(new SingleThreadFlow()  
  20.                 .Add(new HelloCase())  
  21.                 .Add(new LocalCase()));  
  22.   
  23.         using (new Hub.Flows().Startup())  
  24.         {  
  25.             while (true)  
  26.             {  
  27.                 var input = Console.ReadLine();  
  28.                 if (input == "bye")  
  29.                 {  
  30.                     break;  
  31.                 }  
  32.                 new HelloReq { Name = input }.Post();  
  33.             }  
  34.         }  
  35.     }  
  36. }  
This works exactly the same as our original console application, but in an x2 way:
  • A console input generates a HelloReq event.
  • HelloCase takes the HelloReq event and posts a HelloResp event in return, with the generated greeting sentence.
  • LocalCase takes the HelloResp event and prints its content to console output.
Now that we have an x2 application, we can easily change the threading model or distribution topology of our application. For example, applying the following change will let our every case run in a separate thread: 
 
C#
  1. public static void Main()  
  2. {  
  3.     Hub.Instance  
  4.         .Attach(new SingleThreadFlow()  
  5.             .Add(new HelloCase()))  
  6.         .Attach(new SingleThreadFlow()  
  7.             .Add(new LocalCase()));  
Changing its threading model may not be so interesting. But how about making it a client/server application in minutes?
 

Client/Server Distribution

 
First, we prepare a server that runs the HelloCase as its main logic case: 
 
C#
  1. using x2net;  
  2.   
  3. public class HelloTcpServer : AsyncTcpServer  
  4. {  
  5.     public HelloTcpServer() : base("HelloServer")  
  6.     {  
  7.     }  
  8.   
  9.     protected override void Setup()  
  10.     {  
  11.         // Will receive HelloReq events through this link.  
  12.         EventFactory.Register<HelloReq>();  
  13.         // Will send out CapitalizeResp events through this link.  
  14.         // Events will be dispatched according to the _Handle property.  
  15.         Bind(new HelloResp(), Send);  
  16.         // Listen on the port 6789 on start.  
  17.         Listen(6789);  
  18.     }  
  19.   
  20.     public static void Main()  
  21.     {  
  22.         Hub.Instance  
  23.             .Attach(new SingleThreadFlow()  
  24.                 .Add(new HelloCase())  
  25.                 .Add(new HelloTcpServer()));  
  26.   
  27.         using (new Hub.Flows().Startup())  
  28.         {  
  29.             while (true)  
  30.             {  
  31.                 var input = Console.ReadLine();  
  32.                 if (input == "bye")  
  33.                 {  
  34.                     break;  
  35.                 }  
  36.             }  
  37.         }  
  38.     }  
  39. }  
Then, we can write a simple client to connect to the server to get things done: 
 
C#
  1. using x2net;  
  2.   
  3. public class HelloTcpClient : TcpClient  
  4. {  
  5.     class LocalCase : Case  
  6.     {  
  7.         protected override void Setup()  
  8.         {  
  9.             // To print the content of HelloResp to the console output.  
  10.             Bind(new HelloResp(), (e) => {  
  11.                 Console.WriteLine(e.Greeting);  
  12.             });  
  13.         }  
  14.     }  
  15.   
  16.     public HelloTcpClient() : base("HelloClient")  
  17.     {  
  18.     }  
  19.   
  20.     protected override void Setup()  
  21.     {  
  22.         // Will receive CapitalizeResp events through this link.  
  23.         EventFactory.Register<HelloResp>();  
  24.         // Will send out every HelloReq event through this link.  
  25.         Bind(new HelloReq(), Send);  
  26.         // Connect to localhost:6789 on start.  
  27.         Connect("127.0.0.1", 6789);  
  28.     }  
  29.   
  30.     public static void Main()  
  31.     {  
  32.         Hub.Instance  
  33.             .Attach(new SingleThreadFlow()  
  34.                 .Add(new LocalCase())  
  35.                 .Add(new HelloTcpClient()));  
  36.   
  37.         using (new Hub.Flows().Startup())  
  38.         {  
  39.             while (true)  
  40.             {  
  41.                 var input = Console.ReadLine();  
  42.                 if (input == "bye")  
  43.                 {  
  44.                     break;  
  45.                 }  
  46.                 new HelloReq { Name = input }.Post();  
  47.             }  
  48.         }  
  49.     }  
  50. }   
Please note that the HelloCase does not change whether it is run in a standalone application or in a server.
 
In the above server link, you might wonder how we send a response event to the very client who issued the original request. The built-in event property _Handle does the trick. When an x2net link receives an event from a network, its _Handle property is set as the unique link session handle. If the _Handle property of the response event is the same as the original request, which is done by the InResponseOf extension method call, the server can locate the target link session with the _Handle property in this simple example.
 

Wrapping Up

 
The logic-communication decoupling itself is neither a new nor a popular concept. If you are accustomed to SendPacket-like communication, it may take some time until you feel comfortable with the x2-style distribution. This shift is somewhat like moving from message passing to generative communication, and it is surely worth a try.