Microsoft .NET Remoting (Pro-Developer) - page 31

Proxies

In general programming terms, a proxy object is any object that stands in for another object and controls access to that other object. Controlling another object through a proxy can be useful for purposes such as delaying the creation of an object that has expensive startup costs or implementing access control. At a minimum, most remoting technologies—including .NET Remoting—use proxies to make remote objects appear to be local. These remoting technologies usually use proxies to perform a variety of tasks depending on the architecture.

The .NET Remoting proxy layers actually comprise two proxy objects: one implemented primarily in unmanaged code and the other implemented in managed code that you can customize. Splitting the proxy layer into two separate objects is a useful conceptual design that makes customizing this layer easier for complex scenarios. Before we start customizing the proxy layer, let’s examine these two proxy objects.

TransparentProxy

Suppose you call new or Activator.CreateInstance to instantiate a remote object:

MyObj obj = new MyObj();
int Result = obj.MyMethod();						

As soon as the line calling new returns, MyObj contains a reference to a type named TransparentProxy. The .NET Remoting infrastructure dynamically creates the TransparentProxy instance at run time by using reflection against the real object’s metadata. .NET Framework reflection is a powerful technique for examining metadata at run time. Using reflection, the .NET Remoting infrastructure discovers the public interface of the real object and creates a TransparentProxy that mirrors this interface. (For more information about reflection, see Jeffrey Richter’s book Applied Microsoft .NET Framework Programming.) The resulting TransparentProxy object implements the public methods, properties, and members of MyObj. The client code uses this local TransparentProxy object as a stand-in for the real remote object, which is located in another remoting subdivision, such as another context, application domain, process, or machine. Hereafter, you won’t see any difference in the semantics of dealing with a local copy of MyObj and the TransparentProxy that mirrors the remote object.

The main job of the TransparentProxy is to intercept method calls to the remote object and pass them to the RealProxy, which does most of the proxy work. (We’ll discuss RealProxy in a moment.) Using unmanaged code, TransparentProxy intercepts calls to what appears to the caller to be a local object and creates a MessageData struct, consisting mainly of pointers to unmanaged memory and thereby describing the method call. The TransparentProxy passes this struct to the PrivatelInvoke method of the RealProxy. The PrivatelInvoke method uses the MessageData struct to create the remoting messages that will be passed through the message sink chain and eventually delivered to the remote object.

Although TransparentProxy is the type of object that client code holds in lieu of the real remote object, you can’t do anything to customize or extend TransparentProxy. RealProxy is the only proxy that you can customize.

RealProxy

RealProxy is a documented and extensible managed class that has a reference to the unmanaged black box named TransparentProxy. However, RealProxy is an abstract class and therefore isn’t directly creatable. When the TransparentProxy instance hands off the MessageData object to the RealProxy, it’s actually passing it to a concrete class that derives from RealProxy and is named RemotingProxy. RemotingProxy is an undocumented internal class, so we can’t derive from it. Instead, we’ll replace RemotingProxy and derive our own class from RealProxy so that we can seize control and customize remoting behavior. Although most extensions to remoting are done by using message sinks, some reasons to extend RealProxy exist. RealProxy affords us the first opportunity to intercept and customize both remote object construction and remote object method calls. However, because no corresponding customizable server-side proxy exists, you must use message sinks to perform tasks that require client-side processing and server-side processing, such as encryption and compression.

Extending RealProxy

To write a proxy to replace RemotingProxy, we need to derive a class from RealProxy and add a new constructor and private MarshalByRefObject, as shown here:

public class MyProxy : RealProxy
{
    MarshalByRefObject _target;
    public MyProxy(Type type, MarshalByRefObject target)
    : base(type)
    {
        _target = target;
    }
}

We need to capture the reference to the real MarshalByRefObject so that our proxy can forward calls to the real remote object. We also call the RealProxy’s constructor and pass the type of the remote object so that the .NET Remoting infrastructure can generate a TransparentProxy instance to stand in for the object.

Next we need to override the abstract Invoke method:

public override IMessage Invoke(IMessage msg)
{
    // Handle message 
}

Invoke is the main extensibility point of RealProxy. Here we can customize and forward, refuse to forward, or ignore the messages sent by the TransparentProxy and destined for the real object.

