Migrating .NET Remoting to WCF (and even ASMX!)

This is a long post so I’ll summarize my findings up front.

Summary:

  1. Following the .NET Remoting guidance enables quick and painless upgrade to WCF
  2. A single interface can be decorated to support .NET Remoting, WCF and ASMX
  3. The service implementation does not need to change at all to support all three platforms
  4. Wrapping the client connection code in a proxy allows developers to switch between .NET Remoting, WCF and ASMX on-the-fly

Read on for the details…

Details:

So you’ve read all the guidance about how and when to use .NET Remoting and now want to see a working code sample of this in action?

Building the .NET Remoting solution

I have broken this solution into three projects:

  1. SharedContract – this contains the interface shared by client and service

  2. Service – this contains the service implementation of the interface in the SharedContract project and self-hosting code

  3. Client – this contains the client programming against the interface in the SharedContract project and the client proxy

NOTE: I repeat code blocks throughout this post and will highlight changes made in red.

Let’s start with an interface:

namespace SharedContract

{

    public interface IAmTheSharedContract

    {

        string Echo(string input);

    }

}

Using a shared interface is inline with the .NET Remoting guidance. An interface based approach reduces the code shared between client and service and, as you will see, allows for re-use for other distributed applications stacks such as Windows Communication Foundation (WCF) and ASMX. This is interface above is what every client will program against.

Next we need to implement it:

namespace Service

{

    class ServiceImplementation : MarshalByRefObject, IAmTheSharedContract

    {

        public string Echo(string input)

        {

            Console.WriteLine("Received input: " + input);

            return input;

        }

    }

}

Next we need to host the service implementation. Every distributed application stack has different hosting options and techniques. Isolating this code from the implementation is critical to ensure easy migration and portability.

Here is the code needed to host it for .NET Remoting:

namespace Service

{

    class Program

    {

        static void Main(string[] args)

        {

            RemotingConfiguration.Configure("Service.exe.config", true);

            Console.WriteLine("\nThe service is ready.");

            Console.WriteLine("Press <ENTER> to terminate service.\n");

            Console.ReadLine();

        }

    }

}

The configuration for hosting this .NET Remoting service is as follows:

  <!-- Remoting hosting section -->

  <system.runtime.remoting>

    <application>

      <service>

        <wellknown mode="SingleCall" type="Service.ServiceImplementation, Service" objectUri="server.rem" />

      </service>

      <channels>

        <channel ref="tcp" secure="true" port="8080" />

      </channels>

    </application>

  </system.runtime.remoting>

You will notice that I am using the new secure TcpChannel provided in .NET Remoting 2.0. This new secure channel provides identification, encryption and signing simply by adding the secure=”true” attribute.

You now have a complete .NET Remoting service.

And now we need a client:

namespace Client

{

    class Program

    {

        static void Main(string[] args)

        {

            IAmTheSharedContract proxy = ClientProxyFactory.CreateProxyInstance();

            Console.WriteLine("Calling the " + ClientProxyFactory.Platform + " endpoint:");

            Console.WriteLine("\tResult: " + proxy.Echo("Testing " + ClientProxyFactory.Platform + " endpoint"));

        }

    }

}

You will notice that the client does create the proxy directly. As per our guidance we are using a client proxy class to astract the .NET Remotingisms from the client code. The proxy class looks like this:

namespace Client

{

    class ClientProxyFactory

    {

        static ClientProxyFactory()

        {

            RemotingConfiguration.Configure("Client.exe.config", true);

            defaultPlatform = (ClientProxyFactory.CommunicationPlatform)Enum.Parse(

                                typeof(ClientProxyFactory.CommunicationPlatform),

                                ConfigurationManager.AppSettings["communicationPlatform"]);

        }

        static CommunicationPlatform defaultPlatform;

        static ChannelFactory<IAmTheSharedContract> factory = null;

        public enum CommunicationPlatform

