Pure Interface Based Approach - GenuineChannels/GenuineChannels GitHub Wiki

Interface-based approach has several significant benefits in comparison with soapsuds and late-bound hook approaches.

By Dmitry Belikov

When several people want to speak to each other, they should find the common language. Otherwise they will probably never reach an understanding. In exactly the same way you should design a language that will be used by your .NET Remoting applications. You should design the rules, this is the main rule!

Why I advise to use either interfaces or abstract classes. Generally you should consider the following problems:

  1. Encapsulation. Your client should know minimum information about the server. And vice versa.
  2. Inheritance. You should always be able to extend your client-server solution with minimum efforts.
  3. Polymorphism. You should have a minimum set of agreements that potentially describes an unlimited number of relations. The fewer rules there are, the better design you have. Simplicity at this level always causes perfection.
  4. Versioning. It is a good idea to support both old and new clients.

Reading “Client-Server Solution based on Genuine Channels Technology Description” provided with the commercial version of Genuine Channels, you will get my understanding of this area. In this article I will try to show how you can easily build a pure interface-based Well-Known Layer and use it from both the server and the client.

Well-known layer

In my example (open the Server\Server.sln solution file) the well-known layer contains only interfaces. I would recommend to have only abstract classes, delegates and entity classes there.

This snippet shows the entire well-known layer for my simple chat sample. I declared only interfaces.

using System;
using System.Runtime.Remoting.Messaging;

namespace KnownObjects
{
   /// <summary>
   /// Describes a callback called when a message is received.
   /// </summary>
   public interface IMessageReceiver
   {
      /// <summary>
      /// Is called by the server when a message is accepted.
      /// </summary>
      /// <param name="message">A message.</param>
      /// <param name="nickname">Nickname of the client who sent the message.</param>
      object ReceiveMessage(string message, string nickname);
   }

   /// <summary>
   /// Server chat room factory.
   /// </summary>
   public interface IChatServer
   {
      /// <summary>
      /// Logs into the chat room.
      /// </summary>
      /// <param name="iMessageReceiver">Chat message receiver.</param>
      /// <param name="nickname">Nickname.</param>
      /// <returns>Chat room interface.</returns>
      IChatRoom EnterToChatRoom(IMessageReceiver iMessageReceiver,
                                string nickname);
   }

   /// <summary>
   /// ChatRoom provides methods for the chatting.
   /// </summary>
   public interface IChatRoom
   {
      /// <summary>
      /// Sends the message to all clients.
      /// </summary>
      /// <param name="message">Message being sent.</param>
      void SendMessage(string message);
   }
}

IChatServer is the core of my server. It is a factory class that returns IChatRoom instances when a client wants to participate in a chat. IMessageReceiver is a client-side interface to receive messages from the server. It is a callback that will be called each time anyone sends a message.

Client

The more interesting part is our client. I did not design it well and the server address is hard-coded, but it is OK for this sample:

namespace Client
{
   /// <summary>
   /// ChatClient demonstrates a simple client application.
   /// </summary>
   class ChatClient : MarshalByRefObject, IMessageReceiver
   {
      /// <summary>
      /// The only instance.
      /// </summary>
      public static ChatClient Instance = new ChatClient();   

      /// <summary>
      /// Nickname.
      /// </summary>
      public static string Nickname;   

      /// <summary>
      /// Chat room.
      /// </summary>
      public static IChatRoom IChatRoom;   

      /// <summary>
      /// The main entry point for the application.
      /// </summary>
      [STAThread]
      static void Main(string[] args)
      { // . . .

         // fetch chat room business object 
         // that implements IChatRoom interface
         IChatServer iChatServer = (IChatServer) Activator.GetObject
             (typeof(IChatRoom),
             "gtcp://127.0.0.1:8737/ChatServer.rem");
         ChatClient.IChatRoom = iChatServer.EnterToChatRoom
            (ChatClient.Instance, ChatClient.Nickname);
         // . . .

         // and use it when we need to send a message
         ChatClient.IChatRoom.SendMessage(str);
         // . . .
      }


      // This method is called when someone sends a message.
      public object ReceiveMessage(string message, string nickname)
      {
         Console.WriteLine("Message \"{0}\" from \"{1}\".", message,
                                                         nickname );
         return null;
      } 
   }
}

The client retrieves the server chat business object, then requests the chat room business object and sends messages via it. Client receives messages from the server via the provided MBR object supporting the IMessageReceiver instance.

Server

The most interesting part.

namespace Server
{
   /// <summary>
   /// Chat server implements server that configures Genuine 
   /// Server TCP Channel and implements
   /// chat server behavior.
   /// </summary>
   class ChatServer : MarshalByRefObject, IChatServer
   {
      /// <summary>
      /// The main entry point for the application.
      /// </summary>
      [STAThread]
      static void Main(string[] args)
      {
         // . . .
         // Setup business object
         // bind the server
         RemotingServices.Marshal(new ChatServer(), "ChatServer.rem");
       }   