Now that we have a compilable RealProxy derivative, the next step is to hook up this instance to a certain MarshalByRefObject, thereby replacing the RemotingProxy class. Two techniques exist for creating proxies: using a Proxy­Attribute class and creating proxies directly. We’ll explore both techniques in the examples discussed next.

Custom Proxies in Practice

Because the .NET Remoting infrastructure provides so many ways to extend the remoting architecture, you might find that you can perform certain customization tasks in more than one way. Although message sinks and channels tend to be the most useful remoting extensibility points, reasons to perform certain custom­izations by using proxies do exist. Because proxies don’t have a customizable server-side companion layer, they’re best used to perform client-centric interception work. On the other hand, if your customizations require both client intervention and server intervention, you’ll need to use message sinks.

In this section, we’ll look at three examples of custom proxies. First, we’ll use a proxy to intercept the remote object activation process and demonstrate the differences between client activation and server activation. Next we’ll use a proxy to switch between firewall-friendly channels and high-performing channels. We’ll round out the proxy section by showing a load-balancing example that uses call context.

Activation Example

One interesting feature of proxies is that you can use them to intercept the activation of both client-activated and server-activated objects. By intercepting an object’s activation, you can choose to activate another object (perhaps on another machine), modify client-activated object constructor arguments, or plug in a custom activator. Before we start, let’s introduce the ProxyAttribute class.

The ProxyAttribute class provides a way to declare that we want the .NET Remoting framework to use our custom proxy instead of the default RemotingProxy. The only restriction to plugging in custom proxies this way is that the ProxyAttribute can be applied only to objects that derive from ContextBoundObject, which we’ll examine along with contexts in Chapter 6, “Message Sinks and Contexts.” Because ContextBoundObject derives from MarshalByRefObject, this limitation won’t be a problem. To wire up a ProxyAttribute, first derive a class from ProxyAttribute and override the base CreateInstance method:

[AttributeUsage(AttributeTargets.Class)]
public class MyProxyAttribute : ProxyAttribute
{
   public override MarshalByRefObject CreateInstance(Type serverType)
   {
       MarshalByRefObject target = base.CreateInstance(serverType);
       MyProxy myProxy = new MyProxy(serverType, target);
       return (MarshalByRefObject)myProxy.GetTransparentProxy();
   }
}

Next attach the ProxyAttribute to the ContextBoundObject:

[MyProxyAttribute]
public class MyRemoteObject : ContextBoundObject
{
}

When you instantiate MyRemoteObject, the .NET Framework calls the overridden CreateInstance method, in which you instantiate the custom proxy. Next we instantiate our proxy and cast its TransparentProxy to MarshalByRefObject and return. By using this technique, the client can call new or Activator.CreateInstance as usual and still use the custom proxy.

To intercept activation, you need to perform the following tasks:

  • Define a ProxyAttribute class.

  • Define a RealProxy class, and override its Invoke method.

  • In the Invoke method, handle the IConstructionCallMessage.

  • Apply the ProxyAttribute to a ContextBoundObject.

Here’s the ProxyAttribute listing for our activation example:

[AttributeUsage(AttributeTargets.Class)]
public class SProxyAttribute : ProxyAttribute
{
    public override System.MarshalByRefObject 
                    CreateInstance(System.Type serverType)
    {
        Console.WriteLine("SProxyAttribute.CreateInstance()");
        // Get a proxy to the real object.
        MarshalByRefObject mbr =  base.CreateInstance(serverType);

        // Are we on the client side or the server side? This matters
        // because the .NET Remoting infrastructure on both sides of
        // the boundary will invoke this method. If we are on the
        // server side, we need to return the MBR provided by the
        // previous base.CreateInstance call, rather than our custom
        // proxy. If we return our custom proxy, the runtime throws
        // an exception when trying to invoke methods on it.
        //
        if ( RemotingConfiguration.IsActivationAllowed(serverType) )
        {
            return mbr;
        }
        else
        {
            WellKnownServiceTypeEntry[] wcte = 
              RemotingConfiguration
              .GetRegisteredWellKnownServiceTypes();
            if ( wcte.Length > 0 )
            {
                foreach(WellKnownServiceTypeEntry e in wcte)
                {
                    if ( e.ObjectType == serverType )
                    {
                        return mbr;
                    }
                }
            }
        }

        // If we get here, we are running on the client side and we
        // can wrap the proxy with our proxy.
        if ( RemotingServices.IsTransparentProxy(mbr) )
        {
            // Wrap the proxy with our simple interception proxy.
            SimpleInterceptionProxy p = 
              new SimpleInterceptionProxy(serverType, mbr);
            MarshalByRefObject mbr2 = 
              (MarshalByRefObject)p.GetTransparentProxy();
            return mbr2;
        }
        else
        {
            // This is an actual MBR object.
            return mbr;
        }
    }

Our overridden ProxyAttribute.CreateInstance method will be called when the remote object to which the attribute is attached is created. Inside Create­Instance, we first call the base CreateInstance to create a TransparentProxy to the real object. Next we need to be aware that ProxyAttributes are invoked for all remote object instances they attribute. This means that our CreateInstance method will run when the client instantiates the object reference and when the .NET Remoting infrastructure instantiates the object instance on the server. We want to handle only the client-side case, so we need to determine where this ProxyAttribute is running. To determine whether this attribute is running on the server, we first call RemotingConfiguration.GetRegisteredWellKnownServiceTypes to get an array of WellKnownServiceTypeEntry. If our type is in this array, we assume this code is running on the server and we can simply return the TransparentProxy. Otherwise, we assume we’re running on the client, and we instantiate our interception proxy and return its TransparentProxy.

What happens next depends on whether the remote object is client activated or server activated. If the remote object is client activated, the .NET Remoting infrastructure will call our interception proxy’s Invoke method before the client call to new or Activator.CreateInstance returns. If the remote object is server activated, the call to new or Activator.GetObject returns and our proxy’s Invoke method isn’t called until the first method call is made on the remote object.

Here’s the code for our SimpleInterceptionProxy:

public override Invoke(IMessage msg)
{
    // Handle construction call message differently than other method
    // call messages.
    if ( msg is IConstructionCallMessage )
    {
        // Need to finish CAO activation manually.
        string url = GetUrlForCAO(_type);
        if ( url.Length > 0 )
        {
            ActivateCAO((IConstructionCallMessage)msg, url);
        }


        IConstructionReturnMessage crm = 
          EnterpriseServicesHelper.CreateConstructionReturnMessage(
          (IConstructionCallMessage)msg, 
          (MarshalByRefObject)this.GetTransparentProxy());
        return crm;
    }
    else
    {
        MethodCallMessageWrapper mcm = 
          new MethodCallMessageWrapper((IMethodCallMessage)msg);
        mcm.Uri = RemotingServices.GetObjectUri(
          (MarshalByRefObject)_target);
        return RemotingServices.GetEnvoyChainForProxy(
          (MarshalByRefObject)_target).SyncProcessMessage(msg);
    }
}

private void ActivateCAO(IConstructionCallMessage ccm, string url)
{
    // Connect to remote activation service.
    string rem = url + @"/RemoteActivationService.rem"; 
    IActivator remActivator = 
      (IActivator)RemotingServices.Connect(typeof(IActivator), rem);
    IConstructionReturnMessage crm = remActivator.Activate(ccm);

    //
    // The return message's ReturnValue property is the ObjRef for
    // the remote object. We need to unmarshal it into a local proxy
    // to which we forward messages.
    ObjRef oRef = (ObjRef)crm.ReturnValue; 
    _target = RemotingServices.Unmarshal(oRef);
}

string GetUrlForCAO(Type type)
{
    string s = "";
    ActivatedClientTypeEntry[] act = 
      RemotingConfiguration.GetRegisteredActivatedClientTypes();
    foreach( ActivatedClientTypeEntry acte in act )
    {
        if ( acte.ObjectType == type )
        {
            s = acte.ApplicationUrl;
            break;
        }
    }
    return s;
}

Regardless of whether the remote object is client activated or server activated, the first message sent to the proxy’s Invoke method is an IConstructionCallMessage. As we’ll discuss shortly, we must explicitly handle the activation of client-activated objects. We first determine the activation type by using our GetUrlForCAO method. This is similar to how we determined whether the ProxyAttribute was running on the client or the server. But instead of searching for registered server-activated types, we’ll search the list of registered client-activated types by using the RemotingConfiguration.GetRegisteredActivatedClientTypes method. If we find the type of the remote object in the array of ActivatedClientTypeEntry objects, we return the entry’s ApplicationUrl because we’ll need this URL to perform client activation.

If the URL is empty, we assume that the object is server activated. Handling the construction message for a server-activated object is trivial. Recall that server-activated objects are implicitly created by the server application when a client makes the first method call on that object instance. If the URL isn’t empty, we call ActivateCAO to handle client-activated object activation. This process is a bit more involved than the activation of server-activated objects because we must create a new instance of the remote object. When a client-activated object is registered on a server, the .NET Framework creates a server-activated object with the well-known URI RemoteActivation.rem. The framework then uses this server-activated object to create instances of the registered client-activated object. Because we’ve taken control of the activation process, we need to directly call the .NET Framework–created RemoteActivation.rem URI. First, we append RemoteActivation.rem to the ActivatedClientTypeEntry’s URL. Next, by using RemotingServices.Connect, we request an Activator for the desired client-activated object. We create the actual object instance by passing the IConstructionCallMessage to the Activator’s Activate method, which returns an IConstructionReturnMessage. We then extract the ObjRef from IConstructionReturnMessage and unmarshal it to get our proxy to the remote object.

Finally, we decorate the ContextBoundObject with our ProxyAttribute:

[SProxyAttribute()]
public class CBRemoteObject : ContextBoundObject
{
     
}

Our custom proxy now properly handles the activation of both client-activated and server-activated objects. Next we’ll examine a real-world example containing both the IMethodCallMessage and IMethodReturnMessage message types.

Channel Changer Example

Let’s revisit the job assignment application from Chapter 3. Recall that while hosting the JobLib remote object under Microsoft Internet Information Services (IIS) made the job assignment application firewall friendly, performance suffered because IIS supports only HttpChannel. Suppose that some machines running the JobClient application are laptops whose users might be inside the firewall one day and outside it another. It would be nice to use the better-performing TcpChannel when working on the intranet and to switch back to HttpChannel when working on the Internet—all without the user’s knowledge. Some distributed technologies do this by tunneling the TCP protocol over HTTP. We don’t have to tunnel with .NET Remoting because we can solve the problem much more easily by using a custom proxy. Here are the basic steps we’ll take to do this:

