Click here to Skip to main content
15,884,836 members
Articles / Programming Languages / C#

.NET Remoting and Cross Domain Marshalling

Rate me:
Please Sign up or sign in to vote.
4.80/5 (11 votes)
10 Apr 2006CPOL6 min read 88.9K   1.3K   59  
An article about how to marshal a remote client request from one AppDomain to another.
<!--------------------------------------------------------------------------->  
<!--                           INTRODUCTION                                

 The Code Project article submission template (HTML version)

Using this template will help us post your article sooner. To use, just 
follow the 3 easy steps below:
 
     1. Fill in the article description details
     2. Add links to your images and downloads
     3. Include the main article text

That's all there is to it! All formatting will be done by our submission
scripts and style sheets. 

-->  
<!--------------------------------------------------------------------------->  
<!--                        IGNORE THIS SECTION                            -->
<html>
<head>
<title>The Code Project</title>
<Style>
BODY, P, TD { font-family: Verdana, Arial, Helvetica, sans-serif; font-size: 10pt }
H2,H3,H4,H5 { color: #ff9900; font-weight: bold; }
H2 { font-size: 13pt; }
H3 { font-size: 12pt; }
H4 { font-size: 10pt; color: black; }
PRE { BACKGROUND-COLOR: #FBEDBB; FONT-FAMILY: "Courier New", Courier, mono; WHITE-SPACE: pre; }
CODE { COLOR: #990000; FONT-FAMILY: "Courier New", Courier, mono; }
</style>
<link rel="stylesheet" type=text/css href="http://www.codeproject.com/styles/global.css">
</head>
<body bgcolor="#FFFFFF" color=#000000>
<!--------------------------------------------------------------------------->  


<!-------------------------------     STEP 1      --------------------------->
<!--  Fill in the details (CodeProject will reformat this section for you) -->

<pre>Title:       .NET Remoting and cross domain marshalling
Author:      Wytek Szymanski 
Email:       wytek@szymanski.com
Environment: .NET, Visual Studio 2005
Keywords:    .NET remoting, AppDomain, ContextBound objects
Level:       advanced&quot;
Description: An article abou how to marshal a remote client request from one AppDomain to another.

</pre>

<!-------------------------------     STEP 2      --------------------------->
<!--  Include download and sample image information.                       --> 

<ul class=download>
<li><a href="Article_src.zip">Download source - XXX Kb</a></li>
</ul>



<!-------------------------------     STEP 3      --------------------------->
<!--  Add the article text. Please use simple formatting (<h2>, <p> etc)   --> 

<h2>Introduction</h2>
<p>
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 straight forward 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.
</p>
<p>
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.</p>
<p>
This article is about hosting client sessions in separate AppDomains but allowing
    all clients 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.
</p>
<h2>How to intercept and marshal a client's request from one AppDomain to another</h2>
<p>
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 accross the network. On the server's side it is received, deserialized into the a 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.
</p>
<p>
Fortunately, the .NET remoting infrastructure provides us with a facility to make these interception. The key here is the <code>ContextBoundObject</code>. Instead of deriving
a remote service object from <code>MarshalByRefObject</code> you need to derive&nbsp;
    from ContextBoundObject.
</p>
<p>
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 <code>CrossDomainMarshaller</code> that is a ContextBoundObject must be installed as an 
interceptor. 
</p>

<pre>// 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");

</pre>

<p>
The CrossDomainMarshaller assumes the object uri <code>"PrintService.rem"</code>
    of the actual service that the clients are interested in. The connection code on
    the the client side would appear normal, like so.
</p>
<pre>string url = "tcp://myhost:1234/PrintService.rem"
IPrintService service = (IPrintService)Activator.GetObject(typeof(IPrintService), url);
service.PrintMessage("Hello World");

</pre>

<p>
The CrossDomainMarshaller does not implement the <code>IPrintService</code> 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.
</p>
<p>
The important matter is that implementing a CrossDomainMarshaller as a ContextBoundObject requires us also to implement 
a <code>MessageSink</code> object. That offers us
    the opportunity to intercept the Message before it enters the point of reconstruction
    to the ultimate method call.&nbsp;</p>
<pre>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);    
    }
}
</pre>

<p>
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.
</p>
<p>
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 o need to engage the client
into cooperation. The client agrees to connect via a <code>CustomProxy</code>. Here is how the client code would look like.
</p>
<pre>string url = "tcp://myhost:1234/PrintService.rem"
// IPrintService service = (IPrintService)Activator.GetObject(typeof(IPrintService), url);
IPrintService service = (IPrintService)<b>new CustomProxy(url, typeof(IPrintService)).GetTransparentProxy()</b>;
service.PrintMessage("Hello World");

</pre>

<p>
This is not an intolerable imposition. But the client must implement a <code>CutomProxy</code> that must look like so.
</p>
<pre>class CustomProxy : RealProxy
{
    string url;
    <b>string clientID;</b>
    IMessageSink messageSink;

    public CustomProxy(string url, Type type) : base(type)
    {
        this.url = url;
        // create a unique client identifier
        <b>this.clientID = Guid.NewGuid().ToString();</b>

        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
        <b>LogicalCallContext callContext = (LogicalCallContext)msg.Properties["__CallContext"];
        callContext.SetData("__ClientID", this.clientID);</b>
        
        return this.messageSink.SyncProcessMessage(msg);
    }
}

</pre>
<p>
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 <code>LogicalCallContext</code>,
    providing the means to identify each client on the server's side.
</p>
<pre>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)
            <b>return CrossDomainMarshaller.GetService(clientID).Marshal(msg);</b>
        else
            return new ReturnMessage(new ApplicationException("No __ClientID"), (IMethodCallMessage)msg);
    }
}

</pre>
<p>
The line
<code>return CrossDomainMarshaller.GetService(clientID).Marshal(msg);</code> is a short way of saying get the
correct service based on the client identifier and invoke its <code>Marshal</code> method.
Let us look at the implementation of <code>CrossDomainMarshaller.GetService</code>.
</p>
<pre>[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;
    }
}
</pre>
    
<p>
You can readily see that we create an AppDomain per client only once, and not per every call.
We, than instantiate the PrintService in that AppDomain and than store it so it can be retrieved for any subsequent call.
What we return, however, is not
    some interface to a PrintService but an
 <code>ICrossAppDomainService</code> interface which allows us to
  marshal the <code>Message</code> acrros the AppDomain. So, that <code>PrintService</code> also needs
to implement the <code>ICrossDomainService</code> 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.
</p>

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

</pre>

<p>
You can see that the <code>PrintService</code> class extends the <code>CrossDomainService</code> class.
The secrete of converting the Message to a proper call on the PrintService
    is hidden there. Examine the CrossDomainService.</p>

<pre>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()
    {
        <b>this.proxy = new Proxy(this);</b>
    }
    <b>
    public IMessage Marshal(IMessage msg)
    {
        return this.proxy.Invoke(msg);
    }</b>
}

</pre>

<p>
The CrossDomainService implements the ICrossDomainService interface method
 <code>Marshal(IMessage msg)</code>. The Message object is forwarded into a custom proxy which
 finally resolves the call to <code>IPrintService.PrintMessage</code>. The implementation of the custom proxy is not that 
 complicated.</p>
 <h2>Conclusion</h2>
<p>
The ContextBoundObject is rather powerful means to intercept method calls. If you are interested in application
server technologies and ever wanted to build an application server yourself than you are in luck. Application servers are all about intercepting
    and interfering with method calls.</p>
<p>
</p> 
<li>
You can provide your own authentication and authorisation scheme.<br />
</li>
<li>
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. <br />
</li>
<li>
You can selectively apply encryption/decryption to the granularity of a single method parameter.<br /> 
</li>
<li>
You can monitor anything you consider worthwile by counting and timing a method call.<br />
</li>
<p>
The list of possibilities is limited only by your imagination. I hope that this article meets your appreciation.</p>
</body>
</html>

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
United States United States
I am a consultant, trainer, software archtect/engineer, since the early 1980s, working in the greater area of Boston, MA, USA.

My work comprises the entire spectrum of software, shrink-wrapped applications, IT client-server, systems and protocol related work, compilers and operating systems, and more ....

I am currently focused on platform development for distributed computing in service oriented data centers.

Comments and Discussions