        {

            Remoting

        }

        public static CommunicationPlatform Platform

        {

            get

            {

                return defaultPlatform;

            }

        

        }

        public static IAmTheSharedContract CreateProxyInstance()

        {

            return CreateProxyInstance(defaultPlatform);

        }

        public static IAmTheSharedContract CreateProxyInstance(CommunicationPlatform platform)

        {

            IAmTheSharedContract proxy = null;

            switch (platform)

            {

                case CommunicationPlatform.Remoting:

                    proxy = (IAmTheSharedContract)Activator.GetObject(typeof(IAmTheSharedContract), "tcp://localhost:8080/server.rem") as IAmTheSharedContract;

                    break;

            }

            return proxy;

        }

    }

}

The ClientProxyFactory is used to create a proxy dependant on the distributed application platform. The critical parts of this class are the constructor, where the chosen platform is set, and the CreateProxyInstace(), where the service proxy is create. The distributed application platform to use is picked up from config and maps to the CommunicationPlatform enumeration. The various connection URIs could be in config as well.

The configuration for this client looks like this:

  <!-- client proxy config section -->

  <appSettings>

    <!-- use appSetting to configure base address provided by host -->

    <add key="communicationPlatform" value="Remoting" />

  </appSettings>

  <!-- Remoting configuration -->

  <system.runtime.remoting>

    <application>

      <channels>

        <channel ref="tcp" secure="true" tokenImpersonationLevel="identification" />

      </channels>

    </application>

  </system.runtime.remoting>

You will notice that I configure a channel for the client-side as well. This is required for the secure TcpChannel to ensure that the client channel is enabled for security as well.

And now you have a complete .NET Remoting application using the our .NET Remoting programming guidance.

The next step is to enable the solution for WCF.

Enabling the solution for WCF

Now we’ll convert this application to use Windows Communication Foundation (WCF) (Indigo).

First we need to modify the contract:

namespace SharedContract

{

    [ServiceContract]

    public interface IAmTheSharedContract

    {

        [OperationContract]

        string Echo(string input);

    }

}

All we need to do is decorate the interface with the ServiceContract attribute and the methods to expose with the OperationContract attribute. This will be automatically picked up by WCF to enable exposing this interface as a WCF endpoint.

What do we need to do to the implementation class? NOTHING.

To support a WCF endpoint we need to add some hosting code:

namespace Service

{

    class Program

    {

        static void Main(string[] args)

        {

            RemotingConfiguration.Configure("Service.exe.config", true);

            // Get base address from app settings in configuration

            Uri baseAddress = new Uri(ConfigurationManager.AppSettings["baseAddress"]);

            ServiceHost serviceHost = new ServiceHost(typeof(Service.ServiceImplementation), baseAddress);

            // Open the ServiceHostBase to create listeners and start listening for messages.

            serviceHost.Open();

            Console.WriteLine("\nThe services are ready.");

            Console.WriteLine("Press <ENTER> to terminate service.\n");

            Console.ReadLine();

        }

    }

}

This hosting code gets the service address from config and then uses ServiceHost to configure the WCF endpoints. The app.config file contains the configuration for WCF hosting.

We need to add some code to configuration to enable the WCF endpoint:

  <!-- WCF hosting section -->

  <appSettings>

    <!-- use appSetting to configure base address provided by host -->

    <add key="baseAddress" value="net.tcp://localhost:8088/wcfservice" />

  </appSettings>

  <system.serviceModel>

    <services>

      <service

          type="Service.ServiceImplementation, Service">

        <!-- use base address provided by host -->

        <endpoint address=""

                  binding="netTcpBinding"

                  bindingConfiguration="Binding1"

                  contract="SharedContract.IAmTheSharedContract,SharedContract" />

      </service>

    </services>

    <bindings>

      <netTcpBinding>

        <binding name="Binding1" />

      </netTcpBinding>

    </bindings>

  </system.serviceModel>