  1. Register both TCP and HTTP channels on the server.

  2. Host the remote object in a managed executable because IIS doesn’t support TcpChannel.

  3. Define a custom proxy.

  4. Register an HTTP channel within the proxy.

  5. Directly instantiate the proxy within the client code.

  6. In the Invoke method, inspect the Exception property of IMethod­ReturnMessage and resend the message over HTTP if necessary.

JobServer Changes

We need to change JobServer so that it registers both a TCP channel and an HTTP channel:

// Register a listening channel.
TcpChannel oTcpJobChannel = new TcpChannel( 5556 );
ChannelServices.RegisterChannel( oTcpJobChannel );
HttpChannel oHttpJobChannel = new HttpChannel( 5555 );
ChannelServices.RegisterChannel( oHttpJobChannel );

// Register a well-known type.
RemotingConfiguration.RegisterWellKnownServiceType( 
  typeof( JobServerImpl ), "JobURI", WellKnownObjectMode.Singleton );

The most interesting detail here is that no coupling exists between an object and the channel that accesses it. The single JobServerImpl object is accessible from all registered channels. Therefore, in this case, the client can make one method call over TCP and the next over HTTP.

JobClient Changes

Instead of using a ProxyAttribute to instantiate our custom proxy, we’ll directly instantiate the proxy in the client code, as shown here:

MyProxy myProxy = 
  new MyProxy(typeof(RemoteObject),  new RemoteObject());
RemoteObject foo = (RemoteObject)myProxy.GetTransparentProxy();

Although direct proxy creation is the simplest way to plug in a custom proxy, a couple of drawbacks to using this method exist:

