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

.NET Remoting Customization Made Easy: Custom Sinks

0.00/5 (No votes)
20 May 2003 3  
.NET Remoting customization � it doesn't have to be so hard!

Introduction

Isn�t .NET development easy? I, for one, believe that our life as developers is simpler with the .NET framework comparing to prior technologies such as COM / DCOM under C++. .NET Remoting is, without a doubt, an excellent example for this simplicity. After reading a comprehensive article or two chapters of your favorite remoting book, you can immediately begin creating powerful distributed applications. .NET Remoting also offers outstanding flexibility and various customization options. However, this is where simplicity ends. In my opinion, .NET Remoting is very easy to use but not all that easy to customize. To do so, you must be familiar with the inner plumbing of the .NET Remoting infrastructure and are required to write numerous lines of code you couldn�t care less about. Don�t get me wrong. I do believe that a more aware developer, one that understands the underlying development infrastructure, is essentially a better developer. I just don�t think that it means understanding and implementing every little detail as .NET Remoting customizations sometimes require. In this article I would like to present a small library, which simplifies one the most important aspects of the .NET Remoting customization: Custom Sinks.

Don�t be intimidated by the length of this article. Before you read half of it, you�ll be on your way with everything you need to create your own custom sinks within minutes. The rest is extra, more advanced features.

If you already know everything about custom sinks and can�t wait to start implementing, feel free to jump to the Basic Custom Sinks section. In any case, please make sure to read the disclaimer at the very bottom of this article.

What are Sinks?

When you work with a remote object, you do not hold a reference to that object, but a reference to a proxy. The proxy is an object that looks and feels exactly like the remote object and can convert your stack based method calls into messages, and send them to the remote object. In order to send a message to the remote object, the proxy uses a chain of sinks. It calls the first sink in the chain and provides it with the message. The sink optionally modifies the message, and passes it to the next sink, and so on. One of the sinks along the way is a formatter sink. The task of this special sink is to serialize the message into a stream. Sinks after the formatter sink operate on the stream, because at this point the message is no longer relevant (and is provided to the sinks as information only). The last sink in the stream is the transport sink, which is in charge of sending the data to the server and waiting for a response. When response arrives, the transport sink returns it to the previous sink and the response starts finding its way back to the proxy. Along the way, the response goes through the formatter sink, which deserializes the response back into a response message.

What happens on the server side? You guessed right. The server also holds a chain of sinks. This time the chain leads to the target object. The first sink is the transport sink. Along the way lies the formatter sink and finally there is the stack builder which does exactly the opposite of the client-side proxy. It converts the message into a stack based method call to the target object. When the target object�s method returns, information (return value, ref parameters, etc) is packed into a message, which is returned back through the same sink chain starting with the stack builder and ending with the transport sink.

Sample screenshot

In reality there are more sinks than the ones in the above figure, but I wanted to keep things simple and chose to show only the ones relevant to the current discussion. Otherwise I would miss the entire point of this article, wouldn�t I?

Custom Sinks

As shown in the above figure, it is possible to add custom sinks to the chain, both on the client side and server side. So when do we decide to develop our own custom sink? We usually do it when we want to inspect or modify the data sent from the proxy to the remote object and / or the data returned from the remote object back to the proxy.

Let�s say that you want to encrypt the information sent over the wire between the client and the server. To do so, you can create a client-side custom sink that encrypts outgoing request data and decrypts incoming response data. You should also create a server-side custom sink that decrypts request data arriving from the client and encrypts response data sent back to the client.

Custom sinks can be placed either before or after the formatter sink, depending on whether they are designed to manipulate the message or the serialized stream. The encryption custom sink would want to work on the stream (it doesn�t care about the logical meaning of the message; it just needs to scramble it). Therefore the client-side custom sink should be situated after the formatter sink (after the message is serialized to a stream) whilst the server-side custom sink should be situated before the formatter sink (before the stream is desterilized back to a message).

As another example, let�s say you want the client to send username and password information and the server, which should examine them before allowing access to the target object. You could add the username and password as parameters to every method of the target object. This would effectively add the required information to the message, but would be rather cumbersome. As an alternative, you can create a client-side custom sink that adds the username and password to every outgoing message and a server-side custom sink that retrieves this information and throws an exception (which will be propagated back to the client) if the username and / or password are invalid. Since these custom sinks work on the message, rather than on the serialized stream, the client-side custom sink should be placed before the formatter sink (before the message is serialized to a stream) and the server-side custom sink should be placed after the formatter sink (after the stream is desterilized back to a message).

Cool! How do I do it?

