Click here to Skip to main content
15,886,362 members
Articles / Programming Languages / C#

Writing Distributable .NET Application with x2net

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
5 Apr 2018CPOL5 min read 15.8K   123   20   2
Introduction to a new approach to distribution

Introduction

Writing distributed applications, especially deployed across a network, tends to be a challenge, not only due to the trickiness of network programming but more so because your code, business logic messed up with communication details, is probably not flexible, hard to reuse and test in isolation.

Meanwhile, most programmers already know how to make their code flexible, reusable, and testable. Yes, reducing code coupling, often achieved by introducing 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 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 give a look to its README.md and concepts.md.

x2net

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

Using the Code

In order to focus on the structural aspect, we begin with an extremely simple application, Hello:

C#
public class Hello
{
    public static void Main()
    {
        while (true)
        {
            var input = Console.ReadLine();
            if (input == "bye")
            {
                break;
            }
            var greeting = String.Format("Hello, {0}!", input);
            Console.WriteLine(greeting);
        }
    }
}

Defining Events

An x2 application is composed of logic cases (or flows) which communicate only with events one another. So defining shared event hierarchy is the key activity in 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
<?xml version="1.0" encoding="utf-8"?>
<x2 namespace="hello">
  <definitions>
    <!-- Hello request event. -->
    <event name="HelloReq" id="1">
      <!-- Input name. -->
      <property name="Name" type="string"/>
    </event>
    <!-- Hello response event. -->
    <event name="HelloResp" id="2">
      <!-- Resultant greeting sentence. -->
      <property name="Greeting" type="string"/>
    </event>
  </definitions>
</x2>

Running x2net.xpiler on this XML definition file will yield a corresponding C# source file we can include into our project.

Preparing Core Logic Modules

Once we define events, we can write the application logic cases to handle those events. Here, we write a simple case which creates the hello sentence:

C#
public class HelloCase : Case
{
    protected override void Setup()
    {
        // Bind the HelloReq event handler.
        Bind(new HelloReq(), OnHelloReq);
    }

    void OnHelloReq(HelloReq req)
    {
        // Create a new HelloResp event.
        new HelloResp {
            // Set its Greeting property as a generated sentence.
            Greeting = String.Format("Hello, {0}!", req.Name)
        }
            .InResponseOf(req)  // Copy the req._Handle builtin property.
            .Post();            // And post it to the hub.
    }
}

Please note that logic cases react to their interested 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 for. 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#
public class HelloStandalone
{
    class LocalCase : Case
    {
        protected override void Setup()
        {
            // To print the content of HelloResp to the console output.
            Bind(new HelloResp(), (e) => {
                Console.WriteLine(e.Greeting);
            });
        }
    }

    public static void Main()
    {
        Hub.Instance
            .Attach(new SingleThreadFlow()
                .Add(new HelloCase())
                .Add(new LocalCase()));

        using (new Hub.Flows().Startup())
        {
            while (true)
            {
                var input = Console.ReadLine();
                if (input == "bye")
                {
                    break;
                }
                new HelloReq { Name = input }.Post();
            }
        }
    }
}