  • The proxy can’t intercept remote object activation. You might never need to customize the activation process, so this limitation generally isn’t an issue.

  • The proxy can’t be transparent to the client code. Because the client must directly instantiate the proxy, this creation must be hard-coded into the client code.

Custom Proxy Details

Our custom proxy contains all the interesting client-side logic for sending messages using either TcpChannel or HttpChannel. This class will register an HttpChannel, attempt to connect using TcpChannel, and, on failure, retry the connection using HttpChannel.

public class ChannelChangerProxy : RealProxy
{
    string _TcpUrl;
    string _HttpUrl;
    IMessageSink[] _messageSinks; 

    public ChannelChangerProxy (Type type, string url)
    : base(type)
    {
        _TcpUrl = url;
        BuildHttpURL( url );
        _messageSinks = GetMessageSinks();
    }

    private void BuildHttpURL(string _TcpUrl)
    {
        UriBuilder uBuilder = new UriBuilder(_TcpUrl);
        uBuilder.Port = 5555;
        uBuilder.Scheme = "http";
        _HttpUrl = uBuilder.ToString();
    }

    public override IMessage Invoke(IMessage msg)
    {
        Exception InnerException;
        msg.Properties["__Uri"] = _TcpUrl;
        IMessage retMsg =
          _messageSinks[0].SyncProcessMessage( msg );
        if (retMsg is IMethodReturnMessage)
        {
            IMethodReturnMessage mrm = (IMethodReturnMessage)retMsg;
            if (mrm.Exception == null)
            {
                return retMsg;
            }
            else
            {
                InnerException = mrm.Exception;
            }
        }
        // On failure, retry by using the HTTP channel.
        msg.Properties["__Uri"] = _HttpUrl;
        retMsg = _messageSinks[1].SyncProcessMessage( msg );
        if (retMsg is IMethodReturnMessage)
        {
            IMethodReturnMessage mrm = (IMethodReturnMessage)retMsg;
        }
        return retMsg;
    }

