Click here to Skip to main content
Click here to Skip to main content

.NET Remoting Customization Made Easy: Custom Sinks

By , 20 May 2003
 

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

About the Author

Motti Shaked
Software Developer (Senior)
Canada Canada
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
Questionhandling message before formattermembernkabil11 Jun '12 - 6:41 
Hi,
Thank you for this article. I could not understand how to configure provider to handle message before formatter at client side. If anyone helps I will be thankful.
AnswerRe: handling message before formattermembernkabil11 Jun '12 - 9:24 
I found it, just replacing provider with formatter in config file.
QuestionHow program to adjust channels (without usage of files of configurations)?memberMTM9999 Oct '11 - 8:35 
In particular me adjustment of the server and the client on configuration Basic_SampleClient.exe.config interests.
Question.NET Remoting Customization Made Easy: Custom Sinks Problemmembernercan28 Jan '10 - 1:48 
Hi,
 
I'm trying to use your custom skins.
Standard methods is no problem. But I want to use a method like the following server application closes.
What should I do?
 
public List<object> Get<T>(string x, string y) where T:class
        {
            List<object> list = new List<object>();
            list.Add(x);
            list.Add(y);
            return list;
        }

AnswerRe: .NET Remoting Customization Made Easy: Custom Sinks Problemmembernercan28 Jan '10 - 20:19 
I solved the problem. But does not using http channel as far as I understand.
 
Old Config File
<configuration>
	<system.runtime.remoting>
		<application>
			<channels>
				<channel ref="http" port="8085">
					<serverProviders>						
					
						<provider type="CustomSinks.CustomServerSinkProvider, CustomSinks"
							customSinkType="CustomSinks.LameEncryptionServerSink, CustomSinks" />
							
						<formatter ref="soap" />
							
					</serverProviders>
				</channel>
			</channels>			
		</application>
	</system.runtime.remoting>
</configuration>
 
New Config File
<configuration>
	<system.runtime.remoting>
		<application>
			<channels>
				<channel ref="tcp" port="8085">
					<serverProviders>						
					
						<provider type="CustomSinks.CustomServerSinkProvider, CustomSinks"
							customSinkType="CustomSinks.LameEncryptionServerSink, CustomSinks" />
							
						<formatter ref="binary" />
							
					</serverProviders>
				</channel>
			</channels>			
		</application>
	</system.runtime.remoting>
</configuration>
 
Thanks
QuestionOverloaded properties in VB.NETmemberlreeder15 May '07 - 8:06 
I have been converting the C# code from the article by Matti Shaked called ".NET Remoting Made Easy: Custom Sinks" to VB.NET (FW 1.1). I ran into an interesting problem converting the "NextChannelSink" properties in the abstract BaseCustomSink class. When they convert to VB the declarations of the properties are:
 
"Overloads ReadOnly Property NextChannelSink() As IClientChannelSink Implements IClientChannelSink.NextChannelSink"
 
and
 
"Overloads ReadOnly Property NextChannelSink() As IServerChannelSink Implements IServerChannelSink.NextChannelSink"
 
for the client and the server sinks (respectively). In C# the property name in the declaration is qualified by the interface name like this:
 
"IClientChannelSink IClientChannelSink.NextChannelSink"
 
In VB I get a compiler error stating:
 
"Public Overloads ReadOnly Property NextChannelSink() As System.Runtime.Remoting.Channels.IClientChannelSink' and 'Public Overloads ReadOnly Property NextChannelSink() As System.Runtime.Remoting.Channels.IServerChannelSink' cannot overload each other because they differ only by return types."
 
Is there any other way in VB to declare these properties, whose signatures only differ by return types, so that they can overload with the same name? Prefacing the name with the interface name (ala. C#) does not work in VB.
 

GeneralThanks a lot [modified]memberkapil bhavsar3 May '07 - 21:19 
I was really in dispair how to customize remoting ... and seeing the long long codes Sigh | :sigh: and damn horrible terminologies i was quite unable to setup my mind for customizing remoting in my application Confused | :confused: ...
 
And in the middle your came like blessings:->
 
Beutiful well organized article .. much better than even Class room teaching
 
Thanx a lot Smile | :)
 