Here is the catch. In order to implement your own custom sink, you will work hard! You must define a class that implements at least one of the IMessageSink, IClientChannelSink and IServerChannelSink interfaces, depending on whether you define a client-side or server-side custom sink and whether your sink will be situated before or after the formatter sink. You should make sure to forward every call to the next sink. You must implement different logic for synchronous and asynchronous calls. Furthermore, the way asynchronous calls are handled is a completely different story for each of the above three interfaces. Wait! There is more. As a dessert you should also define a Sink Provider class that implements another interface (IClientChannelSinkProvider or IServerChannelSinkProvider) and is able to create instances of your custom sink upon request from the .NET Remoting infrastructure.

These tasks are surely doable, but are quite tedious, time-consuming and error-prone. When I first realized all I had to do here, I asked myself � couldn�t they provide a base class that takes care of all of these details? Can�t I handle only my own business logic by implementing only the relevant parts of the custom sink? Well, I didn�t find such a class in the class library, so I decided to write one on my own. Admittedly, this class does not cover every custom sink scenario. By simplifying things, you sometimes loose some flexibility. However, I believe that the class is valid for most real-world custom sink scenarios. The class BaseCustomSink and friends are contained in the CustomSink class library. You can download the library as well as sample derived custom sinks and sample client and server with full source code.

Features

The BaseCustomSink class, as its name implies, is a base class for custom sinks.

Main features:

  • Supports client-side and server-side sinks.
  • Supports synchronous and asynchronous calls.
  • Can be safely used in multithreaded applications.
  • Automatic reading of data from configuration file.
  • Allows derived classes to decide on runtime whether or not they want to be added to the sink chain.
  • Calls a static initialization method of the derived class, if present.

These features will be further described and demonstrated in the following sections.

Basic Custom Sinks

In order to demonstrate the use of the CustomSink library, let�s implement encryption client / server sinks. The source code for these sinks can be found in the accompanying SampleSinks class library. Since I would like to concentrate in implementing the sinks, rather than delve into cryptography, I will show the implementation of LameEncpriptionClientSink and LameEncryptionServerSink, which simply add / subtract a delta from every byte in the stream, as a simulation of encryption. In the future I may publish real-world encryption sinks, based on the library presented here.

Before implementing the sinks, let�s create a helper class LameEncryptionHelper that will handle the actual encryption. It will have a constructor that accepts the delta value, and two methods: Encrypt and Decrypt. The Encrypt method looks as follows:

public Stream Encrypt(Stream source)
{
      byte tempByteData;
      int tempIntData;
      MemoryStream encrypted = new MemoryStream();
      while ((tempIntData = source.ReadByte()) != -1)
      {
            tempByteData = (byte)tempIntData;
            tempByteData += this.delta;
            encrypted.WriteByte(tempByteData);              
      }
      encrypted.Position = 0;
      return encrypted;
}

The Decrypt method is almost identical, with one difference � it simply subtracts the delta instead of adding it.

The BaseCustomSink class contains two virtual methods, ProcessRequest and ProcessResponse. The ProcessRequest method is called when request data is on its way to the target object and the ProcessResponse method is called when the response data is on its way back to the proxy. You may override these methods in order to add your own processing.


protected virtual void ProcessRequest(
      IMessage message, 
      ITransportHeaders headers, 
      ref Stream stream, 
      ref object state)

protected virtual void ProcessResponse(
      IMessage message, 
      ITransportHeaders headers, 
      ref Stream stream, 
      object state)

As you can see, the parameter list of both methods is almost identical.

  • message � this is the message being transferred.
  • headers � created by the formatter sink and allows adding logical information after the message has been serialized.
  • stream � the stream that contains the serialized message. We can modify the stream or assign a new stream.
  • state � in the ProcessRequest we may assign the state to any object that we later need in the ProcessResponse.

Examine the implementation of these methods for the LameEncryptionClientSink:

protected override void ProcessRequest(
      IMessage message, 
      ITransportHeaders headers, 
      ref Stream stream, 
      ref object state)
{
      stream = this.encryptionHelper.Encrypt(stream);
      headers["LamelyEncrypted"] = "Yes";
}

protected override void ProcessResponse(
      IMessage message, 
      ITransportHeaders headers, 
      ref Stream stream, 
      object state)
{
      if (headers["LamelyEncrypted"] != null)
      {
            stream = this.encryptionHelper.Decrypt(stream);
      }
}

Both methods use a member field of the type LameEncryptionHelper in order to perform the actual encryption. The ProcessRequest also adds information to the headers, specifying that the stream was lamely encrypted. Here is the implementation for the LameEncryptionServerSink:


protected override void ProcessRequest(
      IMessage message, 
      ITransportHeaders headers, 
      ref Stream stream, 
      ref object state)
{
      if (headers["LamelyEncrypted"] != null)
      {
            stream = this.encryptionHelper.Decrypt(stream);
            state = true;
      }
}

protected override void ProcessResponse(
      IMessage message, 
      ITransportHeaders headers, 
      ref Stream stream, 
      object state)
{
      if (state != null)
      {
            stream = this.encryptionHelper.Encrypt(stream);
            headers["LamelyEncrypted"] = "Yes";
      }
}

The code for the server sink is designed to be capable of working with any client, regardless of whether they use the LameEncryptionClientSink. The ProcessRequest method therefore examines the headers to determine whether the stream was lamely encrypted. In such case the method performs two things: it decrypts the method and assigns true to the state, in order to signal the ProcessResponse to encrypt the response. This way, only clients that sent encrypted request streams will receive encrypted response streams. The ProcessResponse method examines the state object to see whether it was assigned (the actual value doesn�t matter since it can only be true or null), and if it was, encrypts the response stream.

That�s it! Our custom sinks are now ready to be used. I will demonstrate how the client and server utilize the sinks using configuration files. Before I do that, I must say a word about providers. The .NET Remoting infrastructure does not directly create custom sinks. Instead it creates a sink provider class, which is able to create the custom sinks upon demand. The CustomSinks class library contains two providers: CustomClientSinkProvider and CustomServerSinkProvider, which are able to provide BaseCustomSink derived classes. You don�t need to worry about these classes. Simply specify the appropriate one (client or server) in the configuration file.

filename: Basic_SampleClient.exe.config

<configuration>
  <system.runtime.remoting>
    <application>       
      <channels>
        <channel ref="http">
          <clientProviders>                                             
            <formatter ref="soap" />
            <provider 
      type="CustomSinks.CustomClientSinkProvider, CustomSinks"
      customSinkType="LameEncryption.LameEncryptionClientSink, SampleSinks" />
          </clientProviders>                    
        </channel>                        
      </channels>             
    </application>
  </system.runtime.remoting>  
</configuration>

filename: Basic_SampleServer.exe.config

<configuration>
  <system.runtime.remoting>
    <application>
      <channels>
        <channel ref="http" port="7878">
          <serverProviders>                                 
            <provider 
       type="CustomSinks.CustomServerSinkProvider, CustomSinks"
       customSinkType="LameEncryption.LameEncryptionServerSink, SampleSinks" />
            <formatter ref="soap" />
          </serverProviders>
        </channel>
      </channels>             
    </application>
  </system.runtime.remoting>
</configuration>

As shown above, the provider is given the custom sink type using the customSinkType attribute. The type is specified in the format �namespace.class, assembly�. In my sample, the custom sink classes reside in a namespace named LameEncryption in an assembly named SampleSinks. The client and server applications will invoke RemotingConfiguration.Configure and pass the configuration file name as a parameter. Note that the sinks appear after the formatter in client configuration file, and before the formatter in the server configuration file. This is crucial for the sinks to function properly, since they must operate on the stream.

NOTE: I used an HTTP channel in my example, but you can easily switch to TCP.

You may now run the provided sample client and server application to see the custom sinks in action. Actually you won�t see much, as the encryption will be done unnoticeably (after all, that�s the whole point). However, I added some console outputs, which will prove that the sinks actually work...

In this section, I demonstrated the relative ease of creating simple custom sinks using the CustomSinks library. After having simplified things, I feel like complicating them a bit... For many custom sinks, the basic features described in this section will suffice. However, to avoid resorting to �manual� implementation of custom sinks when we need just a little bit more functionality, I added some more advanced features into the CustomSink library. You may stop here if that�s all you need and refer to this article in the future.

In the following sections I will further develop the Lame Encryption sinks, to demonstrate additional features of the CustomSinks library. Since I want to leave the simplest sample intact, any further developments will be incorporated into EnhancedLameEncryptionServerSink and EnhancedLameEncryptionClientSink (as if the original names weren�t long enough...). If you wish to test the enhanced sinks, make sure to open SampleClient.cs and SampleServer.cs and uncomment the relevant lines.

Accessing Configuration Data

To design more general custom sinks, we may sometimes need to access data from a configuration file. For example our Lame Encryption sinks may want to read the delta value (the value added / subtracted to / from every byte in the stream). This can be easily accomplished when deriving a sink from BaseCustomSink. If the configuration file contains a customData element under the provider element, the custom sink will be able to retrieve it through its constructor. Let�s review the following excerpt of the modified client and server configuration files (for the sake of brevity, I present here only the clientProviders and serverProviders elements):