    private IMessageSink[] GetMessageSinks()
    {
        IChannel[] registeredChannels = 
          ChannelServices.RegisteredChannels;
        IMessageSink MessageSink;
        string ObjectURI;
        ArrayList MessageSinks = new ArrayList();
        foreach (IChannel channel in registeredChannels )
        {
            if (channel is IChannelSender)
            {
                IChannelSender channelSender = 
                  (IChannelSender)channel;
                MessageSink = channelSender.CreateMessageSink(
                              _TcpUrl, null, out ObjectURI);
                if (MessageSink != null)
                {
                    MessageSinks.Add( MessageSink );
                }
            }
        }
        string objectURI;
        HttpChannel HttpChannel = new HttpChannel();
        ChannelServices.RegisterChannel(HttpChannel);
        MessageSinks.Add(HttpChannel.CreateMessageSink(
                         _HttpUrl, HttpChannel, out objectURI));
        if (MessageSinks.Count > 0)
        {
            return (IMessageSink[])MessageSinks.ToArray(
                                   typeof(IMessageSink));
        }
        // Made it out of the foreach block without finding
        // a MessageSink for the URL.
        throw new 
          Exception("Unable to find a MessageSink for the URL:" +
                    _TcpUrl);
    }
}

The ChannelChangerProxy constructor expects a URL with a TCP scheme as an argument. Next we call BuildHttpUrl to change the port and scheme to the URL of the JobServer’s well-known HTTP channel. In this case, we hard-coded the HTTP port as 80, but you can read these values from a configuration file for greater flexibility. When we get a message within the Invoke method, we’ll eventually forward it to the first message sink in the chain. We’ll look at customizing message sinks in detail in Chapter 6, “Message Sinks and Contexts,” but for now, we’ll just create and use the default sinks.

In the GetMessageSinks method, we enumerate the registered channels. (In this case, we registered only the single TCP channel.) When we find the registered channel, we create the message sink that will forward messages to the given URL (in this case, tcp://JobMachine:5555/JobURI) and store it in the message sink ArrayList. Because the client knows nothing about the proxy’s intentions to use HTTP, we need to register an HTTP channel and then create and store its message sink.

Because the JobServer hosts a server-activated object, there won’t be a constructor call or any traffic to the remote server until the first method call. Our Invoke method will get its first message when the client calls the first method on JobServerLib. When this happens, the TransparentProxy calls our proxy’s RealProxy.PrivateInvoke base class method, which constructs a message and then passes it to our Invoke method. We pass the message directly to the TcpChannel’s message sink by using SyncProcessMessage. SyncProcessMessage returns an IMethodReturnMessage object that we’ll inspect to determine the success of the remote method call. On failure, the .NET Remoting infrastructure won’t throw an exception but instead will populate the Exception property of IMethod­ReturnMessage. If the server is running, if no firewall exists to prevent the TCP traffic, and if no other problems delivering the message occur, the Exception property is null and we simply return the IMethodReturnMessage from Invoke.

If an exception occurs, we retry the operation by using the HttpChannel and the HTTP-specific URL. But we have to change the __Uri property on the message to the HTTP URL before sending out the message. If this operation fails too, we return the IMethodReturnMessage so that the .NET Remoting infrastructure can throw an exception.

In this example, we used a custom proxy to equip the job assignment application to support both high-performance communications and firewall-friendly communications without requiring the client code to make this determination. Even better, the .NET Remoting infrastructure enabled us to perform the task at a high level and to avoid programming at the HTTP and TCP protocol level.

Now let’s consider a more involved example of custom proxies: using a ProxyAttribute and call context.

Load-Balancing Example

Suppose that the JobServer needs to scale to support hundreds or thousands of users and must be available at all times for these users to get their work done. The current architecture of having a single JobServer doesn’t support these requirements. Instead, we need to run redundant JobServer applications on different machines, all of which have access to the latest data for a JobClient. We also need a technique for balancing the load across the JobServers. We’ll have to make several changes to the JobServer’s design, yet by using a custom proxy, we can make the fact that the JobClient will be connecting to more than one JobServer transparent to the client code. Implementing a real-world multiserver example would require some way to share data, probably by using a database. We’ll leave that as an exercise for you to pursue on your own. Instead, this example focuses on the proxy details.

Here’s the list of tasks this example requires:

  • Construct a discovery server located at an endpoint known to the client.

  • Construct a proxy that intercepts the client’s calls and then calls the discovery server to get a list of redundant servers that we can dispatch calls to.

  • Create a separate proxy for each redundant server’s remote object.

  • In the proxy, intercept the client’s method calls and dispatch the calls to the proxies in a round-robin fashion.

  • Add call context to transparently support load balancing.

LoadBalancingServer Changes

To support discovery of other redundant servers, we’ll add those servers’ well-known URLs to the LoadBalancingServer’s configuration file, as shown here:

<configuration>
<appSettings>
    <add key="PeerUrl1" value="tcp://localhost:5556/JobURI"/>
    <add key="PeerUrl2" value="tcp://localhost:5557/JobURI"/>
    <add key="PeerUrl3" value="tcp://localhost:5558/JobURI"/>
</appSettings>
 

RemoteObject Changes

To support a discovery server JobServerImpl, we’ll implement a new interface, as follows:

public interface IDiscoveryServer
{
    string[] GetPeerServerUrls();
}

public string[] GetPeerServerUrls()
{
    ArrayList PeerUrls = new ArrayList();
    bool UrlsFound = true;
    string BaseKeyString = "PeerUrl";
    string KeyString;
    int Count = 0;
    while(UrlsFound)
    {
        KeyString = BaseKeyString + (++Count).ToString();
        string PeerUrl = ConfigurationSettings.AppSettings[KeyString];
        if (PeerUrl != null)
        {
            PeerUrls.Add(PeerUrl);
        }
        else
        {
            UrlsFound = false;
        }
    }
    return (string[])PeerUrls.ToArray( typeof(string) );
}

The intent here is to return the configured list of servers on request. The JobServer example doesn’t perform any time-consuming tasks, so we’ll add a method and simulate the load by causing the server to sleep for anywhere from 1 to 6 seconds:

public bool DoExpensiveOperation()
{
    	Random Rand = new Random();
	    int RandomNumber = Rand.Next(1000, 6000);
    System.Threading.Thread.Sleep( RandomNumber );
}

Custom Proxy Details

Instead of using a round-robin approach in which the client has to know the location of all the servers, we can use a discovery server to tell us where the other servers are located. This way, we need to publish only a single well-known discovery server URL. Although we do introduce a single point of failure at discovery time, we can add more discovery servers to get around this problem.

Adding Call Context

Now suppose that you want to add data to a method call without modifying the argument list. Many reasons for doing this exist, including the following:

  • You don’t want to change an object’s interface, possibly because of versioning reasons.

  • You don’t want the caller to know that this information is being sent, possibly for security reasons.

  • You want to send data with many varying method calls, and adding redundant arguments would pollute the method signatures. Web servers send such data by issuing cookies that browsers retain and then seamlessly piggyback on an HTTP request to the Web server. Most nonbrowser-based applications must send this cookie data as a parameter to every method call.

We could solve all these problems if we could pass a boilerplate method parameter to and from methods behind the scenes, just as browsers do with cookies. This is exactly the what call context does.

In our example, we could balance the load on the servers by using a simple round-robin scheme, but it would be better if the servers told us what their load was and sent the next message to the server that’s the least loaded. We can solve this problem very nicely by using call context.

What Is Call Context?

CallContext is an object that flows between application domains and can be inspected by various intercepting objects (such as proxies and message sinks) along the way. CallContext is also available within the client and server code, so it can be used to augment the data that’s passed between method calls—without changing the parameter list. You simply put an object into CallContext by using the SetData method and remove it by using the GetData method. To support CallContext, the contained object must be marked with the Serializable attribute and must inherit the ILogicalThreadAffinative interface. ILogicalThreadAffinative is simply a marker and doesn’t require that the object implement any methods.

Here’s the object we’ll place into CallContext:

[Serializable]
public class CallContextData : ILogicalThreadAffinative
{
    int _CurrentConnections;
    public CallContextData(int CurrentConnections)
    {
        _CurrentConnections = CurrentConnections;
    }

    public int CurrentConnections
    {
        get
        {
            return _CurrentConnections;
        }
    }
}

CallContextData is merely a wrapper around an int that allows us to send this variable via CallContext. By using CallContextData, the server will return the total number of connected clients. Our proxy will use this value to prioritize the calling order of the redundant server proxies. Of course, this simple example doesn’t really support real-world load balancing because the connection count likely won’t be up to date by the time we make the next method call. However, the example does show the power of using proxies and call context together—the client code needn’t be aware that its calls are dispatched to various servers and that additional data that isn’t part of any method’s parameter list is flowing between client and server.

Server Call Context

We add these lines to DoExpensiveWork to populate the call context with the current client connection count:

_CurrentConnections++;
 
CallContextData ccd = new CallContextData(_CurrentConnections--);
CallContext.SetData("CurrentConnections", ccd);

Then we extract the value in the client-side proxy:

object oCurrentConnections = 
  CallContext.GetData("CurrentConnections");

Proxy Changes

The following is the complete listing for our LoadBalancingProxy class:

public class LoadBalancingManager
{
    ArrayList _LoadBalancedServers = new ArrayList();
    public class LoadBalancedServer
    {
        public MarshalByRefObject Proxy;
        public int CurrentConnections;
        public bool IsActive;
    }

    public void AddProxy(MarshalByRefObject Proxy)
    {
        LoadBalancedServer lbs = new LoadBalancedServer();
        lbs.Proxy = Proxy;
        lbs.CurrentConnections = 0;
        lbs.IsActive = true;
        _LoadBalancedServers.Add(lbs);
    }

    public void SetCurrentConnections(MarshalByRefObject Proxy,
                                      int CurrentConnections)
    {
        foreach (LoadBalancedServer lbs in _LoadBalancedServers)
        {
            if (lbs.Proxy == Proxy)
            {
                lbs.CurrentConnections = CurrentConnections;
                return;
            }
        }
    }

    public MarshalByRefObject GetLeastLoadedServer()
    {
        LoadBalancedServer LeastLoadedServer = null;
        foreach (LoadBalancedServer CurrentServer
                 in _LoadBalancedServers)
        {
            if (LeastLoadedServer == null)
            {
                LeastLoadedServer = CurrentServer;
                continue;
            }
            if (CurrentServer.CurrentConnections < 
                LeastLoadedServer.CurrentConnections)
            {
                LeastLoadedServer = CurrentServer;
            }
        }

        if (LeastLoadedServer == null)
        {
            return null;
        }
        else
        {
            return LeastLoadedServer.Proxy;
        }
    }
}

public class LoadBalancingProxy : RealProxy
{
    LoadBalancingManager LoadManager;
    int MaxProxies;

    public LoadBalancingProxy (Type type, string DiscoveryUrl)
    : base(type)
    {
        MarshalByRefObject ftp = 
          (MarshalByRefObject)RemotingServices.Connect(type,
                                                       DiscoveryUrl,
                                                       null);
        IDiscoveryServer discoveryServer = (IDiscoveryServer)ftp;
        string[] PeerUrls = discoveryServer.GetPeerServerUrls();
        LoadManager = new LoadBalancingManager();
        foreach( string Url in PeerUrls)
        {
            MarshalByRefObject PeerServer = 
              (MarshalByRefObject)RemotingServices.Connect(type, Url,
                                                           null);
            LoadManager.AddProxy(PeerServer);
        }
    }

    public override IMessage Invoke(IMessage msg)
    {
        MarshalByRefObject CurrentServer = 
          LoadManager.GetLeastLoadedServer();
        if (CurrentServer == null)
        {
            throw new Exception(
              "No remote servers are responding at this time.");
        }
        RealProxy rp = RemotingServices.GetRealProxy(CurrentServer);
        IMessage retMsg = rp.Invoke(msg);
        if (retMsg is IMethodReturnMessage)
        {
            IMethodReturnMessage mrm = (IMethodReturnMessage)retMsg;
            if (mrm.Exception == null)
            {
                // Check for call context.
                object oCurrentConnections = 
                  CallContext.GetData("CurrentConnections");
                if (oCurrentConnections == null)
                {
                    // Because the server didn't send back any load
                    // information, consider it least loaded.
                    LoadManager.SetCurrentConnections( CurrentServer,
                                                       0);
                }
                else if (oCurrentConnections is CallContextData)
                {
                LoadManager.SetCurrentConnections( CurrentServer, 
                  ((CallContextData)oCurrentConnections)
                   .CurrentConnections);
                }
                return retMsg;
            }
        }
        return retMsg;
    }
}

We’ve added a LoadBalancingManager class to control the details of our load-balancing algorithm. This class’s primary job is to maintain a prioritized list of proxies based on the smallest client connection load. Our custom proxy will call GetLeastLoadedServer to dispatch each new method call.

Call Context Message Flow

Call context data is piggybacked on a method call but isn’t necessarily related to the specific method. In accordance, the .NET Remoting infrastructure sends our CallContextData object via the SOAP header rather than the SOAP Body element. Here’s the response SOAP message for the DoExpensiveWork method call:

<SOAP-ENV:Header>
   <h4:__CallContext href="#ref-3" 
     xmlns:h4="http://schemas.microsoft.com/
     clr/soap/messageProperties" SOAP-ENC:root="1">
   <a1:LogicalCallContext  
     xmlns:a1="http://schemas.microsoft.com/clr/ns/
     System.Runtime.Remoting.Messaging">
      <CurrentConnections href="#ref-6"/>
   </a1:LogicalCallContext>
   <a2:CallContextData  
     xmlns:a2="http://schemas.microsoft.com/clr/nsassem/
     LoadBalancing/DiscoveryServerLib%2C%20
     Version%3D1.0.871.14847%2C%20Culture%3Dneutral%2C%20
     PublicKeyToken%3Dnull">
      <_CurrentConnections>1</_CurrentConnections>
   </a2:CallContextData>
</SOAP-ENV:Header>

<SOAP-ENV:Body>
   <i7:DoExpensiveOperationResponse  
     xmlns:i7="http://schemas.microsoft.com/clr/nsassem/
     LoadBalancing.IJobServer/LoadBalancingLib">      
      <return>true</return>   
   </i7:DoExpensiveOperationResponse>
</SOAP-ENV:Body>