May be you shoulb try to write books Wink | ;)
 

 

-- modified at 4:12 Friday 4th May, 2007
GeneralException in custom sink not handled when debuggingmemberDave Midgley4 Feb '07 - 23:00 
I have a very odd problem which I hope someone can help me with. I have written a very simple test program as an exercise in using a custom sink - just a simple server with a single method called GetReply() and a simple client. After configuring the remoting the code in the client looks like this:
 
try
{
responseTextBox.Text = server.GetReply();
}
catch (Exception MyErr)
{
MessageBox.Show(MyErr.Message, MyErr.GetType().ToString());
}
 

Now, if I run the client without running the server I expect to get an exception, which indeed I do - a SocketException with the message "No connection could be made because the target machine actively refused it", and without the custom sink this exception is caught in the above code and the message box displayed, just as you would expect.
 
However, when I add in the custom sink the behaviour changes - but ONLY when I am debugging in Visual Studio. Now the exception is flagged up in the IClientChannelSink.ProcessMessage() method by the debugger as an unhandled exception when it calls the ProcessMessage() method on the next sink (BaseCustomSink.cs line 112). If I continue execution after the exception the message box is shown as before. Also, if I run the program outside the development environment it behaves as expected.
 
The custom sink will eventually be used in a 'real'application, in which it is quite acceptable for the client to run without the server, catch the exception and react accordingly. However, it is going to make debugging very difficult if, whenever I run the client in the development environment, it halts at this exception.
 
If anyone can shed any light on this I would be most grateful. I am using C# in Visual Studio 2005 with .NET2.0.
 

 
Dave

GeneralCallContext not available in ProcessRequest() of Servermembersandit277 Dec '06 - 23:21 
Hi,
 
CallContext is not available in ProcessRequest() method of Server.
Can somebosy help me ?
 

Thanks !
Sandeep DSmile | :)
 
SandeepA

GeneralMotti where are youmemberBob-ish4 Oct '06 - 7:36 
Motti, please contact me at bobishkindaguy@hotmail.com if you are available to do a bit of consulting on remoting.
Bob
GeneralGreat! It's my textbook for remotingmemberLi Xiaojian(China)1 Aug '06 - 5:16 
Great! It's my textbook for remoting, I am from the client side of Pacific Ocean--chinese, and you from the server side of Pacific Ocean--USA, thanks!Smile | :)
GeneralDemo please~memberh2duck27 Mar '06 - 21:58 
very Hard.Cool | :cool:
 
haha
GeneralRe: Demo please~memberbebangs13 Aug '06 - 20:44 
DITTO.WTF | :WTF: newbie here.. got to study more...
GeneralRedirecting messagemembershulmanv5 Dec '05 - 12:25 
If i intercepted message in client's custom sink (before the formatter) , how do i change it's destination (that is send it to other server) ?
 

Thanks
QuestionSwitch to TCPmemberDyareela10 Oct '05 - 22:40 
Hello!
Could you show me how i can switch to tcp channel. I´m trying to encrypt data communication with a tcp channel, but it calls the IMessageSink.SyncProcessMessage(IMessage reqMsg) Method, wich doesn´t encrypt to a stream, because it doesn´t need a Stream. what can i do?!
Please help!! Confused | :confused:
QuestionHow To Debug?sussAnonymous24 Jun '05 - 8:08 
Can you provide some hints on how to step through this code using the debugger?
Generaltoo complicatedsussGbtex3 Feb '05 - 23:43 
You can find better way in www.dotnetremoting.com
QuestionRe: too complicatedmemberDyareela10 Oct '05 - 22:37 
where can i find it there?
GeneralRe: too complicatedmemberMr Morden22 Jan '06 - 19:52 
But you have to pay for it...
 
this site is a commercial site. Nothing wrong with selling a product. But there is something wrong with SPAM!
 
Boycott spam. Boycott dotNetRemoting.
 