Enhanced_SampleClient.exe.config

<clientProviders>
  <formatter ref="soap" />
  <provider type="CustomSinks.CustomClientSinkProvider, CustomSinks"
 customSinkType="LameEncryption.EnhancedLameEncryptionClientSink, SampleSinks">
    <customData delta = "15" /> 
</provider>                                                             
</clientProviders>                        

Enhanced_SampleServer.exe.config

<serverProviders>                               
  <provider type="CustomSinks.CustomServerSinkProvider, CustomSinks"
customSinkType="LameEncryption.EnhancedLameEncryptionServerSink, SampleSinks">
    <customData delta = "15" />     
  </provider>
  <formatter ref="soap" />
</serverProviders>

In order to retrieve this data, the sink should have a constructor that accepts one parameter of the type SinkCreationData. In such case, this constructor will be called and be provided with the customData element through this parameter. Note that given the presence of such constructor, the parameterless constructor will never be called (even if there is no customData element for the appropriate sink).

Let�s review the constructor of the modified custom client sink (EnhancedLameClientEncryptionSink). The server sink�s constructor is identical.

public EnhancedLameEncryptionClientSink(SinkCreationData creationData)
{
      byte delta = 1;
      if (creationData.ConfigurationData.Properties["delta"] != null)
      {
            delta = byte.Parse(
              creationData.ConfigurationData.Properties["delta"].ToString());
      }
      this.encryptionHelper = new LameEncryptionHelper(delta);
}

Self Exclusion from Sink Chain

One of the main differences between client and server sinks is the timing of their creation. Server sinks are created once, when the channel is configured. Client sinks are created every time a new proxy is created (every proxy may have a different chain of sinks). Thus, client sinks may be created multiple times.

Your custom sink (either client-side or server-side) can prevent its addition to the sink chain in runtime. If for some reason (after inspecting the customData for an instance), your custom sink decides that it shouldn�t be a part of the chain after all, it may throw an ExcludeMeException from its constructor. The provider will catch this exception and act accordingly. Furthermore, if your custom sink decides that it should never be created again, it can be more specific. Instead of throwing the ExcludeMeException every time its constructor is called, it can provide true to the constructor of ExcludeMeException to signal that it wants to be excluded permanently. After that, the provider will not even attempt to create an instance of the provider.

Notes:

  1. The excludeMePermanently (the parameter of the ExcludeMeException�s constructor) is relevant only to client-side sinks. It is ignored when thrown by server-side sinks.
  2. The excludeMePermanently works on a per provider basis. If the same client-side sink appears multiple times in the configuration file, for example in both HTPP and TCP channels, and the custom sink�s constructor for the HTTP channel throws and exception specifying it should be excluded permanently, it will only be excluded for the HTTP channel. The provider will still attempt to create instances of this custom sink for the TCP channel.

Obtaining Additional Sink Creation Parameters

Your custom sinks may obtain additional information upon their creation. This is done by having a constructor that accepts one parameter of type ClientSinkCreationData or ServerSinkCreationData (depending on the type of your custom sink). Both derive from SinkCreationData, which I presented in the previous section. This constructor has precedence over any other constructor. If it exists, it will be the only one to get called.

The ClientSinkCreationData class contains the following fields: ConfigurationData � the previously discussed SinkProviderData object, channel � the channel for which the sink is created, Url � the URL of the remote object, and RemoteChannelData � data about the channel at the server side (when applicable). You may refer to the parameters of IClientChannelSinkProvider.CreateSink in the .NET Framework SDK documentation for further details.

The ClientSinkCreationData class contains the following fields: ConfigurationData � the previously discussed SinkProviderData object, and channel � the channel for which the sink is created. You may refer to the parameters of IServerChannelSinkProvider.CreateSink in the .NET Framework SDK documentation for further details.

Back to our Lame Encryption example, let�s say that we don�t need encryption when the target object resides on �localhost�. Review the following constructor (which supports previously developed functionality as well). The new constructor for EnhancedLameClientEncryptionSink is a follows:

public EnhancedLameEncryptionClientSink(ClientSinkCreationData creationData)
      : this((SinkCreationData)creationData)
{
      Uri uri = new Uri(creationData.Url);
      if (uri.IsLoopback)
      {
            throw new ExcludeMeException();
      }
}