This configuration establishes a TCP endpoint using the WCF NetTcpBinding.

Now the service is hosting both .NET Remoting and WCF!

On to the client…

The code in the client Main() from before does NOT change!  

The programming against the shared interface remains the same but we need to update the ClientProxyFactory to enable it to connect to WCF endpoints:

namespace Client

{

    class ClientProxyFactory

    {

        static ClientProxyFactory()

        {

            RemotingConfiguration.Configure("Client.exe.config", true);

            defaultPlatform = (ClientProxyFactory.CommunicationPlatform)Enum.Parse(

                                typeof(ClientProxyFactory.CommunicationPlatform),

                                ConfigurationManager.AppSettings["communicationPlatform"]);

        }

        static CommunicationPlatform defaultPlatform;

        static ChannelFactory<IAmTheSharedContract> factory = null;

        public enum CommunicationPlatform

        {

            Remoting,

            WCF

        }

        public static CommunicationPlatform Platform

        {

            get

            {

                return defaultPlatform;

            }

       

        }

        public static IAmTheSharedContract CreateProxyInstance()

        {

            return CreateProxyInstance(defaultPlatform);

        }

        public static IAmTheSharedContract CreateProxyInstance(CommunicationPlatform platform)

        {

            IAmTheSharedContract proxy = null;

            switch (platform)

            {

                case CommunicationPlatform.Remoting:

                    proxy = (IAmTheSharedContract)Activator.GetObject(typeof(IAmTheSharedContract), "tcp://localhost:8080/server.rem") as IAmTheSharedContract;

                    break;

                case CommunicationPlatform.WCF:

                    if(factory == null)

                        factory = new ChannelFactory<IAmTheSharedContract>("");

                    proxy = factory.CreateChannel();

                    break;

            }

            return proxy;

        }

    }

}

As you can see we simply added WCF to the CommunicationPlatform enumeration and then added code to create a proxy to the WCF endpoint using the ChannelFactory<T>.CreateChannel() API.

This requires additional client configuration for the WCF endpoint:

    <add key="communicationPlatform" value="WCF" />

  <!-- WCF hosting configuration -->

  <system.serviceModel>

    <client>

      <endpoint name=""

            address="net.tcp://localhost:8088/wcfservice"

            binding="netTcpBinding"

            bindingConfiguration="Binding1"

                contract="SharedContract.IAmTheSharedContract, SharedContract">

      </endpoint>

    </client>

    <bindings>

      <netTcpBinding>

        <binding name="Binding1" />

      </netTcpBinding>

    </bindings>

  </system.serviceModel>

We changed the CommunicationPlatform value to now use WCF and then added the configuration to point to the WCF endpoint.

Now you have a client that talks both to WCF and .NET Remoting!

To change which endpoint you are talking to requires only that you change the CommunicationPlatform valule in the Client.exe.config file.

Enabling the solution for ASMX

Now you may be wondering if this conversion is possible to ASMX Web Services. Yes, it is!

Here are the steps to convert to support ASMX as well.

First update the shared interface:

namespace SharedContract

{

    [ServiceContract]

    [WebServiceBinding(ConformsTo=WsiProfiles.BasicProfile1_1,EmitConformanceClaims=true)]

    public interface IAmTheSharedContract

    {

        [OperationContract]

        [WebMethod]

        string Echo(string input);

    }

}

As you can see, ASMX is enabled simply by further interface and method decoration to enable ASMX as well. The WebServiceBinding attribute is new in .NET Framework 2.0 and allows ASMX to support declaration of your web service methods through an interface.

What changes in the service implementation? NOTHING! That is pretty cool.  

ASMX in .NET Framework 2.0 will automatically pick up the fact that the service implementation implements and interface marked with the WebServiceBinding attribute and will generate a WSDL based on the interface methods marked with WebMethod.