Cheers
 
The universe is driven by the complex interaction between three ingredients: matter, energy, and enlightened self-interest.

QuestionStatic Initializers vs. Init(...)?memberSpunk12 Dec '04 - 3:55 
I was a bit confused on why static intitializers can not be used for the class? I see that you chose to use the Init(...) with a try/catch, but didn't quite understand why static initializers (or static constructor) wouldn't work. Can you elablroate on this?
AnswerRe: Static Initializers vs. Init(...)?memberChandrasekar Ganesan8 Nov '05 - 1:07 
Type constructors as they are called are handled by .NET runtime in a special way. If an exception is thrown out of static constructor, the type is never initialized and successive calls are not routed to the type, .net simply throws the cached exception. This is the reason.
 
heavyjunk
GeneralBefore or After the Formatter SinksussDiego Carreras24 Nov '04 - 13:09 
How do I change the location of my custom sink to either before or after the formatter sink on the server? In the example the default location appears to be when the stream is not null indicating the stream has already been generated. I would like to manipulate my message object in the server response before it gets formatted into a stream. How do I configure it so my custom sink is in the correct location in the chain?
GeneralRe: Before or After the Formatter SinksussDiego Carreras25 Nov '04 - 6:20 
Answered my own question.
 
I read the article again. I needed to change the order of the formatter tag and provider tag in the config file.
GeneralRe: Before or After the Formatter SinkmembermPirate15 Nov '07 - 0:59 
cool) thanks;)
Generalgreat!!sussPunitRSethi29 Aug '04 - 23:57 
Cool! I've really found it difficult to get good information about custom sinks and how to create them. This one seems really straight forward. However, how can I do something like - use my customized sink only for certain message? for example - encrypt only message that passes uname/password and let others go straight without encryption sink bothering?
 
Thanks.Smile | :)
GeneralThis work of yours worths a lot of moneysussAmir, Israel28 May '04 - 6:54 
Great code,
Fantastic work.
Thanks Motti.
GeneralBeautifulmemberAbulala13 May '04 - 19:49 
Beautiful ariticle. Great Job.........Excellent.Smile | :)
GeneralSystem.Net.SocketException during high loadmemberMattias Nordberg12 Sep '03 - 5:38 
Hello, first I would like to thank you for this great piece of code, it has really made me understand the inner workings of Remoting Sinks.
 
Unfortunately when I use the CustomSink in one of my projects and alot of remoting calls are made from the client (i think about 600 repetitive ones), I get a System.Net.SocketException saying
 
"Only one usade of each socket address (protocol/network address/port) is normally permitted."
 
It would seem as if the Client part of CustomSinks have some sort of leak, not cleaning up the sockets or maybe a threading problem.
 
Would be greatfull for any help you could provide in this matter.
GeneralRe: System.Net.SocketException during high loadmemberMattias Nordberg15 Sep '03 - 1:31 
Never mind, I solved the problem, I didn't close the source stream after returning an encrypted stream, so during high load the garbage collector didnt really keep up, thats my guess.
 
I'm sorry I bothered you Smile | :)
GeneralProgrammatic Configuration? Doesn't work!membermokka°logic8 Aug '03 - 17:36 
Well, at least I tried everything I would think of tonight.
You have pointed out how to configure a remoting app via .config files, but obviously for security and complexity concerns one should rather consider programmatic configuration.
However, I was trying the following:
 
TcpClientChannel channel;
IDictionary props = new Hashtable ();
SinkProviderData [] data = new SinkProviderData [1];
 
props ["customSinkType"] = "MyApp.AuthenticationClientSink, MyApp";
data [0] = new SinkProviderData ("customData");
 
channel = new TcpClientChannel ("", new CustomClientSinkProvider (props, data));
ChannelServices.RegisterChannel (channel);
 
But there does not seem to be a way to chain the binary formatter and the tcp client transport sink to my custom sink. Raaah! I tried everything: guestimating property-hashtable key/values ("formatter", "binary"); setting the nextSink field programmatically to a "new BinaryClientFormatterSink ()" etc pp...
 