Notes:

  • Since the EnhancedLameEncryptionServerSink (and the basic LameEncryptionServerSink for that matter) was already designed to support both encrypted and unencrypted communication, it does not require any modifications.
  • The first constructor, the one that takes SinkCreationData as a parameter, is now redundant (not listed here, but still present in the code). However I left it because it is presented in previous sections. Since I already kept it, I redirected to it instead of writing again the code for retrieving the delta from configuration file. However, for efficiency reasons, you should normally first decide whether to throw an ExcludeMeException and only afterwards perform additional construction logic.

Static Initialization

Since client-side custom sinks may be created numerous times, you may sometimes want your class to initialize as much static information as possible. This way, you refrain from processing the same information for every newly created instance. Normally, when you need static initialization, you add a static constructor to your class. You may certainly adopt this approach for your custom sink. However it has one major drawback. If your static constructor throws an exception, this exception cannot normally be caught and handled.

Here is my solution. Define the following method in your custom client-side sink:

public static void Init(SinkProviderData data, ref object perProviderState) 
{ }

This method is guaranteed to be called before any instance of your custom sink is created. You can now place the call to RemotingConfiguration.Configure in try / catch block and catch any exception this method might throw. Moreover, this method is provided with the data from the customData element in the configuration file. NOTE: although this method is more relevant to client-side sinks, it is valid in server-side sinks as well.

Important: There is a major distinction between this method and a static constructor. If your custom sink appears more than once in the configuration file, the Init method will be called multiple times, once per occurrence. In such scenarios, you should avoid assigning data to static fields, because every call to Init will override the fields values assigned in previous calls to. To overcome this problem, use the perProviderState.

The perProviderState allows you to save data on a per provider basis. If your sink appears multiple times in the configuration file, the .NET Remoting infrastructure will create multiple instances of CustomClientSinkProvider, one for each occurrence. You can take advantage of this behavior by assigning one data object of your choice to the perProviderState parameter of the Init method. This way, your object will be held within the provider object and multiple calls to Init (which are done from different instances of the provider) will not override the previously set data. Your custom sink will be able to access the data through the inherited BaseCustomSink.PerProviderData property.

The Init method is not demonstrated in the Lame Encryption sample. However the SampleSinks project contains another example, The Credentials Sinks, which uses this feature. The Credentials Sinks are described below.

A Deeper Look at ProcessRequest and ProcessResponse

The first three parameters of ProcessRequest and ProcessResponse behave differently depending on whether your custom sink is a server-side or client-side sink and whether it is situated before or after the formatter sink. Here is the complete analysis.

ProcessRequest

Client-side, before the formatter sink:

  • message � request message.
  • headers � null.
  • stream � null. Do not assign this parameter to another stream.

Client-side, after the formatter sink:

  • message � request stream message. Do not modify the message as it has already been serialized.
  • headers � request transport headers.
  • stream � request stream.

Server-side, before the formatter sink:

  • message � null.
  • headers � request transport headers.
  • stream � request stream.

Server-side, after the formatter sink:

  • message � request message.
  • headers � request transport headers.
    stream � null. Do not assign this parameter to another stream.

ProcessReponse

Client-side, before the formatter sink:

  • message � response message.
  • headers � null.
  • stream � null. Do not assign this parameter to another stream.

Client-side, after the formatter sink:

  • message � null.
  • headers � response transport headers.
  • stream � response stream.

Server-side, before the formatter sink:

  • message � response message. Do not modify the message as it has already been serialized.
  • headers � response transport headers.
  • stream � response stream.

Server-side, after the formatter sink:

  • message � response message.
  • headers � null.
    stream � null. Do not assign this parameter to another stream.

The Credentials Sinks

The Lame Encryption sinks presented throughout this article manipulate the stream of the request and the response. For completeness, I also included the Credential Sinks, which demonstrate message-based processing.

The task of the client-side sink is to add the username and password to every outgoing communication. The server-side sink verifies these credentials and throws an exception if they are found to be invalid. The server-side retrieves the credentials from the configuration file. The client-side sink also retrieves the credentials from the configuration file. However, different credentials can be assigned to different servers and even ports.

You are invited to review the DemoCredentialServerSink and DemoCredentialClientSink in the SampleSinks project.

Final Note

I made every effort to make this article as mistakes-free and the code as bug-free as possible. If however, I missed anything, I would really like to know. Also, suggestion for further improvements of the CustomSinks library and general comments are more than welcome.

Disclaimer

This article and the accompanying code are provided as-is. You may use it as you please (I�m becoming a poet...). You may NOT hold me liable for any damage caused to you, your company, your neighbors or anyone else as a result of reading this article or using the code. Whatever you do with this article and the accompanying code is at your own risk.

Enjoy.

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