I need an alternative similar to System.Runtime.Remoting that is supported in .NET 5.0

2.6k Views Asked by At

I have a current software product written in C# WinForms that I'm migrating to WPF .NET 5.0. I've almost got it all migrated, but one of the last projects to migrate is the remoting support for the app. This is intended for remote control from a client in an intranet, NOT Internet.

The current version of the product uses .NET Remoting, however, I sadly have found that this is not supported in .NET 5.0.

I've seen something about gRPC, but can't find anything to show me how to implement or whether there might be something more similar to .NET Remoting to use so that there are minimal changes required.

Thanks in advance for your help!

1

There are 1 best solutions below

2
Jeff H On

@Fildor, this isn't really an answer, perse, just posting what I ended up doing for a simplistic approach, rather than leveraging a remoting framework. My remoting implementation always runs on a private network or over a corporate VPN, so security isn't an issue. Thus, I can take a simplistic approach.

What I finally ended up implementing is a simple TCP/IP Socket implementation using TcpClient class and TcpListener that passes Json strings to represent a command and the response objects defined as classes. This class includes server API methods, each of which map to the defined API.

I created a class that inherits from TcpClient (RemoteClientSocket) for the client and another that inherits from TcpListener (RemoteListener) on the server.This class includes client API methods, each of which map to defined API.

I defined an enum, CommandType , that includes types for each remote command.

The RemoteClientSocket class includes method, SendCommand() that is invoked by each of the client API methods after instantiating a RemoteClientCommand object, this method takes a RemoteClientCommand argument.

The RemoteListener class includes methods for each of the remote commands defined in the CommandType enum.

I then created RemoteClientCommand class that is serialized into a Json string and sent from the client and deserialized by the RemoteListener object, which maps to the server command. I then created a RemoteClientResponse class that is serialized to a Json string, which is returned to the client. Each class includes a Serialize() and DeSerialize() method to serialize/deserialze to/from a Json string that is actually sent over the TCP/IP connection.

Lastly, add methods to the RemoteClientSocket class for each of the commands that populates the CommandArgs object array to send to the server and add methods to the RemoteListener class that that executes the server method corresponding to the CommandType contained in the RemoteClientCommand object.

After the RemoteListener executes the command, it instantiates a new RemoteCommandResponse object and populates the CommandReturn object and the CommandOutParams object array, calls Serialize() on the RemoteCommandResponse object and sends that Json string in response over the TCP/IP connection.

When the RemoteClientSocket receives the response, it calls DeSerialize() on the Json response string and unpackages the CommandReturn object, if any, and the CommandOutParams, if any, before returning to from the client method.

Here is the code for the RemoteListener:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using RemoteClient;

namespace RemoteServer
{
    public class RemoteListener : TcpListener, IWinCalRemotingService
    {
        private static readonly IPAddress _ipAddress = IPAddress.Parse("127.0.0.1"); // Local host

        // Keep track of the threads and create CancellationTokenSource for each
        private Dictionary<Thread, CancellationTokenSource> m_ThreadDictionary = new Dictionary<Thread, CancellationTokenSource>();

        public RemoteListener()
            : base(_ipAddress, Constants.DEFAULT_REMOTING_PORT)
        {
            RegisterCommands();
        }

        /// <summary>
        /// Start the server
        /// </summary>
        public void StartServer()
        {
            Start();

            // Create a thread for the server to listen on
            Thread t = new Thread(new ThreadStart(StartListener));
            t.Start();
        }

        /// <summary>
        /// Stop the server
        /// </summary>
        public void StopServer()
        {
            foreach (KeyValuePair<Thread, CancellationTokenSource> pair in m_ThreadDictionary)
            {
                // Cancel all of the client threads
                pair.Value.Cancel();
            }

            Stop();
        }