Can anyone help, please? How do I tell the channel that I want to use the binary formatter IMessageSink as well as the default TcpClientChannelSink?
In fact, the whole thing seems overcomplicated. Why can't I just put my sink into the chain at any point? Why do I have to re-create the whole chain apparently? What's the point in SinkProviders, when we could put the sinks into the chain just as well? And if SinkProviders, why does the TcpClientChannel constructor (and the other channel constructors as well) only accept _one_ provider?? Perhaps a SinkProviderProvider should be introduced... no, but honestly, any advice would be highly valued.
 
Many thanks,
Philipp
GeneralRe: Programmatic Configuration? Doesn't work!memberbasseman18 Apr '05 - 10:59 
Hi,
 
I am having the same problem
 
I would apreciate if someone would giveme a hint on how to do it without having a config file
but instead in the code same as Philipp
 
Thanks
Bassem
GeneralRe: Programmatic Configuration? Doesn't work!memberPhilipp Schumann18 Apr '05 - 11:11 
Hi Bassem,
 
1 1/2 years ago ... vaguely remember that I somehow managed, but can't recall _how_.
I believe you can construct your sink chains specifying the "next" sink in each sink's constructor ... or something similar.
 
Good luck, keep playing with the API and the docs.
My original project was abandoned and I don't have the sources anymore, otherwise I'd post it.
GeneralRe: Programmatic Configuration? Doesn't work!membermhoffmann699 May '05 - 10:32 
Has anyone solved this issue in the meantime? Played around to solve it myself, but was not successful. Any help still appreciated...
GeneralRe: Programmatic Configuration? Doesn't work!sussHeavyHenke1 Jun '05 - 21:45 
I have managed to configure the system programmaticly. I am not sure that it is the correct way to do it, but it worksRoll eyes | :rolleyes: . This is what i do on the client side:
 
BinaryClientFormatterSinkProvider clientProvider;
BinaryServerFormatterSinkProvider serverProvider;
IDictionary props;
TcpChannel channel;
 
props = new Hashtable();
props["customSinkType"] = "LameEncryption.LameEncryptionClientSink, SampleSinks";
props["port"] = 0;
props["typeFilterLevel"] = System.Runtime.Serialization.Formatters.TypeFilterLevel.Full;
 
serverProvider = null;
clientProvider = new BinaryClientFormatterSinkProvider();
clientProvider.Next = new CustomSinks.CustomClientSinkProvider(props, new object[0]);
 
channel = new TcpChannel(props, clientProvider, serverProvider);
 
ChannelServices.RegisterChannel(channel);
 

And the same is done on the serverside, except that LameEncryptionServerSink is used instead of LameEncryptionClientSink.
GeneralRe: Programmatic Configuration? Doesn't work!membermjanulaitis123424 May '06 - 9:23 
The code you are using is opening a port and listening. I'm confused, you are registring a client remoting object. Why would you want to open a dynamic port? I've used the following code in the past which doesn't listen on a port:
 
Type type = typeof(TestRmtObj);
string url = "tcp://localhost:8999";
RemotingConfiguration.RegisterActivatedClientType(type, url);
GeneralRe: Programmatic Configuration? Doesn't work! [modified]memberDave Midgley31 Jan '07 - 6:14 
For the benefit of anyone else struggling with this, I thought I would put in my twopennyworth.
First of all, the server isn't quite as simple as replacing LameEncryptionClientSink with LameEncryptionServerSink. In the server the custom sink should be placed after the formatter sink. Also, it needs a real port number, and a serverProvider rather than a clientProvider thus:
 
IDictionary props = new Hashtable();
props["customSinkType"] = "LameEncryption.LameEncryptionClientSink, SampleSinks";
props["port"] = portNumber;
CustomServerSinkProvider cssp= new CustomServerSinkProvider(props, new object[0]);
cssp.Next = new BinaryServerFormatterSinkProvider();
channel = new HttpChannel(props, null, cssp);
 
