From .NET Remoting to the Windows Communication Foundation (WCF)
Ingo Rammer
thinktecture
Applies to:
Visual Studio .NET
Windows Communication Foundation (WCF)
Migration
Note: This article is out of date and does not reflect the current best practices for .NET Remoting. Please refer to this topic for the recommended best practices on migrating from .NET Remoting to WCF. .NET Remoting is a legacy product and not secure in untrusted environments, it is not safe to expose public .NET Remoting endpoints. Microsoft now recommends that any .NET Remoting applications operating in untrusted environments should be migrated entirely to WCF.
Summary: This article discusses potential scenarios for the integration and migration of your existing .NET Remoting based applications to the Windows Communication Foundation (WCF, formerly codenamed Indigo) to take advantage of the new infrastructure for the creation of service oriented applications. You will learn about the necessary key changes and the design decisions that you should take into account. (26 printed pages)
Click here to download the code sample for this article.
Contents
Integrate or Migrate?
Let's Set the Baseline for our Migration
Step 1 - Make Interface-Contracts more Explicit
Include Fault-Information in Your Interfaces
Step 2 - Use DataContract for Explicit Data Type Definition
Step 3 - Use Sessions instead of Client-Activated Objects
Callbacks and Events Become Simpler
Passing Object Graphs the Way Remoting Did
Migrating Extensibility Points
Conclusion
Integrate or Migrate?
Before going into the details about how you can migrate your .NET Remoting based application to WCF, let me assure you: it might not even be necessary. WCF and Remoting can be used side-by-side even inside the same application or inside the same AppDomain. You can freely mix and match WCF with Remoting code and—as you'll see in the demo application code for this article—you can even create server-side objects that can be used with WCF and Remoting at the same time.
A migration to WCF might nevertheless make a lot of sense for your particular case, especially if you want or need to take advantage of the features of this new platform. WCF might be interesting for you if you want to expose your services to different platforms, if you decide to use interoperable security and transactional flow, or if you want to increase your choice for communication channels from TCP and HTTP to also include MSMQ and a considerably faster named pipes channel.
And as you'll see in the remainder of this article, a migration from .NET Remoting to WCF might be easier than expected. If you decide not to migrate your application right now, however, you will learn which features of .NET Remoting can be easily transitioned to WCF later, and which feature sets should only be used with caution.
Let's Set the Baseline for our Migration
For a few years, the general recommendation has been to use .NET Remoting similar to Web services by using mainly server activated components in SingleCall or Singleton mode. This is also the scenario that can be migrated quite easily.
In most Remoting-based applications, you will usually see at least three different Visual Studio projects that deal with the remote interactions:
- A client-side application (EXE).
- A DLL that is shared between client and server. It contains interface-definitions and [Serializable] objects.
- An implementation of your services. This might be located in an EXE if hosted standalone, or in a DLL if hosted in Internet Information Server (IIS).
A subset of a service for storing customer information could, for example, use shared interface and data type definitions similar to the following. (I have used a mix of [Serializable] and ISerializable to show a comprehensive migration scenario.)
public interface ICustomerManager { void StoreCustomer(Customer cust); } [Serializable] public class Customer { public string Firstname; public string Lastname; public Address DefaultDeliveryAddress; public Address DefaultBillingAddress; } [Serializable] public class Address: ISerializable { public string Street; public string Zipcode; public string City; public string State; public string Country; public Address() { } public Address(SerializationInfo info, StreamingContext context) { Street = info.GetString("Street"); Zipcode = info.GetString("Zipcode"); City = info.GetString("City"); State = info.GetString("State"); Country = info.GetString("Country"); } public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("Street", Street); info.AddValue("Zipcode", Zipcode); info.AddValue("City", City); info.AddValue("State", State); info.AddValue("Country", Country); } }
The boilerplate implementation code for this service would be located in a class that is derived from MarshalByRefObject and that implements the interface ICustomerManager:
public class CustomerService: MarshalByRefObject, ICustomerManager { public void StoreCustomer(Customer cust) { // real operation removed Console.WriteLine("Storing customer ..."); } }
To expose this class as a .NET Remoting component, you would then create a configuration file like the following to associate the URL https://<servername>:8080/CustomerManager.rem with the class CustomerService.
<configuration > <system.runtime.remoting> <application> <channels> <channel ref="http" port="8080" /> </channels> <service> <wellknown mode="Singleton" type="Server.CustomerService, Server" objectUri="CustomerManager.rem" /> </service> </application> </system.runtime.remoting> </configuration>
The final step to creating this .NET Remoting-based application (if you decide not to run it inside IIS) is to implement a custom hosting application that calls RemotingConfiguration.Configure() to start listening to incoming requests.
using System; using System.Runtime.Remoting; using Shared; class ServerProgram { public static void Main(string[] args) { StartRemotingServer(); Console.ReadLine(); } private static void StartRemotingServer() { string configFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile; RemotingConfiguration.Configure(configFile); Console.WriteLine("Remoting-Server is running ..."); } }
The matching client application can then use Activator.GetObject() to create a reference to the server-side CustomerService and call its methods:
class ClientProgram { public static void Main(string[] args) { CallSimpleServiceWithRemoting(); Console.WriteLine("Done"); Console.ReadLine(); } private static void CallSimpleServiceWithRemoting() { Console.WriteLine("Calling a simple service with .NET Remoting"); Customer cust = new Customer(); cust.Firstname = "John"; cust.Lastname = "Doe"; cust.DefaultBillingAddress = new Address(); cust.DefaultBillingAddress.Street = "One Microsoft Way"; cust.DefaultDeliveryAddress = cust.DefaultBillingAddress; ICustomerManager mgr = (ICustomerManager) Activator.GetObject(typeof(ICustomerManager), "https://localhost:8080/CustomerManager.rem"); mgr.StoreCustomer(cust); } }
Now that we've created a sample baseline application, let's look at its migration to WCF.
Step 1 - Make Interface-Contracts more Explicit
To move a .NET Remoting-based application to WCF, you can usually follow a simple three step process. As the first step, you have to change the interface definitions so that they follow the more explicit WCF way of using [ServiceContract] and [OperationContract]. Depending on the application and on your interoperability requirements, this might be the only thing you have to change. (The other two steps can be completely optional as long as you did not use .NET Remoting's Client-Activated Objects.)
To make the interface contracts more explicit, you have to decorate the remote interfaces with [ServiceContract] and each exposed method with [OperationContract]. (These attributes are part of the library System.ServiceModel.DLL and are contained in the namespace System.ServiceModel alongside the majority of the WCF components).
You would therefore have to change the interface presented above so that it looks like this:
[ServiceContract] public interface ICustomerManager { [OperationContract] void StoreCustomer(Customer cust); }
The server-side implementation can remain completely unchanged and in this first step you also don't need to change any details of your [Serializable] or ISerializable objects. You only have to change the configuration files on client and server side to use WCF instead of .NET Remoting.
You will have to extend the server-side configuration file with a section <system.serviceModel> that contains the information about the WCF services your application should expose.
<configuration> <system.runtime.remoting> <!-- you can leave this as-is --> </system.runtime.remoting> <system.serviceModel> <services> <service name="Server.CustomerService"> <endpoint address="net.tcp://localhost:8081/CustomerManager" binding="netTcpBinding" contract="Shared.ICustomerManager" /> </service> </services> </system.serviceModel> </configuration>
In this case, the server-side component will be exposed using the WCF's tcp binding at the address net.tcp://localhost:8081/CustomerManager. To start the service host, you will have to change your application to create and open a ServiceHost for your service:
class ServerProgram { private static ServiceHost _customerServiceHost; public static void Main(string[] args) { StartWCFServer(); Console.ReadLine(); } private static void StartWCFServer() { _customerServiceHost = new ServiceHost(typeof(CustomerService)); _customerServiceHost.Open(); Console.WriteLine("WCF Server is running ..."); } }
Please note that this is only a small server host for demonstration purposes. In reality, you will quite likely use an already existing host like Internet Information Server instead of creating your own hosting application.
On the client-side, you will now have to create a matching configuration file. The following snippet associates the configuration name "customermanager" with the destination URL previously defined .
<configuration> <system.serviceModel> <client> <endpoint name="customermanager" address="net.tcp://localhost:8081/CustomerManager" binding="netTcpBinding" contract="Shared.ICustomerManager"/> </client> </system.serviceModel> </configuration>
You also have to change the client-side application code so that it uses WCF's ChannelFactory instead of Activator.GetObject():
class ClientProgram { public static void Main(string[] args) { CallSimpleServiceWithWCF(); } private static void CallSimpleServiceWithWCF() { Console.WriteLine("Calling a simple service with WCF"); Customer cust = new Customer(); cust.Firstname = "John"; cust.Lastname = "Doe"; cust.DefaultBillingAddress = new Address(); cust.DefaultBillingAddress.Street = "One Microsoft Way"; cust.DefaultDeliveryAddress = cust.DefaultBillingAddress; ChannelFactory<ICustomerManager> fact = new ChannelFactory<ICustomerManager>("customermanager"); ICustomerManager mgr = fact.CreateChannel(); mgr.StoreCustomer(cust); } }
As you have seen in the samples above, the first step for migrating a .NET Remoting application to WCF is quite simple: you only need to annotate the interfaces with [ServiceContract] and [OperationContract], add a section to the configuration files, change two lines of server-side hosting code and two lines of client-side activation code to be running on WCF.
Include Fault-Information in Your Interfaces
When making your interfaces more explicit, you will also have to add information about the possible error results to your method declarations. For our first iteration, it will be enough to just enable the transfer of exception information over the WCF boundary. To do this, you need to decorate the service implementation with the attribute [ServiceBehavior] with its flag ReturnUnknownExceptionsAsFaults set to true:
[ServiceBehavior(ReturnUnknownExceptionsAsFaults=true)] public class CustomerService: ICustomerManager { //
…}
If you do not specify this attribute, WCF will return a CommunicationException to the caller whenever the communication with the server could not be completed successfully (including the cases when a server-side exception has been thrown.) As soon as this attribute is defined on a service, all uncaught exceptions will be transferred as a FaultException that you can catch at the client side to retrieve the server's exception information. You can also allow the transfer of specific custom faults by using the [FaultContract] attribute on your interface method definitions. You can find out more about how to do this in the WinFX SDK's Online Help at the topic "FaultContractAttribute Class."
Congratulations! Depending on the kind of application you are creating, your interoperability needs, and the degree of service orientation you desire, this might be everything you need to change. WCF will automatically honor [Serializable] and ISerializable for the parameters you pass to your remote components. Please note that WCF will however not expose exactly the same behavior as .NET Remoting for passing complex object graphs. Instead of following the remoting model of transparently restoring in-memory object references after the deserialization, WCF by default follows a model similar to ASP.NET Web services that will always try to render the serialized object as a hierarchical structure. This means that object graphs with internal or circular references will for example not be serialized properly. Please see the section Passing Object Graphs the Way Remoting Did for more details.
Step 2 - Use DataContract for Explicit Data Type Definition
If you continue to use [Serializable] (and even more so when using ISerializable) you might walk on the borderline of violating one of the principles of service orientation: crossing the boundary between client and server should be very explicit. When using [Serializable], all private members of a class will be serialized automatically. The process of adding, moving or renaming a private field will therefore implicitly change the interface contract and will break existing clients.
[Serializable] in version 1.1 of the .NET Framework also did not support a clean way for versioning your data structures so that you will quite likely have had to resort to using ISerializable purely for making sure that multiple versions of your clients can talk to an existing server. (Otherwise adding a new field in the server's version of your data structures would have rendered your existing clients unusable.)
To change a class from [Serializable] to the full WCF version, you have to mark the class with [DataContract] and all field members (private, public and properties) that should be included with [DataMember]. You can then optionally remove the [Serializable] attribute if you don't plan on using the same class in an older .NET Remoting environment.
The changed Customer class will therefore look like the following:
[DataContract] public class Customer { [DataMember] public string Firstname; [DataMember] public string Lastname; [DataMember] public Address DefaultDeliveryAddress; [DataMember] public Address DefaultBillingAddress; }
This initial conversion is quite straight-forward. You only have to take special care when using classes that have implemented ISerializable to support versioning. The reason is that using ISerializable for this purpose is quite often only a workaround (as .NET Remoting doesn't natively support data structure versioning). In WCF however, you can rely on the built-in versioning support for [DataMember] by specifying the version number in which a field has been added and whether or not it is optional.
The following .NET Remoting-based ISerializable implementation shows this kind of versioning. In this example, the field AttentionOf has been added in the second version of the application. In the deserialization constructor, the call to GetString() is encapsulated in a try/catch block to ignore the error if this field is not present in the input message.
[Serializable] public class Address: ISerializable { public string Street; public string AttentionOf; public string Zipcode; public string City; public string State; public string Country; public Address() { } public Address(SerializationInfo info, StreamingContext context) { Street = info.GetString("Street"); Zipcode = info.GetString("Zipcode"); City = info.GetString("City"); State = info.GetString("State"); Country = info.GetString("Country"); // Version 2 try { AttentionOf = info.GetString("AttentionOf"); } catch {} } public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("Street", Street); info.AddValue("Zipcode", Zipcode); info.AddValue("City", City); info.AddValue("State", State); info.AddValue("Country", Country); // Version 2 info.AddValue("AttentionOf", AttentionOf); } }
When changing this implementation to WCF's [DataContract] it will become substantially simpler, as this attribute allows you to specify whether a certain field is required or optional. (Optional is the default).
[DataContract] public class Address { [DataMember(IsRequired=true)] public string Street; [DataMember(IsRequired=true)] public string Zipcode; [DataMember(IsRequired=true)] public string City; [DataMember(IsRequired=true)] public string State; [DataMember(IsRequired=true)] public string Country; [DataMember] public string AttentionOf; }
When you use the [DataMember] attribute without setting the IsRequired flag, the field will automatically be treated as optional to allow for change-tolerant serialization.
Step 3 - Use Sessions instead of Client-Activated Objects
.NET Remoting has a concept called Client-Activated Objects (CAOs) that allows you to create references to server side instances and treat these like local objects. If you would for example create multiple distinct client-side instance references they would point to the same number of distinct server-side objects.
The same thing (distinct proxies pointing to distinct server-side objects) happens whenever an object derived from MarshalByRefObject is passed over a remoting boundary. In this case, only an object reference (a serialized ObjRef object) is actually sent over the wire and a proxy pointing back to the original object will be created on the destination side. Every subsequent method call will be sent back to the original object. Contrary to [Serializable] objects, these CAOs never leave the AppDomain in which they have been created.
In .NET Remoting, you can implement a behavior like this simply by creating a method that returns MarshalByRefObject as shown in the following sample:
public interface IRemoteFactory { IMySessionBoundObject GetInstance(); } public interface IMySessionBoundObject { string GetCurrentValue(); void SetCurrentValue(string val); void PrintCurrentValue(); }
The implementation of IRemoteFactory, shown in the following code, would then be registered as a regular server-activated object.
public class RemoteFactory : MarshalByRefObject, IRemoteFactory { public IMySessionBoundObject GetInstance() { return new MySessionBoundObject(); } } public class MySessionBoundObject : MarshalByRefObject, IMySessionBoundObject { private string _value; public void PrintCurrentValue() { Console.WriteLine("Current value is " + _value); } public string GetCurrentValue() {return _value;} public void SetCurrentValue(string val) {_value = val;} }
As you can see, in .NET Remoting no special marshalling or preparation work needs to be done. You can simply pass a new instance of MySessionBoundObject back to your client and use it like any regular object. (The following example assumes that you have registered RemoteFactory with .NET Remoting at the URL https://localhost:8080/Factory.rem.)
private static void CallRemoteObjectWithRemoting() { IRemoteFactory fact = (IRemoteFactory )Activator.GetObject(typeof(IRemoteFactory ), "https://localhost:8080/Factory.rem"); IMySessionBoundObject o1 = fact.GetInstance(); IMySessionBoundObject o2 = fact.GetInstance(); o1.SetCurrentValue("Hello"); o2.SetCurrentValue("World"); if (o1.GetCurrentValue() == "Hello" && o2.GetCurrentValue() == "World") { Console.WriteLine("Remote instance management works as expected"); } }
When running this code, the variables o1 and o2 will point to two different server-side objects. After setting the string value for each distinct object, it can be retrieved again and will not be affected by any other interactions or other sessions. They act just like normal objects.
This simplicity for passing remote references is also one of the biggest dangers in .NET Remoting. In a lot of architectures, the transfer of remote references is not desired apart from some very limited and controlled cases. Passing a MarshalByRefObject instead of a [Serializable] object—by mistake—usually results in dramatically worsened performance and scalability. After all: each method call will now have to travel over the network, potentially spanning multiple cities, countries or continents.
In WCF you can pass references to remote services but you have to be very explicit about doing so. It usually can't just happen without very clear intent.
At first, you have to enhance the [OperationContract] by setting its field Session to true to indicate that it needs a transport channel that supports sessions.
[ServiceContract(Session=true)] public interface IMySessionBoundObject { [OperationContract] string GetCurrentValue(); [OperationContract] void SetCurrentValue(string val); [OperationContract] void PrintCurrentValue(); }
Please note: a session in terms of WCF is substantially different from a session in terms of ASP.NET or other Web application frameworks. A session does not usually span multiple services, but is instead only valid in the communication with a single service instance. It just means that you will talk with the same server-side object for the lifetime of your client-side proxy.
In the implementation class, you have to add a [ServiceBehavior] attribute to indicate which kind of session isolation you desire. (Sessions can either be private or shared. A reference to a shared session service can be passed to other communication partners.)
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Shareable)] public class MySessionBoundObject : MarshalByRefObject, IMySessionBoundObject { private string _value; public void PrintCurrentValue() { Console.WriteLine("Current value is " + _value); } public string GetCurrentValue() {return _value;} public void SetCurrentValue(string val) {_value = val;} }
It is not strictly necessary to create a factory for WCF as you can instead just register the session-bound object in the client and server side configuration files. To do so, you would add the following <service> entry to your server side configuration:
<service name="Server.MySessionBoundObject"> <endpoint address="net.tcp://localhost:8081/MySessionBound" binding="netTcpBinding" contract="Shared.IMySessionBoundObject" /> </service>
And the following <endpoint> entry to your client-side configuration file:
<endpoint name="sessionbound" address="net.tcp://localhost:8081/MySessionBound" binding="netTcpBinding" contract="Shared.IMySessionBoundObject" />
You can now use the ChannelFactory to create channels to this service. Behind the scenes, a session-aware connection will be created so that the following two references o1 and o2 point to two different server-side objects:
private static void CallRemoteObjectWithWCFWithoutFactory() { ChannelFactory<IMySessionBoundObject> chfact = new ChannelFactory<IMySessionBoundObject>("sessionbound"); IMySessionBoundObject o1 = chfact.CreateChannel(); IMySessionBoundObject o2 = chfact.CreateChannel(); o1.SetCurrentValue("Hello"); o2.SetCurrentValue("World"); if (o1.GetCurrentValue() == "Hello" && o2.GetCurrentValue() == "World") { Console.WriteLine("Remote instance management works as expected"); } }
If you decide to use a server-side factory (maybe because you want to return references to different services depending on some external condition), you do not have to register the <endpoint> for the session bound object on the client side.
Instead you can re-use the .NET Remoting based IRemoteFactory interface and annotate it to be compatible with WCF. The main change is that you are not allowed to directly return an instance of IMyRemoteObject but instead you have to return an object of type EndpointAddress10 (this is a WS-Addressing 1.0 compatible endpoint address). The client later has to manually create a channel to this returned address to ensure that the client developer knows that every method call on this object will trigger a remote service invocation.
[ServiceContract] public interface IRemoteFactory { [OperationContract] EndpointAddress10 GetInstanceAddress(); }
In the implementation for the remote factory's GetInstanceAddress() method, you will have to create a server-side channel to the registered object before you can return the EndpointAddress10 object:
public class RemoteFactory : MarshalByRefObject, IRemoteFactory { public static ChannelFactory<IMySessionBoundObject> _fact = new ChannelFactory<IMySessionBoundObject>("sessionbound"); public EndpointAddress10 GetInstanceAddress() { IClientChannel chnl = (IClientChannel) _fact.CreateChannel(); return EndpointAddress10.FromEndpointAddress(chnl.RemoteAddress); } }
For this code to work, you have to register the session bound object both as a <service> and as a <client> in the server side configuration file. This is necessary because the server-side factory needs to create the session-based channel to the new service instance and therefore has to act as a client for its own services:
<client> <endpoint name="sessionbound" address="net.tcp://localhost:8081/MySessionBound" binding="netTcpBinding" contract="Shared.IMySessionBoundObject " /> </client> <services> <service name="Server.RemoteFactory "> <endpoint address="net.tcp://localhost:8081/MyRemoteFactory" binding="netTcpBinding" contract="Shared.IRemoteFactory " /> </service> </services>
On the client side, you do not have to register the session bound object, but instead just the factory:
<endpoint name="factory" address="net.tcp://localhost:8081/MyRemoteFactory" binding="netTcpBinding" contract="Shared.IRemoteFactory " />
To create a channel to the session based service, you can now first create a channel to the factory, call its GetInstanceAddress() method and subsequently create a new ChannelFactory and channel to the session bound object by converting EndpointAddress10 (which you have received from the factory) to a regular WCF EndpointAddress object:
private static void CallRemoteObjectWithWCFWithFactory() { ChannelFactory<IRemoteFactory> chfact = new ChannelFactory<IRemoteFactory>("factory"); IRemoteFactory fact = chfact.CreateChannel(); EndpointAddress10 adr1 = fact.GetInstanceAddress(); EndpointAddress10 adr2 = fact.GetInstanceAddress(); ChannelFactory<IMySessionBoundObject> f1 = new ChannelFactory<IMySessionBoundObject>( new NetTcpBinding(), adr1.ToEndpointAddress()); ChannelFactory<IMySessionBoundObject> f2 = new ChannelFactory<IMySessionBoundObject>( new NetTcpBinding(), adr2.ToEndpointAddress()); IMySessionBoundObject o1 = f1.CreateChannel(); IMySessionBoundObject o2 = f2.CreateChannel(); o1.SetCurrentValue("Hello"); o2.SetCurrentValue("World"); if (o1.GetCurrentValue() == "Hello" && o2.GetCurrentValue() == "World") { Console.WriteLine("Remote instance management works as expected"); } }
These two different approaches with WCF allow you to mimic .NET Remoting's reference passing in a more explicit way. The lifetime management system in this case is based on the existence of a session-bound channel: an object will be released as soon as no more channels point towards it and the underlying timer has been expired.
Callbacks and Events Become Simpler
A rather complex area in .NET Remoting is the use of callbacks and events. Especially in the latter case, you have to use an intermediary shim object to receive the event. This shim would then send the event along to the real client-side implementation. In its entirety this process is rather complex so that a lot of long-running operations (which would lead themselves to being called asynchronously) end up being performed synchronously when using .NET Remoting.
In WCF on the other hand, you have the choice to use a so called "Duplex" message exchange pattern. A duplex exchange is based on a series of one-way interactions that form a complete conversation. A long running operation can for example be started by a single method call for which the client doesn't have to wait for completion. The server can then periodically notify the caller about the percentage of work that has been completed.
If you for example have a long running operation that calculates a customer's lifetime value for your company by checking multiple external services and databases, it might make sense to provide a purely asynchronous interface. In this case, you have to create two different [ServiceContracts]: one that is used to communicate from client to server and one that will be used for the notifications on the backchannel.
You will have to create the contract from client to server so that the [ServiceContract] specifies the type of the callback contract. In addition, for the [OperationContract] you have to set IsOneWay to true to allow for truly asynchronous behavior. The callback contract itself is just a regular contract for which the [OperationContract]-members have also been marked as one way interactions. You can seen this in the following example:
[ServiceContract(CallbackContract=typeof(ICustomerCalculationCallback))] public interface ICustomerCalculation { [OperationContract(IsOneWay=true)] void CalculateLifetimeValue(int customerID); } [ServiceContract] public interface ICustomerCalculationCallback { [OperationContract(IsOneWay=true)] void PercentCompleted(int percentCompleted); [OperationContract(IsOneWay = true)] void CalculationCompleted(decimal lifetimeValue); }
On the client side, you have to create a class that implements the callback interface ICustomerCalculationCallback. To create a reference to the remote service, you will use an object of type DuplexChannelFactory (instead of ChannelFactory for regular channels). This duplex channel factory is initialized by passing the object that will handle the callback as a first parameter.
public class Calculation : ICustomerCalculationCallback { private ICustomerCalculation _calc; public void CalculateLifetimeValue(int customerID) { DuplexChannelFactory<ICustomerCalculation> calcFactory = new DuplexChannelFactory<ICustomerCalculation>( this, "customercalculation"); Console.WriteLine("Starting customer calculation"); _calc = calcFactory.CreateChannel(); _calc.CalculateLifetimeValue(customerID); Console.WriteLine("Calculation started"); } public void PercentCompleted(int percentCompleted) { Console.WriteLine("{0} % completed", percentCompleted); } public void CalculationCompleted(decimal lifetimeValue) { Console.WriteLine("Lifetime value is {0}.", lifetimeValue); ((IChannel)_calc).Close(); } }
At the server side, you can acquire the backchannel that is pointing to the client-side implementation of the callback handler by using the current OperationContext's GetCallbackChannel() method. You can use this channel just as any regular WCF communication interface.
public class CustomerCalculationService : ICustomerCalculation { public void CalculateLifetimeValue(int customerID) { ICustomerCalculationCallback _callback = OperationContext.Current. GetCallbackChannel<ICustomerCalculationCallback>(); System.Threading.Thread.Sleep(1000); _callback.PercentCompleted(25); System.Threading.Thread.Sleep(1000); _callback.PercentCompleted(50); System.Threading.Thread.Sleep(1000); _callback.PercentCompleted(75); System.Threading.Thread.Sleep(1000); _callback.PercentCompleted(100); _callback.CalculationCompleted(19344); } }
When running this code, you will see that the call to CalculateLifetimeValue() completes immediately so that the client does not have to wait for completion of the long running operation. Whenever the server has new information it will instead invoke a method on the callback channel so that the client can asynchronously react on it.
Passing Object Graphs the Way Remoting Did
As I've mentioned above, there is one critical area where the behavior of .NET Remoting differs substantially from the way WCF works: the passing of object graphs. To illustrate this, let's take a look at how the Customer object I've used throughout the samples above has been populated on the client side:
Customer cust = new Customer(); cust.Firstname = "John"; cust.Lastname = "Doe"; cust.DefaultBillingAddress = new Address(); cust.DefaultBillingAddress.Street = "One Microsoft Way"; cust.DefaultDeliveryAddress = cust.DefaultBillingAddress;
On the client side, the fields DefaultBillingAddress and DefaultDeliveryAddress reference the same Address object. If you would pass this object over a .NET Remoting boundary, the same thing would be true on the server side: only one instance of the type Address would exist and both of the customer's addressing fields would reference this same instance.
This is, however, not the case when using WCF. The reason is that there is simply no standardized way of passing internal references in an object graph in an interoperable way. A possible Java client might have a hard time figuring out your intention as the underlying XML transfer format is intrinsically hierarchical and not graph/network- oriented. As cross-platform interoperability has been one of WCF's design goals, it therefore doesn't support the "old" semantics of .NET Remoting out of the box.
Fortunately, the underlying serializer has been built with flexibility in mind. If you don't care about interoperability—which will be the case for most .NET Remoting based applications—then you can change this behavior by using one of WCF's extensibility points, a DataContractSerializerOperationBehavior. Without going into the details of how this extensibility point works internally, let me first present you the complete source code for one possible implementation (which I first learned about at the weblog of the WCF team member Sowmy Srinivasan):
public class PreserveReferencesOperationBehavior: DataContractSerializerOperationBehavior { public PreserveReferencesOperationBehavior ( OperationDescription operationDescription) : base(operationDescription) { } public override XmlObjectSerializer CreateSerializer( Type type, string name, string ns, IList<Type> knownTypes) { return CreateDataContractSerializer(type, name, ns, knownTypes); } private static XmlObjectSerializer CreateDataContractSerializer( Type type, string name, string ns, IList<Type> knownTypes) { return CreateDataContractSerializer(type, name, ns, knownTypes); } public override XmlObjectSerializer CreateSerializer( Type type, XmlDictionaryString name, XmlDictionaryString ns, IList<Type> knownTypes) { return new DataContractSerializer(type, name, ns, knownTypes, 0x7FFF, false, true /* preserveObjectReferences */, null); } }
The main magic happens in the overridden method CreateSerializer() that passes true as the value for preserveObjectReferences to configure the underlying serializer to support internal object references.
To enable this functionality in your application, you will need a second extensibility implementation, called an operation behavior attribute. You can later use this attribute on your service interface methods to switch between the old and new style of reference preservation:
public class PreserveReferencesAttribute : Attribute, IOperationBehavior { public void AddBindingParameters(OperationDescription description, BindingParameterCollection parameters) {} public void ApplyClientBehavior(OperationDescription description, ClientOperation proxy) { IOperationBehavior innerBehavior = new PreserveReferencesOperationBehavior(description); innerBehavior.ApplyClientBehavior(description, proxy); } public void ApplyDispatchBehavior(OperationDescription description, DispatchOperation dispatch) { IOperationBehavior innerBehavior = new PreserveReferencesOperationBehavior(description); innerBehavior.ApplyDispatchBehavior(description, dispatch); } public void Validate(OperationDescription description) { } }
Whenever you place this attribute on the method of a service contract interface, WCF will automatically use the customized serializer and will use the same graph passing behavior as .NET Remoting did. In the following example, the method StoreCustomerWithReferences() will be configured in this way:
[ServiceContract] public interface ICustomerManager { [OperationContract] void StoreCustomer(Customer cust); [OperationContract] [PreserveReferences] void StoreCustomerWithReferences(Customer cust); [OperationContract] void TestException(); }
Migrating Extensibility Points
Both .NET Remoting and WCF offer a large and comprehensive extensibility model. In both systems, you can configure and change the complete processing chain from client to the server. The extensibility model of WCF is also a lot easier to use than the one of .NET Remoting. This change in interfaces however also means that the migration of your extensibility code might be quite complex. (After all, you might have to re-think most of the extensibility logic and move it to the new interfaces.)
This was the bad news. The good news is that you quite likely don't even have to migrate a majority of your code. A lot of extensibility implementations that I've seen in the past few years were geared towards working around some of the limitations of .NET Remoting. I've seen custom sinks and channels for security, transaction flow, load balancing, logging and tracing, MSMQ and named pipe support, and so on. All these features are now part of WCF and of the Windows Server System so that you might not even have to migrate your custom sinks and channels. Instead you can just use out-of-the box functionality of WCF in all these cases.
But even if you really have to move your extensibility sinks and channels to WCF: the new model will support this task with a vastly easier API for custom behaviors. The details are beyond the scope of this article, but you can expect more whitepapers and articles on MSDN about these topics in the future.
Conclusion
As you have seen, a migration from .NET Remoting to WCF is not a task you have to be afraid of. For most applications, a simple three-step process can bring your application to the new platform. In most cases, you will only have to mark your interface contracts with [ServiceContract] and [OperationContract], your data structures with [DataContract] and [DataMember] and maybe change some parts of your activation model to be based on sessions instead of client-activated objects.
If you decide that you want to take advantage of the features of the Windows Communication Foundation, the complete migration from .NET Remoting to WCF should therefore be a rather easy task for the majority of applications.
About the author
Ingo Rammer is co-founder of thinktecture, a company providing in-depth technical consulting and mentoring services for software architects and developers. Ingo is an expert for design and development of distributed applications, and he provides architecture, design, and architecture review services for teams of all sizes. He focuses mainly on improving performance, scalability and reliability of critical .NET applications. You can contact him at [email protected] and read more about him at https://www.thinktecture.com/staff/ingo.