        public void StartListener()
        {
            try
            {
                while (true)
                {
                    Debug.WriteLine("Waiting for a Remoting Connection...");
                    TcpClient client = AcceptTcpClient();
                    Debug.WriteLine($"Remoting Client Connected from IP Address:{client.Client.RemoteEndPoint}");
                    Thread t = new Thread(new ParameterizedThreadStart(HandleClient));

                    // Add a mapping
                    m_ThreadDictionary.Add(t, new CancellationTokenSource());
                    t.Start(client);
                }
            }
            catch (SocketException e)
            {
                Debug.WriteLine("SocketException: {0}", e);
            }
        }

        public void HandleClient(Object obj)
        {
            TcpClient client = (TcpClient)obj;

            CancellationTokenSource cancelToken = m_ThreadDictionary[Thread.CurrentThread];
            var stream = client.GetStream();
            string imei = String.Empty;
            string remoteCommand = null;
            Byte[] bytes = new Byte[512];
            int i;
            try
            {
                while (!cancelToken.IsCancellationRequested && (i = stream.Read(bytes, 0, bytes.Length)) != 0)
                {
                    string hex = BitConverter.ToString(bytes);
                    remoteCommand = Encoding.ASCII.GetString(bytes, 0, i);
                    Debug.WriteLine("{1}: Received: {0}", remoteCommand, Thread.CurrentThread.ManagedThreadId);


                    string response = ProcessCommand(remoteCommand);
                    Byte[] reply = System.Text.Encoding.ASCII.GetBytes(response);
                    stream.Write(reply, 0, reply.Length);
                    Debug.WriteLine("{1}: Sent: {0}", response, Thread.CurrentThread.ManagedThreadId);
                }

                m_ThreadDictionary[Thread.CurrentThread].Dispose();
            }
            catch (Exception e)
            {
                SystemEventsMgr.AddException("Remoting Server Client Thread Exception", e);
            }
            finally
            {
                // Remove this thread from the map
                m_ThreadDictionary.Remove(Thread.CurrentThread);
                client.Close();
            }
        }

        private string ProcessCommand(string jsonCommand)
        {
            RemoteClientCommand cmd = RemoteClientCommand.DeSerialize(jsonCommand);
            RemoteCommandResponse response = null;

            switch (cmd.CommandType)
            {
                case ClientCommand.GetAutoInfo:
                    GetAutoInfo(cmd);
                    break;
            }

            return response.Serialize();
        }

    }

    private RemoteCommandResponse GetAutoInfo(RemoteClientCommand cmd)
    {
        int autoID = cmd.CommandArgs[0];
        string manufacturer = "";
        string model = "";
        int year = 0;
        string retString = GetAutoInfo(autoID, out string manufacturer, out string model, out int year);
        object[] outParams = new object[]{manufacturer, model, year};
        
        RemoteCommandResponse resp = new RemoteCommandResponse(CommandType.GetAutoInfo, retString, outParams);
    return resp;
    }

    private string GetAutoInfo(int autoID, out string autoManufacturer, out string autoModel, out int autoYear)
    {
        autoManufacturer = _autos[autoID].Manufacturer;
        autoModel = _autos[autoID].Model;
        autoYear = _autos[autoID].Year;

        return "Success";
    }
}

Here is the code for the RemoteClientSocket:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace RemoteClient
{
    /// <summary>
    /// IMPORTANT: When adding new commands, do the following:
    /// 1. Add to the command types
    /// 2. Add to the RemoteListener ProcessCommands switch statement
    /// </summary>
    public enum ClientCommand
    {
        GetAutoInfo,
    }

    class RemoteClientSocket : TcpClient, IAutoInfoInterface
    {
        /// <summary>
        /// Instantiate and connect the client
        /// </summary>
        /// <param name="ipAddress"></param>
        /// <param name="port"></param>
        public RemoteClientSocket(string server, int port)
            : base(server, port)
        {
        }

        /// <summary>
        /// Send a command of input type and receive a response as output type
        /// </summary>
        /// <param name="cmdObject">The Command Object</param>
        /// <param name="inputType">The Type for the Command Object</param>
        /// <param name="outputType">the Type for the return object</param>
        /// <returns></returns>
        public RemoteCommandResponse SendCommand(RemoteClientCommand cmd)
        {
            RemoteCommandResponse cmdResp = null;
            NetworkStream stream = GetStream();

            Byte[] data = Encoding.ASCII.GetBytes(cmd.Serialize());
            stream.Write(data, 0, data.Length);

            data = new Byte[256];
            string response = string.Empty;
            Int32 bytes = stream.Read(data, 0, data.Length);
            response = System.Text.Encoding.ASCII.GetString(data, 0, bytes);

            // Deserialize the response
            cmdResp = RemoteCommandResponse.DeSerialize(response);

            stream.Close();

            return cmdResp;
        }

        virtual public string GetAutoInfo(int autoID, out string autoManufacturer, out string autoModel, out int autoYear)
        {
            string err = "None";
            RemoteClientCommand cmd = new RemoteClientCommand(CommandType.GetAutoInfo, new object[] { autoID });

            RemoteCommandResponse resp = SendCommand(cmd);

            // Unpackage the return type and output arguments
            err = resp.CommandReturn as string;
            autoManufacturer = resp.CommandOutParams[0] as string;
            autoModel = resp.CommandOutParams[1] as string;
            autoYear = (int)resp.CommandOutParams[2];

            return err;
        }
    }

    /// <summary>
    /// The WinCal Remoting Client Command Package Containing Json Strings for Objects
    /// </summary>
    public class RemoteClientCommand
    {
        /// <summary>
        /// Command Type - one for each remote command
        /// </summary>
        public WinCalClientCommand CommandType { get; private set; }

        /// <summary>
        /// Command argument object(s)
        /// </summary>
        public object[] CommandArgs { get; private set; }

        public RemoteClientCommand(WinCalClientCommand cmdType, object[] cmdArgs)
        {
            CommandType = cmdType;
            CommandArgs = cmdArgs;
        }

        /// <summary>
        /// Serialize the class to Json string for the command
        /// </summary>
        /// <returns></returns>
        public string Serialize()
        {
            return JsonSerializer.Serialize(this);
        }

        /// <summary>
        /// Deserialize the command object
        /// </summary>
        /// <param name="jsonString"></param>
        /// <returns></returns>
        public static RemoteClientCommand DeSerialize(string jsonString)
        {
            return JsonSerializer.Deserialize(jsonString, typeof(RemoteCommandResponse)) as RemoteClientCommand;
        }
    }

    /// <summary>
    /// The object serialized and returned by the RemoteListener
    /// </summary>
    public class RemoteCommandResponse
    {
        /// <summary>
        /// The Command Type - one for each remote command
        /// </summary>
        public WinCalClientCommand CommandType { get; private set; }

        /// <summary>
        /// The single command return object
        /// </summary>
        public object CommandReturn { get; private set; }

        /// <summary>
        /// The Json strings for all out params - may be empty
        /// </summary>
        public object[] CommandOutParams { get; private set; }

        public RemoteCommandResponse(WinCalClientCommand cmdType, object retObj, object[] outParamObjs)
        {
            CommandType = cmdType;
            CommandReturn = retObj;
            CommandOutParams = outParamObjs;
        }

        /// <summary>
        /// Serialize the class to a Json string
        /// </summary>
        /// <returns></returns>
        public string Serialize()
        {
            return JsonSerializer.Serialize(this);
        }

        /// <summary>
        /// Deserialize the response object
        /// </summary>
        /// <param name="jsonString"></param>
        /// <returns></returns>
        public static RemoteCommandResponse DeSerialize(string jsonString)
        {
            return JsonSerializer.Deserialize(jsonString,typeof(RemoteCommandResponse)) as RemoteCommandResponse;
        }
    }
}