I also had a real problem adapting the encryption sinks to use a real encryptor - the problem is that the Stream object that is passed to ProcessResponse and ProcessRequest is either a TcpFixedLengthReadingStream or a ChunkedMemoryStream - both inaccessible classes in the System.Runtime.Remoting.Channels namespace. The problem is that for some inexplicable reason the Length and Position properties are not implemented in TcpFixedLengthReadingStream - it throws a NotSupportedException, which makes it rather awkward to get the contents of the stream. In the end I had to copy them to a MemoryStream before encrypting/decrypting. This is my version of Motti's encryption helper class, which uses a 3DES encryption:
 
internal class Encryptor
{
private static string encryptionKey = "sjrgsm7j43knfj7ejvmgjw8o";
 
// stream maybe a TcpFixedLengthReadingStream or a ChunkedMemoryStream.
public static Stream Encrypt(Stream stream)
{
TripleDESCryptoServiceProvider CSP = new TripleDESCryptoServiceProvider();
CSP.Key = ASCIIEncoding.ASCII.GetBytes(encryptionKey.Substring(0,24));
CSP.IV = ASCIIEncoding.ASCII.GetBytes(encryptionKey.Substring(0,8));
// For some incomprehensible reason TcpFixedLengthReadingStream doesn't support
// the Length or Position properties
MemoryStream inStream = new MemoryStream();
int inByte;
while ((inByte = stream.ReadByte()) >= 0)
{
inStream.WriteByte((byte)inByte);
}
ICryptoTransform cryptoTransform = CSP.CreateEncryptor();
MemoryStream encryptedStream = new MemoryStream();
byte[] dataOut =
cryptoTransform.TransformFinalBlock(inStream.ToArray(), 0, (int)inStream.Length);
encryptedStream.Write(dataOut, 0, dataOut.Length);
encryptedStream.Position = 0; // must reset the stream
return encryptedStream;
}
 
public static Stream Decrypt(Stream stream)
{
TripleDESCryptoServiceProvider CSP = new TripleDESCryptoServiceProvider();
CSP.Key = ASCIIEncoding.ASCII.GetBytes(encryptionKey.Substring(0, 24));
CSP.IV = ASCIIEncoding.ASCII.GetBytes(encryptionKey.Substring(0, 8));
// For some incomprehensible reason TcpFixedLengthReadingStream doesn't support
// the Length property
MemoryStream inStream = new MemoryStream();
int inByte;
while ((inByte = stream.ReadByte()) >= 0)
{
inStream.WriteByte((byte)inByte);
}
ICryptoTransform cryptoTransform = CSP.CreateDecryptor();
MemoryStream decryptedStream = new MemoryStream();
byte[] dataOut =
cryptoTransform.TransformFinalBlock(inStream.ToArray(), 0, (int)inStream.Length);
decryptedStream.Write(dataOut, 0, dataOut.Length);
decryptedStream.Position = 0; // must reset the stream
return decryptedStream;
}
 
Hope somebody find that useful. Any observations gratefully received.
 

-- modified at 11:20 Thursday 1st February, 2007
 
Dave

GeneralRe: Programmatic Configuration? Doesn't work!memberDzamirro19 Mar '07 - 5:54 
I resolved today, i will post soon with the solution!!
GeneralRe: Programmatic Configuration? Doesn't work!memberandecla120 Mar '07 - 0:37 
I solved the problem in this way:
 


<formatter ref="binary" />






 
the code below do the same thing of this configuration file:
 
IDictionary props = new Dictionary();
props["ref"] = "tcp";
props["port"] = "8086";
props["ccustomErrors"] = "false";
#if (DEBUG)
props["timeout"] = "-1";
#else
props["timeout"] = "10000";
#endif
 
IDictionary serverProvider = new Hashtable();
serverProvider["type"] = "CustomSinks.CustomServerSinkProvider, CustomSinks";
serverProvider["customSinkType"] = "ServerContabilita.Remoting.CredentialsServerSink, ServerContabilita";
ArrayList providerData = new ArrayList(1);
SinkProviderData customData = new SinkProviderData("customData");
customData.Properties.Add("param1", "data1");
customData.Properties.Add("param2", "data2");
providerData.Add(customData);
 

IServerChannelSinkProvider serverSinkAuthenticationProvider = new CustomServerSinkProvider(serverProvider, providerData);
BinaryServerFormatterSinkProvider sinkFormatterProvider =
new BinaryServerFormatterSinkProvider();
 
sinkFormatterProvider.Next = serverSinkAuthenticationProvider;
TcpServerChannel channel = new TcpServerChannel(props, sinkFormatterProvider);
 
ChannelServices.RegisterChannel(channel);
 
andecla
GeneralRe: Programmatic Configuration? Doesn't work!memberDerek Viljoen12 Nov '07 - 4:50 
Here's how I did the server-side initialization w/o config files:
 

IDictionary props = new Hashtable();
props[ "name" ] = _serviceName + "_channel";
props[ "port" ] = int.Parse( _configuration[ "ServicePort" ] );
props[ "customSinkType" ] = typeof( CallStatusServerSink ).AssemblyQualifiedName;
props[ "typeFilterLevel" ] = TypeFilterLevel.Full;
 
SinkProviderData data = new SinkProviderData( "customData" );
data.Properties[ "Service" ] = this;
BinaryServerFormatterSinkProvider provider1 = new BinaryServerFormatterSinkProvider();
CustomServerSinkProvider provider = new CustomServerSinkProvider( props, new object[] { data } );
 
provider1.Next = provider;
_channel = new TcpChannel( props, null, provider1 );
ChannelServices.RegisterChannel( _channel, false );
RemotingServices.Marshal( this, _serviceName );

 
I think the key is creating two SinkProviders and chaining them with the provider.Next assignment. You add the head of the chain to the TcpChannel constructor, and viola!
 
I haven't done the client-side yet, but I expect it to be similar.
 
Derek
AnswerRe: Programmatic Configuration? Doesn't work!memberGeoJan24 Mar '07 - 4:42 
Philipp,
 
You need to programmatically create the BinaryFormater, and chain it to the CustomClientSinkProvider.
 
Based on your code, that would be something like:
 
TcpClientChannel channel;
IDictionary props = new Hashtable ();
SinkProviderData [] data = new SinkProviderData [1];
 
props ["customSinkType"] = "MyApp.AuthenticationClientSink, MyApp";
data [0] = new SinkProviderData ("customData");
 
CustomClientSinkProvider custSink = CustomClientSinkProvider (props, data)
BinaryClientFormatterSinkProvider bfSinkProvider = new BinaryClientFormatterSinkProvider();
bfSinkProvider.Next = custSink;
 
channel = new TcpClientChannel ("", bfSinkProvider);
ChannelServices.RegisterChannel (channel);

 
Regards
Georg
 
http://www.l4ndash.com - Log4Net Dashboard / Viewer

AnswerRe: Programmatic Configuration? Doesn't work!membervtelenak16 Jun '07 - 10:48 
public class SecureSink : BaseChannelObjectWithProperties, IClientChannelSink, IServerChannelSink, IMessageSink //My custom sink provider (Server and Client side is same class)
 
...
 
Client side
-----------
private TcpChannel channel;
 
...
 
IDictionary props = new Hashtable();
props["secure"] = false;
props["port"] = 0;
props["name"] = "Kanal" + Guid.NewGuid().ToString().Substring(0, 6); //Random channel name
BinaryClientFormatterSinkProvider clientBinaryProvider = new BinaryClientFormatterSinkProvider();
ClientSecureSinkProvider clientSecureProvider = new ClientSecureSinkProvider();
clientSecureProvider.Next = clientBinaryProvider;
channel = new TcpChannel(props, clientSecureProvider, null);
ChannelServices.RegisterChannel(channel);
 
...
 
public class ClientSecureSinkProvider : IClientChannelSinkProvider //My client sink provider
{
 
public IClientChannelSink CreateSink(IChannelSender channel, string url, object remoteChannelData)
{
IClientChannelSink nextSilk = nextProvider.CreateSink(channel, url, remoteChannelData);
return new SecureSink(nextSilk);
}
 
private IClientChannelSinkProvider nextProvider = null;
public IClientChannelSinkProvider Next
{
get { return nextProvider; }
set { nextProvider = value; }
}
 
}
 

 
Server side
-----------
private TcpChannel channel;
 
...
 
IDictionary props = new Hashtable();
props["secure"] = false;
props["port"] = 2080;
props["name"] = "Kanal" + Guid.NewGuid().ToString().Substring(0, 6); //Random channel name
BinaryServerFormatterSinkProvider serverBinaryProvider = new BinaryServerFormatterSinkProvider();
ServerSecureSinkProvider serverSecureProvider = new ServerSecureSinkProvider();
serverSecureProvider.Next = serverBinaryProvider;
channel = new TcpChannel(props, null, serverSecureProvider);
ChannelServices.RegisterChannel(channel);
 
...
 
public class ServerSecureSinkProvider : IServerChannelSinkProvider
{
 
private IServerChannelSinkProvider nextProvider = null;
public IServerChannelSinkProvider Next
{
get { return nextProvider; }
set { nextProvider = value; }
}
 
public IServerChannelSink CreateSink(IChannelReceiver channel)
{
IServerChannelSink nextSilk = nextProvider.CreateSink(channel);
return new SecureSink(nextSilk);
}
 
public void GetChannelData(IChannelDataStore channelData)
{
}
}
 
Many Accomplishments
Petr
QuestionI want to get some information,what should I do?memberhappytonny15 Jul '03 - 18:07 

The steam,header and message flow from the client to the server thought the channel sinks.Now I want to get these informations in the server and can do someting about them.I want to control the remote object in the server,what should I do!
Thanks
GeneralA remoting questionmemberDavid Gallagher1 Jun '03 - 7:55 
Is there a difference between .net 1.0 and 1.1,
I get a security error in 1.1 when trying to hook up an event that
doesn't appear when using 1.0 ?
Thanks
GeneralRe: A remoting questionmemberMats2 Jun '03 - 2:29 
One difference between 1.0 and 1.1 is that in order to be able to hook up to an event you nedd to add a property to your ServerFormatterSinkProvider.
 
IDictionary props = new Hashtable();
props["typeFilterLevel"] = "Full"; // Must be set in 1.1
props["port"] = nPort;
BinaryServerFormatterSinkProvider formatterProvider = new BinaryServerFormatterSinkProvider(props, null);
 


 
/Mats
GeneralRe: A remoting questionmemberDavid Gallagher2 Jun '03 - 7:39 
Thanks , I'll try that when I get home,
Where did you see that ? Does MS have a 1.0 to 1.1 site, I couldn't find anything.
GeneralRe: A remoting questionmemberMats2 Jun '03 - 8:39 
I think it was on http://gotdotnet.com

GeneralVery nice!memberThe Infamous Propellerhead28 May '03 - 6:17 
This is the best example of remoting that I have found yet. It actually works right out of the box!
 
Now, how would I write a service class (like SampleServer) that can be started up automatically? For example, can it be started up by calling an aspx page and remain running for some period of time? Perhaps just until the page times out?
GeneralElegantmemberPadgett Rowell22 May '03 - 21:53 
Well written, and most helpful for my current project. I too would like to see a fuller implementation of the encryption class.
GeneralVery interestingmemberOrantho22 May '03 - 9:59 
Thanks for this very interesting article. I would love to see an example of encrypted remoting in a future article about 'real-world encryption sinks'.
GeneralExcellent ArticlememberRichard A. Johnn21 May '03 - 10:55 
Wink | ;) Very will explained. I haven't had the time to "play" with the code yet, but I plan to later tonight.
I really like the way you broke down the tasks of customizing the sink. Great job!


 
For readers who need more information on proxies, check out this article: Inform IT - C# Design Patterns - Proxy
 
- RAJ

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 21 May 2003
Article Copyright 2003 by Motti Shaked
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid