Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

.NET Remoting and Cross Domain Marshalling

0.00/5 (No votes)
10 Apr 2006 1  
An article about how to marshal a remote client request from one AppDomain to another.

Introduction

Consider writing a .NET remoting based server application that needs to serve every client session in a separate AppDomain. You could take a simple and straightforward approach by allocating multiple AppDomains and configuring each AppDomain for .NET remoting. Each AppDomain, however, must be configured so that the server's listening channels are assigned a different port. TCP will not permit more than one listener per port.

On the client side, every application must know which endpoint (hostname:port) to address for a session to be established in a designated AppDomain. This concept of binding a session to a designated AppDomain and directing the client application via a specific endpoint there can certainly be realized, but it is neither practical nor conventional. A client typically requests to be served at a commonly known endpoint, irregardless in which AppDomain the server wishes to host the session.

This article is about hosting client sessions in separate AppDomains but allowing all clients to connect to a commonly known endpoint. The idea is that the server receives a client request in the default AppDomain but then intercepts and forwards it on to a different AppDomain.

How to intercept and marshal a client's request from one AppDomain to another

A remote method invocation is all about Message objects passing through a chain of sinks from the client's end to the server's end. A method call is transformed into a serializable object, passed into a sink chain on the client side, serialized into a stream, and transmitted across the network. On the server's side, it is received, deserialized into the Message object, and passed up a sink chain to be finally reconstructed into a call to the service. We want to intercept the Message object before it reaches the point of reconstruction into the proper method call. Once intercepted, we can redirect the Message object to the appropriate AppDomain where the reconstruction to the final method call will take place.

Fortunately, the .NET remoting infrastructure provides us with a facility to make these interceptions. The key here is the ContextBoundObject. Instead of deriving a remote service object from MarshalByRefObject, you need to derive from ContextBoundObject.

Of course, the clients can never directly connect to the actual service objects that are instantiated in separate AppDomains. The object they will connect to must be some sort of a cross domain marshaller that can intercept a method call and forward it to the appropriate AppDomain. A CrossDomainMarshaller that is a ContextBoundObject must be installed as an interceptor.

// this define the interceptor

[CrossDomainContextAttribute]
class CrossDomainMarshaller : ContextBoundObject
{
    // code ommitted for brevity

}

// this installs the interceptor in the server's default AppDomain

CrossDomainMarshaller marshaller = new CrossDomainMarshaller();
RemotingServices.Marshal(marshaller, "PrintService.rem");

The CrossDomainMarshaller assumes the object URI "PrintService.rem" of the actual service that the clients are interested in. The connection code on the the client side would appear normal, like so:

string url = "tcp://myhost:1234/PrintService.rem"
IPrintService service = 
  (IPrintService)Activator.GetObject(typeof(IPrintService), url);
service.PrintMessage("Hello World");

The CrossDomainMarshaller does not implement the IPrintService interface at all. So, how can this possibly work? The answer is with the ContextBoundObject. I will not explain how to implement a ContextBoundObjet but refer you instead to MSDN or an excellent book about .NET Remoting, such as "Advanced .NET Remoting", written by Ingo Rammer et all, and published by Apress. This article also includes source code that you may download and inspect.

The important matter is that implementing a CrossDomainMarshaller as a ContextBoundObject requires us also to implement a MessageSink object. That offers us the opportunity to intercept the Message before it enters the point of reconstruction to the ultimate method call.

class MessageSink : IMessageSink
{
    // other methods ommitted for brevity

    
    // here is where we must intercept redirect the client's method call

    public IMessage SyncProcessMessage(IMessage msg)
    {
        // if we just let this stand the msg object will just be

        // dispatched to the CrossDomainMarshaller

        // which does not have an implementation

        // of the IPrintService

        return this.nextSink.SyncProcessMessage(msg);    
    }
}

As I have earlier mentioned, a Message travels along a chain of message sinks until it arrives on the server's side to be converted into a method call. As the code snippet indicates, the method invocation is bound to the IPrintService interface and the CrossDomainMarshaller does not implement one. So, passing it off to the next sink will result in failure. We need to redirect the call to the appropriate service object in the appropriate AppDomain.

The problem is one of finding the correct AppDomain for the presently calling client. The CrossDomainMarshaller intercepts all messages send by all clients but we have no means to identify each client. The Message object contains no particular information as to the client's identity. To improve upon this, we need to engage the client into cooperation. The client agrees to connect via a CustomProxy. Here is how the client code would look like.

string url = "tcp://myhost:1234/PrintService.rem"
// IPrintService service = 

//  (IPrintService)Activator.GetObject(typeof(IPrintService), url);

IPrintService service = (IPrintService)new CustomProxy(url, 
                         typeof(IPrintService)).GetTransparentProxy();
service.PrintMessage("Hello World");

This is not an intolerable imposition. But the client must implement a CutomProxy that must look like so:

class CustomProxy : RealProxy
{
    string url;
    string clientID;
    IMessageSink messageSink;

    public CustomProxy(string url, Type type) : base(type)
    {
        this.url = url;
        // create a unique client identifier

        this.clientID = Guid.NewGuid().ToString();

        foreach (IChannel channel in ChannelServices.RegisteredChannels)
        {
            if (channel is IChannelSender)
            {
                IChannelSender sender = (IChannelSender)channel;
                if (string.Compare(sender.ChannelName, "tcp") == 0)
                {
                    string objectUri;
                    this.messageSink = 
                         sender.CreateMessageSink(this.url, 
                         null, out objectUri);
                    if (this.messageSink != null)
                        break;
                }
            }
        }

        if (this.messageSink == null)
            throw new Exception("No channel found for " + this.url);
    }

    public override IMessage Invoke(IMessage msg)
    {
        msg.Properties["__Uri"] = this.url;
        
        // pass the client's id as part of the call context

        LogicalCallContext callContext = 
                 (LogicalCallContext)msg.Properties["__CallContext"];
        callContext.SetData("__ClientID", this.clientID);
        
        return this.messageSink.SyncProcessMessage(msg);
    }
}

The CustomProxy allows to create, and then transmit, a unique client identifier with every method call. The Message object allows to pass along additional out of band data as part of its LogicalCallContext, providing the means to identify each client on the server's side.

class MessageSink : IMessageSink
{
    // other methods ommitted for brevity

    
    // here is where we must intercept redirect the client's method call

    public IMessage SyncProcessMessage(IMessage msg)
    {
        LogicalCallContext callContext = 
              (LogicalCallContext)msg.Properties["__CallContext"];
        string clientID = (string)callContext.GetData("__ClientID");
        if(clientID != null)
            return CrossDomainMarshaller.GetService(clientID).Marshal(msg);
        else
            return new ReturnMessage(new ApplicationException("No __ClientID"), 
                       (IMethodCallMessage)msg);
    }
}

The line return CrossDomainMarshaller.GetService(clientID).Marshal(msg); is a short way of saying 'get the correct service based on the client identifier and invoke its Marshal method'. Let us look at the implementation of CrossDomainMarshaller.GetService.

[CrossDomainContextAttribute]
class CrossDomainMarshaller : ContextBoundObject
{
    public static Dictionary<string, ICrossDomainService> 
           Dictionary = new Dictionary<string, ICrossDomainService>();

    public static ICrossDomainService GetService(string clientID)
    {
        // we created an AppDomain per client only once

        if (Dictionary.ContainsKey(clientID))
            return Dictionary[clientID];

        AppDomain appDomain = AppDomain.CreateDomain(clientID);
        ICrossDomainService service = 
            (ICrossDomainService)appDomain.CreateInstanceAndUnwrap(
            "ContextBoundRemoting.Service",
            "ContextBoundRemoting.PrintService");

        Dictionary.Add(clientID, service);

        return service;
    }
}

You can readily see that we create an AppDomain per client only once, and not per every call. We, then, instantiate the PrintService in that AppDomain and then store it so it can be retrieved for any subsequent call. What we return, however, is not some interface to a PrintService but an ICrossAppDomainService interface which allows us to marshal the Message across the AppDomain. So, that PrintService also needs to implement the ICrossDomainService interface. The remaining and interesting question is how passing the Message object to the PrintService can possibly result in a proper method call on the PrintService. The short answer lies with another CustomProxy. Take a quick look at the complete implementation of the PrintService class:

public class PrintService :
    CrossDomainService,
    IPrintService
{
    public string PrintMessage(string msg)
    {
        Console.WriteLine("{0} in AppDomain {1}", msg, 
                          AppDomain.CurrentDomain.FriendlyName);
        return "Ok " + msg;
    }
}

You can see that the PrintService class extends the CrossDomainService class. The secret of converting the Message to a proper call on the PrintService is hidden there. Examine the CrossDomainService:

public class CrossDomainService :
       MarshalByRefObject,
       ICrossDomainService
{
    class Proxy : RealProxy
    {
        string uri;

        public Proxy(MarshalByRefObject obj)
        {
            this.uri = RemotingServices.Marshal(obj).URI;
        }
        public override IMessage Invoke(IMessage msg)
        {
            msg.Properties["__Uri"] = this.uri;
            return ChannelServices.SyncDispatchMessage(msg);
        }
    }

    Proxy proxy;

    public CrossDomainService()
    {
        this.proxy = new Proxy(this);
    }
    
    public IMessage Marshal(IMessage msg)
    {
        return this.proxy.Invoke(msg);
    }
}

The CrossDomainService implements the ICrossDomainService interface method Marshal(IMessage msg). The Message object is forwarded into a custom proxy which finally resolves the call to IPrintService.PrintMessage. The implementation of the custom proxy is not that complicated.

Conclusion

The ContextBoundObject is a rather powerful means to intercept method calls. If you are interested in application server technologies and ever wanted to build an application server yourself, then you are in luck. Application servers are all about intercepting and interfering with method calls.

  • You can provide your own authentication and authorisation scheme.
  • You can rewrite the Message object to redirect the call into an alternate object/method. This is one useful technique if you ever re-deploy your server but cannot update your clients with the new interface definitions that are normally kept in a shared assembly.
  • You can selectively apply encryption/decryption to the granularity of a single method parameter.
  • You can monitor anything you consider worthwhile by counting and timing a method call.

The list of possibilities is limited only by your imagination. I hope that this article meets your appreciation.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here