       /// <summary>
       /// This example was designed to have the only chat room.
       /// </summary>
       public static ChatRoom GlobalRoom = new ChatRoom();

       // IChatServer is a factory that returns IChatRoom 
       // by client request
       public IChatRoom EnterToChatRoom(IMessageReceiver
                iMessageReceiver, string nickname)
       {
          GlobalRoom.AttachClient(iMessageReceiver, nickname);
          ChatServer.CurrentSession["Nickname"] = nickname;
          return GlobalRoom;
       }
   }
}

I did not show the chat room implementation to avoid unnecessary details.

The approach

I would formulate the basic premises of pure interface-based approach as

  1. Your well-known layer contains declaration of interfaces, abstract classes and entity classes.
  2. You should declare one or several factory interface(s) that will spawn (or just return) business objects for different affairs.
  3. The server defines an MBR-derived object supporting the factory interface and makes it available via RemotingServices.Marshal:
RemotingServices.Marshal(new ChatServer(), "ChatServer.rem");
  1. The client knows exactly what kind of business object it needs. The client can download the latest version of the known layer dll at run-time. So the client just requests a factory object and then provides all the necessary information to obtain a business object:
IChatServer iChatServer = (IChatServer)
        Activator.GetObject(typeof(IChatRoom),
        "gtcp://127.0.0.1:8737/ChatServer.rem");

ChatClient.IChatRoom = iChatServer.EnterToChatRoom
        (ChatClient.Instance, ChatClient.Nickname);

If you think that you get the idea, now you should be able to implement the same chat application without receiving a client’s MBR object implementing the IMessageReceiver interface for the callback. Try to elaborate this. Genuine Channels implements bidirectional link. And you are able to get the Genuine Channels URI of the remote host and use it to build the path to the remote MBR without requesting it directly.

Further development

The ready-made sample is here.

Looking through the well-known layer, you can notice that the ChatServer.EnterToChatRoom method does not require a client's receiver anymore.

/// <summary>
/// Message received.
/// </summary>
public interface IMessageReceiver
{
   /// <summary>
   /// Is called by the server when a message is accepted.
   /// </summary>
   /// <param name="message">A message.</param>
   /// <param name="nickname">Nickname of the client who sent the message.</param>
   object ReceiveMessage(string message, string nickname);
}

/// <summary>
/// Server chat room factory.
/// </summary>
public interface IChatServer
{
   /// <summary>
   /// Performs log in to the chat room.
   /// </summary>
   /// <param name="nickname">Nickname.</param>
   /// <returns>Chat room interface.</returns>
   IChatRoom EnterToChatRoom(string nickname);
}

The client marshals the global receiver to a well-known URI:

namespace Client
{
   /// <summary>
   /// ChatClient demostrates simple client application.
   /// </summary>
   class ChatClient : MarshalByRefObject, IMessageReceiver
   {
      // . . .
      static void Main(string[] args)
      {
         // . . .
         //************************
         // bind client's receiver
         RemotingServices.Marshal(ChatClient.Instance,
                     "MessageReceiver.rem");

The server can fetch a client’s receiver because it knows its name:

namespace Server
{
   /// <summary>
   /// Represents a chat room.
   /// </summary>
   public class ChatRoom : MarshalByRefObject, IChatRoom
   {
      // . . .
      /// <summary>
      /// Attaches the client.
      /// </summary>
      /// <param name="nickname">Nickname.</param>
      public void AttachClient(string nickname)
      {
         string receiverUri = GenuineUtility.FetchCurrentRemoteUri() +
           "/MessageReceiver.rem";
         IMessageReceiver iMessageReceiver = (IMessageReceiver)
           Activator.GetObject(typeof(IMessageReceiver), receiverUri);
         this._dispatcher.Add((MarshalByRefObject) iMessageReceiver);
         GenuineUtility.CurrentSession["Nickname"] = nickname;
      }

The server uses Client Session for keeping the client name. Client Session is available during events, therefore the server can get the client name even if the client has disconnected. Please note how easy it is to create a local transparent proxy and attach it to the Broadcast Engine dispatcher.

Summary

  1. Encapsulation.
    The interface approach completely solves this problem and allows you to change the implementation almost without affecting the other side. At least until you do not change the well-known layer and call conditions.
  2. Inheritance.
    Almost boundless freedom as soon as you will be able to upload the latest version of the well-known layer automatically.
  3. Polymorphism.
    The pure interface-based approach allows you to have minimum and limited set of agreements that potentially describes an unlimited number of relations.
  4. Versioning.
    Extend your interfaces inheriting them from the previous one. Or declare a new interface and implement both of them (previous and new) in one class.
⚠️ **GitHub.com Fallback** ⚠️