If you want to change the namespace of the service then you can decorate the implementation class with a WebService attribute and control the namespace as well. This will NOT interfere with hosting this class in .NET Remoting or WCF.

Next we look at the hosting problem. When I built this sample I took the simple route:

  1. I changed the Service project build properties to output to bin\ instead of bin\Debug to support the IIS model of <app>\bin

  2. Then I created a VDir that pointed to the folder where the service code lives.

  3. Then I added an WebService.asmx file to the service project. The code is below:

<%@ WebService language="C#" class="Service.ServiceImplementation,Service" %>

Because of the changes in .NET Framework 2.0 the ASMX file will be compiled on the fly so no building is required. If you go to https://localhost/<VDir>/WebService.asmx you will see that the ASMX web service is now hosted. Go ?wsdl to make sure that the WSDL is also generated.

You are now hosting this same interface and implementation in ASMX as well.

Next we move on the client. On the client-side we need to generate and ASMX client proxy. Use wsdl.exe /out:asmxProxy.cs https://localhost/<VDir>/WebService.asmx?wsdl to generate the client proxy class.

The generated proxy needs to be changed in the following ways:

  1. Change the name (and the ctor) of the proxy class to a different name (e.g., AsmxClientProxy) to avoid conflicts with the shared interface (SharedContract.IAmTheSharedContract)

  2. Change the client proxy to implement SharedContract.IAmTheSharedContract by changing the class declaration to indicate that the proxy class implements the interface

The code in the client Main() from before does NOT change!  

And the ClientProxyFactory needs to be changed as well:

namespace Client

{

    class ClientProxyFactory

    {

        static ClientProxyFactory()

        {

            RemotingConfiguration.Configure("Client.exe.config", true);

            defaultPlatform = (ClientProxyFactory.CommunicationPlatform)Enum.Parse(

                                typeof(ClientProxyFactory.CommunicationPlatform),

                                ConfigurationManager.AppSettings["communicationPlatform"]);

        }

        static CommunicationPlatform defaultPlatform;

        static ChannelFactory<IAmTheSharedContract> factory = null;

        public enum CommunicationPlatform

        {

            Remoting,

            WCF,

            ASMX

        }

        public static CommunicationPlatform Platform

        {

            get

            {

                return defaultPlatform;

            }

       

        }

        public static IAmTheSharedContract CreateProxyInstance()

        {

            return CreateProxyInstance(defaultPlatform);

        }

        public static IAmTheSharedContract CreateProxyInstance(CommunicationPlatform platform)

        {

            IAmTheSharedContract proxy = null;

            switch (platform)

            {

                case CommunicationPlatform.Remoting:

                    proxy = (IAmTheSharedContract)Activator.GetObject(typeof(IAmTheSharedContract), "tcp://localhost:8080/server.rem") as IAmTheSharedContract;

                    break;

                case CommunicationPlatform.WCF:

                    if(factory == null)

                        factory = new ChannelFactory<IAmTheSharedContract>("");

                    proxy = factory.CreateChannel();

                    break;

                case CommunicationPlatform.ASMX:

                    proxy = new AsmxClientProxy() as IAmTheSharedContract;

                    break;

            }

            return proxy;

        }

    }

}

As you can see we simply added ASMX to the CommunicationPlatform enumeration and then added code to create an instance of the generated ASMX proxy and return this proxy as the shared interface.

Now you can change the Client.exe.config to use the ASMX endpoint as well:

    <add key="communicationPlatform" value="ASMX" />

And now you have one client that can talk to .NET Remoting, WCF and ASMX! All of these use the same shared interface!

Key Takeaways:

  1. Follow the .NET Remoting guidance
  2. Use interfaces to define service contracts since they translate well to ASMX, WCF and .NET Remoting
  3. Service implementations should not take any dependency on the hosting model or distributed application platform to enable migration and portability
  4. Abstracting proxy creation and hosting code can help with easier migration in the future