This works exactly the same as our original console application, but in 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#
...
    public static void Main()
    {
        Hub.Instance
            .Attach(new SingleThreadFlow()
                .Add(new HelloCase()))
            .Attach(new SingleThreadFlow()
                .Add(new LocalCase()));
...

Changing its threading model may not be so interesting. But how about making it a client/server application in minutes?

2-Tier Distribution: Client/Server

First, we prepare a server which runs the HelloCase as its main logic case:

C#
public class HelloTcpServer : AsyncTcpServer
{
    public HelloTcpServer() : base("HelloServer")
    {
    }

    protected override void Setup()
    {
        // Will receive HelloReq events through this link.
        EventFactory.Register<HelloReq>();
        // Will send out HelloResp events through this link.
        // Events will be dispatched according to the _Handle property.
        Bind(new HelloResp(), Send);
        // Listen on the port 6789 on start.
        Listen(6789);
    }

    public static void Main()
    {
        Hub.Instance
            .Attach(new SingleThreadFlow()
                .Add(new HelloCase())
                .Add(new HelloTcpServer()));

        using (new Hub.Flows().Startup())
        {
            while (true)
            {
                var input = Console.ReadLine();
                if (input == "bye")
                {
                    break;
                }
            }
        }
    }
}

Then we can write a simple client to connect to the server to get things done:

C#
public class HelloTcpClient : TcpClient
{
    class LocalCase : Case
    {
        protected override void Setup()
        {
            // To print the content of HelloResp to the console output.
            Bind(new HelloResp(), (e) => {
                Console.WriteLine(e.Greeting);
            });
        }
    }

    public HelloTcpClient() : base("HelloClient")
    {
    }

    protected override void Setup()
    {
        // Will receive HelloResp events through this link.
        EventFactory.Register<HelloResp>();
        // Will send out every HelloReq events through this link.
        Bind(new HelloReq(), Send);
        // Connect to localhost:6789 on start.
        Connect("127.0.0.1", 6789);
    }

    public static void Main()
    {
        Hub.Instance
            .Attach(new SingleThreadFlow()
                .Add(new LocalCase())
                .Add(new HelloTcpClient()));

        using (new Hub.Flows().Startup())
        {
            while (true)
            {
                var input = Console.ReadLine();
                if (input == "bye")
                {
                    break;
                }
                new HelloReq { Name = input }.Post();
            }
        }
    }
}

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 network, its _Handle property is set as the 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, the server can locate the target link session with the _Handle property.

Adding Features

Let's say that we are to add a new feature that converts the result string to uppercase letters. We append two more events to the definition file as follows:

XML
<?xml version="1.0" encoding="utf-8"?>
<x2 namespace="hello">
  <definitions>
    <!-- Hello request event. -->
    <event name="HelloReq" id="1">
      <!-- Input name. -->
      <property name="Name" type="string"/>
    </event>
    <!-- Hello response event. -->
    <event name="HelloResp" id="2">
      <!-- Resultant greeting sentence. -->
      <property name="Greeting" type="string"/>
    </event>

    <!-- Capitalize request event. -->
    <event name="CapitalizeReq" id="3">
      <!-- Input string. -->
      <property name="Input" type="string"/>
    </event>
    <!-- Capitalize response event. -->
    <event name="CapitalizeResp" id="4">
      <!-- Output string. -->
      <property name="Output" type="string"/>
    </event>
  </definitions>
</x2>

And we add a new logic case to our shared module:

C#
public class CapitalizerCase : Case
{
    protected override void Setup()
    {
        // Bind the CapitalizeReq event handler.
        Bind(new CapitalizeReq(), OnCapitalizeReq);
    }

    void OnCapitalizeReq(CapitalizeReq req)
    {
        // Create a new CapitalizeResp event.
        new CapitalizeResp {
            // Set its Output property, applying ToUpper() method.
            Output = req.Input.ToUpper()
        }
            .InResponseOf(req)  // Copy the req._Handle builtin property.
            .Post();            // And post it to the hub.
    }
}

Then we can rewrite our standalone application as follows:

C#
public class HelloStandalone
{
    class LocalCase : Case
    {
        protected override void Setup()
        {
            // To chain a new CapitalizeReq in response of a HelloResp event.
            Bind(new HelloResp(), (e) => {
                new CapitalizeReq {
                    Input = e.Greeting
                }.InResponseOf(e).Post();
            });
            // To print the content of CapitalizeResp to the console output.
            Bind(new CapitalizeResp(), (e) => {
                Console.WriteLine(e.Output);
            });
        }
    }

    public static void Main()
    {
        Hub.Instance
            .Attach(new SingleThreadFlow()
                .Add(new HelloCase())
                .Add(new CapitalizerCase())
                .Add(new LocalCase()));

        using (new Hub.Flows().Startup())
        {
            while (true)
            {
                var input = Console.ReadLine();
                if (input == "bye")
                {
                    break;
                }
                new HelloReq { Name = input }.Post();
            }
        }
    }
}

3-Tier Distribution: Client/FrontendServer/BackendServer

In an x2 application, adding or removing a distribution layer is not a big deal. All you need to do is setting up the required links to properly send/receive events.

Here is our backend server which runs the CapitalizerCase as its main logic case:

C#
public class HelloTcpBackend : AsyncTcpServer
{
    public HelloTcpBackend() : base("HelloBackend")
    {
    }

    protected override void Setup()
    {
        EventFactory.Register<CapitalizeReq>();
        Bind(new CapitalizeResp(), Send);
        Listen(7890);
    }

    public static void Main()
    {
        Hub.Instance
            .Attach(new SingleThreadFlow()
                .Add(new CapitalizerCase())
                .Add(new HelloTcpBackend()));

        using (new Hub.Flows().Startup())
        {
            while (true)
            {
                var input = Console.ReadLine();
                if (input == "bye")
                {
                    break;
                }
            }
        }
    }
}

We also build a frontend server that runs the HelloCase as its main logic case and delegate the capitalization task to the backend server:

C#
class BackendClient : AsyncTcpClient
{
    public BackendClient() : base("BackendClient") {}

    protected override void Setup()
    {
        EventFactory.Register<CapitalizeResp>();
        Bind(new CapitalizeReq(), Send);
        Connect("127.0.0.1", 7890);
    }
}

public class HelloTcpFrontend : AsyncTcpServer
{
    public HelloTcpFrontend() : base("HelloFrontend")
    {
    }

    protected override void Setup()
    {
        EventFactory.Register<HelloReq>();
        Bind(new HelloResp(), OnHelloResp);
        Listen(6789);
    }

    public static void Main()
    {
        Hub.Instance
            .Attach(new SingleThreadFlow()
                .Add(new HelloCase())
                .Add(new BackendClient())
                .Add(new HelloTcpFrontend()));

        using (new Hub.Flows().Startup())
        {
            while (true)
            {
                var input = Console.ReadLine();
                if (input == "bye")
                {
                    break;
                }
            }
        }
    }

    IEnumerator OnHelloResp(Coroutine coroutine, HelloResp e)
    {
        // Backup the _Handle builtin property of the original response.
        int handle = e._Handle;

        // Post a CapitalizeReq event in chain
        // and wait for the corresponding response.
        yield return coroutine.WaitForSingleResponse(
            new CapitalizeReq { Input = e.Greeting },
            new CapitalizeResp());

        var result = coroutine.Result as CapitalizeResp;
        if (result == null)
        {
            // Timeout
            yield break;
        }

        // Now we got the CapitalizeResp event.
        // Set the _Handle property to match the original response.
        result._Handle = handle;
        // And send the resultant event to the client,
        // according to the _Handle builtin property.
        Send(result);
    }
}

In the previous client/server distribution, we relied on the built-in event property _Handle to dispatch the response event to the appropriate session. But in this topology, we cannot do the same. If it was an authentication-based real-world application, we might bind events by authenticated user identifiers. However, in order to handle the case in this simple example, we bring up a special x2net coroutine handler as shown above.

Then we can use a similar client to connect to the frontend server to get things done:

C#
public class HelloTcpClient : TcpClient
{
    class LocalCase : Case
    {
        protected override void Setup()
        {
            // To print the content of CapitalizeResp to the console output.
            Bind(new CapitalizeResp(), (e) => {
                Console.WriteLine(e.Output);
            });
        }
    }

    public HelloTcpClient() : base("HelloClient")
    {
    }

    protected override void Setup()
    {
        EventFactory.Register<CapitalizeResp>();
        Bind(new HelloReq(), Send);
        Connect("127.0.0.1", 6789);
    }

    public static void Main()
    {
        Hub.Instance
            .Attach(new SingleThreadFlow()
                .Add(new LocalCase())
                .Add(new HelloTcpClient()));

        using (new Hub.Flows().Startup())
        {
            while (true)
            {
                var input = Console.ReadLine();
                if (input == "bye")
                {
                    break;
                }
                new HelloReq { Name = input }.Post();
            }
        }
    }
}

Points of Interest

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Korea (Republic of) Korea (Republic of)
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionthank you! Pin
Member 112791658-Apr-18 13:10
professionalMember 112791658-Apr-18 13:10 
AnswerRe: thank you! Pin
Jay Kang8-Apr-18 15:54
Jay Kang8-Apr-18 15:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.