Click here to Skip to main content
Click here to Skip to main content
Go to top

Test Driven Prototyping - Learning About .NET Remoting

, 7 Apr 2007
Rate this:
Please Sign up or sign in to vote.
Use test driven development processes to determine the issues affecting application architecture and design with regards to .NET remoting.

Introduction

I was inspired by Colin's article Test Driven / First Development by Example to use the test driven approach for investigating .NET remoting. I wanted to investigate remoting as an option for dealing with some flaky aspects of our video kiosk system, namely the DirectX AVI and DVD player code, which has a tendency to crash or lock up the application when a bad movie or some problematic DVD is encountered. These are things not under my control, and when something bad happens, I've seen it lock up all the application threads, so something external, like a watchdog app, needs to be monitoring the viewers.

I figured remoting might be an interesting way of isolating the business and data access layers from the presentation layer, which is not very reliable. I also figured that a test driven prototyping approach would be a good way to investigate and simulate the kinds of problems we've encountered and test how these are handled under remoting. To that end, I'm using my Advanced Unit Test engine and, since I've only done remoting once before and have forgotten everything I learned (which wasn't much to begin with), I'm also using Patrick Smacchia's excellent book Practical .NET2 and C#2, in particular, the stuff in Chapter 22.

Advanced Unit Test Engine

You can read about the AUT engine on the following links:

Part I - Overview
Part II - Core Implementation
Part III - Testing Processes
Part IV - Fixture Setup/Teardown, etc...

and download the latest version of the engine from here.

Single Call Activation vs. Singleton Activation

To begin with, I wanted to verify the differences between single call activation and singleton activation, so I wrote a couple unit tests:

[Test]
public void InstancePerCallTest()
{
  ITest t1 = RemotingHelper.CreatePerCallTestObject();
  t1.Value1 = 1;
  t1.Value2 = 2;
  Assertion.Assert(t1.Value1 == 0, "Expected t1.Value1 to be 0 for a 
                  single call activation remote object.");
}

[Test]
public void SingletonActivationTest()
{
  ITest t1 = RemotingHelper.CreateSingletonTestObject();
  t1.Value1 = 1;
  t1.Value2 = 2;
  Assertion.Assert(t1.Value1 == 1, "Expected t1.Value1 to be 1 for a 
              singleton remote object.");
  ITest t2 = RemotingHelper.CreateSingletonTestObject();
  Assertion.Assert(t2.Value1 == 1, "Expected t2.Value1 to be 1 for a 
              second instance of the singleton object.");
}

The first test verifies that, by making an assignment to Value2, the object is instantiated again, and therefore Value1 is initialized to its default value, which is 0, and thus loses its assignment in the line above. And indeed, this is how a single call activation works.

The second test verifies that, which a singleton activation, this is not the case, and furthermore, that a call to create a second object does nothing more than actually return the first object. I decided this test was a bit too complicated and wrote a simpler one:

[Test]
public void SameReferenceTest()
{
  ITest t1 = RemotingHelper.CreateSingletonTestObject();
  ITest t2 = RemotingHelper.CreateSingletonTestObject();
  Assertion.Assert(t1 == t2, "Expected t1==t2.");
}

Factory Instantiation

Because neither the single call activation nor the singleton activation are going to be appropriate for our requirements, I expanded the tests further by using a factory class to instantiate the test object. I expected in this case that two or more instances created by the factory would be separate. The following two tests illustrate this:

[Test]
public void FactoryActivationTest()
{
  IFactory factory = RemotingHelper.CreateFactoryObject();
  ITest t1 = factory.CreateTest();
  t1.Value1 = 1;
  t1.Value2 = 2;
  Assertion.Assert(t1.Value1 == 1, "Expected t1.Value1 to be 1 for a 
              factory created remote object.");
  ITest t2 = factory.CreateTest();
  Assertion.Assert(t2.Value1 == 0, "Expected t2.Value1 to be 0 for a 
              second instance of a factory created remote object.");
}

[Test]
public void FactoryDifferentReferenceTest()
{
  IFactory factory = RemotingHelper.CreateFactoryObject();
  ITest t1 = factory.CreateTest();
  ITest t2 = factory.CreateTest();
  Assertion.Assert(t1 != t2, "Expected t1 != t2.");
}

And indeed, these tests pass.

Important Understanding Gained Here

This is an important step because I now know that we can use the remote service to instantiate discrete objects of the same class, using a factory.

The Code So Far

After studying the chapter on remoting and then writing the unit tests, I implemented the server and client code. Amazingly, the unit tests passed the first time, verifying that my understanding, and Mr. Smacchia's book, are correct.

The code so far consists of:

  • the interface specification
  • the concrete Test class implementation
  • the concrete Factory class implementation
  • the server code
  • the client code

The Interface Specification

There are two interfaces, one for the factory, and one for the test class:

using System;

namespace Interfaces
{
  public interface IFactory
  {
    ITest CreateTest();
  }

public interface ITest
  {
    int Value1 { get;set;}
    int Value2 { get;set;}
  }
}

The Concrete Test Class Implementation

The server maintains the concrete implementation for the Test class:

using System;

using Interfaces;

namespace Server
{
  public class Test : MarshalByRefObject, ITest
  {
    protected int value1;
    protected int value2;

    /// <span class="code-SummaryComment"><summary>
</span>
    /// Gets/sets value2
    /// <span class="code-SummaryComment"></summary>
</span>
    public int Value2
    {
      get { return value2; }
      set { value2 = value;}
    }

    /// <span class="code-SummaryComment"><summary>
</span>
    /// Gets/sets value1
    /// <span class="code-SummaryComment"></summary>
</span>
    public int Value1
    {
      get { return value1; }
      set { value1 = value;}
    }

    public Test()
    {
    }
  }
}

Two things to note here are:

  1. The Test class derives from MarshalByRefObject, which is necessary for remoting that uses marshalling by reference, in which a proxy of the object is created at the client (as opposed to marshal by value, which uses the [Serializable] attribute and creates a clone of the server object on the client and therefore the two are disconnected.
  2. The Test class implements the ITest interface, which is all the metadata that client needs to know about the Test class.

The Concrete Factory Implementation

This is a very simple class, returning a new instance of the concrete Test class. Again, the class is derived from MarshalByRefObject and implements in this case the IFactory interface.

using System;

using Interfaces;

namespace Server
{
  public class Factory : MarshalByRefObject, IFactory
  {
    public ITest CreateTest()
    {
      return new Test();
    }
  }
}

The Server Code

To complete the server is the actual program:

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;

using Interfaces;

namespace Server
{
  class Program
  {
    static void Main(string[] args)
    {
      HttpChannel channel = new HttpChannel(65100);
      ChannelServices.RegisterChannel(channel, false);
      RemotingConfiguration.RegisterWellKnownServiceType(typeof(Test), 
         "SingleCallTestService", WellKnownObjectMode.SingleCall);
      RemotingConfiguration.RegisterWellKnownServiceType(typeof(Test), 
         "SingletonTestService", WellKnownObjectMode.Singleton);
      RemotingConfiguration.RegisterWellKnownServiceType(typeof(Factory), 
         "FactoryService", WellKnownObjectMode.SingleCall);
      Console.WriteLine("Press a key to stop the server...");
      Console.Read();
    }
  }
}

Of note here are the separate object URI's to distinguish between single call and singleton instantiation, and the separate URI for the factory.

The Client Code

Besides the unit tests, I'm using a helper class this simulates the actual instantiation process of the objects:

using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Http;

using Interfaces;

namespace ClientTests
{
  public static class RemotingHelper
  {
    public static HttpChannel channel;

    public static void RegisterChannel()
    {
      channel = new HttpChannel(0);
      ChannelServices.RegisterChannel(channel, false);
    }

    public static void UnregisterChannel()
    {
      ChannelServices.UnregisterChannel(channel);
    }

    public static ITest CreatePerCallTestObject()
    {
      MarshalByRefObject objRef = (MarshalByRefObject)
         RemotingServices.Connect(typeof(ITest), 
         "http://localhost:65100/SingleCallTestService");
      ITest test = objRef as ITest;

      return test;
    }

    public static ITest CreateSingletonTestObject()
    {
      MarshalByRefObject objRef = (MarshalByRefObject)
          RemotingServices.Connect(typeof(ITest), 
          "http://localhost:65100/SingletonTestService");
      ITest test = objRef as ITest;

      return test;
    }

    public static IFactory CreateFactoryObject()
    {
      MarshalByRefObject objRef = (MarshalByRefObject)
          RemotingServices.Connect(typeof(IFactory), 
          "http://localhost:65100/FactoryService");
      IFactory factory = objRef as IFactory;

      return factory;
    }
  }
}

Server-Side Exceptions

The next thing I want to understand are server-side exceptions. What happens when an exception occurs by the code running at the server? For this, I want to test both system exceptions and custom exceptions, so I'm going to create two methods in ITest:

public interface ITest
{
  int Value1 { get;set;}
  int Value2 { get;set;}
  void CreateSystemException();
  void CreateCustomException();
}

Now, this is unknown territory, so I'm going to make some assumptions about the unit tests:

[Test]
[ExpectedException(typeof(Exception))]
public void SystemExceptionTest()
{
  ITest t1 = RemotingHelper.CreateSingletonTestObject();
  t1.CreateSystemException();
}

[Test]
public void CustomExceptionTest()
{
  ITest t1 = RemotingHelper.CreateSingletonTestObject();
  t1.CreateCustomException();
}

And the implementation:

public void CreateSystemException()
{
  throw new Exception("The method or operation is not implemented.");
}

public void CreateCustomException()
{
  throw new TestException("The method or operation is not implemented.");
}

And the custom Exception class:

public class TestException : Exception
{
  public TestException(string msg)
    : base(msg)
  {
  }
}
I'm expecting to get an Exception at the client from a system exception, but I really have no idea what I'll be getting by throwing an exception that the client doesn't know about. So we'll let the unit test engine tell me. The first test passes, however the second test actually throws a SerializationException, with the message:
The type Server.TestException in Assembly Server, Version=1.0.0.0, 
Culture=neutral, PublicKeyToken=null is not marked as serializable.
OK, so I'll mark it serializable:
[Serializable]
public class TestException : Exception
{
  public TestException(string msg)
    : base(msg)
  {
  }
}
Hmm. I still get a SerializationException:
Parse Error, no assembly associated with Xml key blahblahblah TestException

Important Understanding Gained Here

This is obvious but the implications are vast. If the object I'm remoting throws an exception for which the client doesn't have metadata, a SerializationException will be thrown at the client. Given the nature of the what I'm looking at using for remoting, I can't guarantee that I know every kind of exception that the DirectX libraries might throw (especially since we're also planning on using VMR9 for handling the overlays). Since I don't want the "controller" application to have to reference those libraries, I'm going to have to be very careful to wrap each call in a try-catch block that either consumes the exception or passes back to the client a common exception known by the client.

Fixing The Unit Test

For the moment, let's simply fix the unit test by adding SerializationException as the expected exception:
[Test]
[ExpectedException(typeof(SerializationException))]
public void CustomExceptionTest()
{
  ITest t1 = RemotingHelper.CreateSingletonTestObject();
  t1.CreateCustomException();
}

Classes That Reference Non-Marshallable, Unserializable Objects

In our situation, the interface that the client and server use is really nothing more than an API for communicating to the underlying "real" objects that are going to be doing the work, such as the DirectX viewer. We certainly don't want to marshal these objects, so let's test out what happens when the concrete implementation references a class that does not support marshalling or serialization. I created the following test (along with the interface and creation method):
[Test]
public void CreateComplexTest()
{
  IComplexTest compTest = RemotingHelper.CreateComplexTest();
  string ret = compTest.DoSomething();
  Assertion.Assert(ret == "Doing something.", "Expected valid return.");
}
and the following test, which creates a non-marshallable, unserialized class as a property of the test class:
using System;
using Interfaces;

namespace Server
{
  public class RealClass
  {
    public string DoSomething()
    {
      return "Doing something.";
    }
  }

  public class ComplexTest : MarshalByRefObject, IComplexTest
  {
    protected RealClass realClass;

    public string DoSomething()
    {
      return realClass.DoSomething();
    }

    public ComplexTest()
    {
      realClass = new RealClass();
    }
  }
}
And the test passes, proving that the server class can include other classes that are not marshallable or serializable without causing problems for the client. This is what I'd expect, but it's good to verify my expectations, no matter how obvious they may seem.

Infinite Operation

What if the method call, which runs on the "server", hangs? What happens to the client in that case? This is a real world situation that I have to deal with if I'm going to use remoting, because some of the failure conditions manifest in such a way that they never return to the caller. We've seen this with both AVI and DVD playback. Such is real life. So, I'm going add a unit test to call a method that will just sit in a while (true) loop:
[Test]
public void InfiniteLoopTest()
{
  ITest t1 = RemotingHelper.CreateSingletonTestObject();
  t1.InfiniteLoop();
}
I won't bore you with the interface and server-side implementation. Sure enough, the client hangs. This is Not Good. So, we want to ignore this test:
[Test]
[Ignore]
public void InfiniteLoopTest()
{
  ITest t1 = RemotingHelper.CreateSingletonTestObject();
  t1.InfiniteLoop();
}
The solution is to implement an asynchronous remoting call. I was surprised that Mr. Smacchia didn't discuss asynchronous remote calls, so I had to go back to some MSDN documentation. There are two things I want to test with an async callback--that both an async call that, well, does nothing in my test, and an async call that has an infinite loop--both of these should return immediately to the client.
[Test]
public void AsyncReturnImmediateTest()
{
  ITest t1 = RemotingHelper.CreateSingletonTestObject();
  RemoteAsyncDelegate remoteDlgt = new RemoteAsyncDelegate(
        t1.ReturnImmediately);
  remoteDlgt.BeginInvoke(null, null);
}

[Test]
public void AsyncReturnExpectedTest()
{
  ITest t1 = RemotingHelper.CreateSingletonTestObject();
  RemoteAsyncDelegate remoteDlgt = new RemoteAsyncDelegate(t1.InfiniteLoop);
  remoteDlgt.BeginInvoke(null, null);
}
Both of these tests return to the unit test engine, so we know that they are executing asynchronously. Note that the issues of signaling completion are not really in the scope of what I want to test here--I merely want to make sure that for methods that I choose, I can execute them asynchronously.

Important Understanding Gained Here

A remoted object can hang the client just as easily as if the method were running locally. Using asynchronous calls works with remoting. Of course, this is all pointing in the direction of an application specific API that handles the issues of synchronous and asynchronous calls, transparent from the controlling application. So, we see how a test-driven prototyping helps to identify design issues, such as the API having an event that is called when an async operation fails after a certain time period.

Killing The Server Thread

As I'm writing this after running the above test, I realize that while the unit tests passed, I still have the server running and one of the CPU cores is at 100%! So, what I do need to deal with is how to kill the offending thread if it does happen to go into an infinite loop. Having no idea how to do that, let's write a test and figure out what the implementation should be. First off, can I still execute other methods on that object? Since I don't want to create a bunch of these infinite loops, I'm going to move these tests to another class and use AUT's ability to sequence unit tests:
using System;
using System.Runtime.Serialization;

using Vts.UnitTest;

using Interfaces;

namespace ClientTests
{
  [TestFixture, ProcessTest]
  public class InfiniteLoopTests
  {
    public delegate void RemoteAsyncDelegate();
    public ITest infiniteObject;

    [Test, Sequence(0)]
    public void AsyncReturnExpectedTest()
    {
      infiniteObject = RemotingHelper.CreateSingletonTestObject();
      RemoteAsyncDelegate remoteDlgt = new RemoteAsyncDelegate(
         infiniteObject.InfiniteLoop);
      remoteDlgt.BeginInvoke(null, null);
      Assertion.Assert(true, "Expected async return.");
    }

    [Test, Sequence(1)]
    public void ExecuteOtherMethodsOnInfiniteObjectTest()
    {
      infiniteObject.ReturnImmediately();
      Assertion.Assert(true, "Expected return.");
    }
  }
}
And indeed, ExecuteOtherMethodsOnInfiniteObjectTest does return. This is expected, since the BeginInvoke call should be running on a separate thread. But how do we find out what that thread is? One option might be to set the thread ID in an "out" parameter of the async method, so that the client can tell the server to later on kill that thread. Let's try that with a different infinitely looping method:
public void InfiniteLoop2(out int threadId)
{
  threadId = Thread.CurrentThread.ManagedThreadId;

  while (true) { };
}
And the modified unit test, which first validates that we get the thread ID:
[Test, Sequence(0)]
public void AsyncReturnExpectedTest()
{
  infiniteObject = RemotingHelper.CreateSingletonTestObject();
  RemoteAsyncDelegate remoteDlgt = new RemoteAsyncDelegate(
      infiniteObject.InfiniteLoop2);
  remoteDlgt.BeginInvoke(out threadId, null, null);
  Assertion.Assert(true, "Expected async return.");
  Assertion.Assert(threadId != 0, "Expected thread ID to be initialized.");
}
And guess what? The second assertion fails! The out parameter is not set until the method returns, which it never does. This may have seemed obvious to many readers, but the point is to test the obvious. So this mechanism for returning the thread ID will not work.

What Now?

To get my hands dirty with a different approach, I tried the following:
public void InfiniteLoop()
{
  ThreadHelper.SaveThreadInformation("Test.InfiniteLoop");
  while (true) { };
}
Here a static class is used to manage information, at the server, about the executing thread:
using System;
using System.Collections.Generic;
using System.Threading;

namespace Server
{
  public static class ThreadHelper
  {
    internal static Dictionary<string, Thread> threadMap = 
        new Dictionary<string, Thread>();

    public static void SaveThreadInformation(string methodName)
    {
      threadMap[methodName] = Thread.CurrentThread;
    }

    public static void KillThread(string methodName)
    {
      Thread thread = threadMap[methodName];
      thread.Abort();
    }
  }
}
And by creating an AsyncManagement class and its interface:
using System;

using Interfaces;

namespace Server
{
  public class AsyncManagement : MarshalByRefObject, IAsyncManagement
  {
    public void KillThread(string methodName)
    {
      ThreadHelper.KillThread(methodName);
    }
  }
}
I can write the following unit test (having reverted the Sequence(0) test back to its original form):
[Test, Sequence(2)]
public void KillThread()
{
  IAsyncManagement asyncMgr = RemotingHelper.CreateAsyncManagement();
  asyncMgr.KillThread("Test.InfiniteLoop");
}
Running this sequence of tests does confirm that the thread aborts (the CPU usage goes back to normal).

Important Understanding Gained Here

Obviously, these tests ignore the issues of re-entrant calls, cleaning up the thread dictionary, and the clunky mechanism of using a string as the key to identify the thread. It also doesn't deal with what the application should do to recover/reset itself from a service whose thread had to be aborted. However, I did learn that the thread can be aborted at the server, and I did learn that other mechanisms, such as returning the thread ID in an out parameter, don't work. Ian Griffiths also writes about the evils of Thread.Abort, however, in our scenario, if a call never returns, the entire subsystem needs to be reset, so issues like cleanup during a "finally" block are not a concern for us. Furthermore, this isn't a guaranteed way of terminating the thread. According to MSDN, regarding Thread.Abort: When this exception is raised, the runtime executes all the finally blocks before ending the thread. Since the thread can do an unbounded computation in the finally blocks, or call Thread.ResetAbort to cancel the abort, there is no guarantee that the thread will ever end. Therefore, this is only one possible approach. It may be necessary to kill the entire process (something Ian Griffiths suggests--using processes). This takes us to the next step in the test driven prototyping.

Killing And Restarting The Server

There may be times when the entire remote server needs to be restarted, especially when a thread fails to abort or some other critical error occurs. To test starting, killing, and restarting the server process, I'm going to write a few sequenced tests with the idea in mind to not only verify starting and stopping the server process, but also to determine what happens when I try to access an object that is attached to a now dead process. The initial tests look like this:
using System;
using System.Diagnostics;
using System.Runtime.Serialization;

using Vts.UnitTest;

using Interfaces;

namespace ClientTests
{
  [TestFixture, ProcessTest]
  public class ProcessStartStopTests
  {
    protected Process p;
    ITest test;

    [Test, Sequence(0)]
    public void StartProcess()
    {
      p = new Process();
      p.StartInfo.FileName = @"..\..\..\Server\bin\debug\Server.exe";
      p.Start();
    }

    [Test, Sequence(1)]
    public void CreateObject()
    {
      test = RemotingHelper.CreateSingletonTestObject();
    }

    [Test, Sequence(2)]
    public void KillProcess()
    {
      p.Kill();
    }

    [Test, Sequence(3)]
    public void TestObjectOnDeadProcess()
    {
      test.Value1 = 1;
    }
  }
}
To run the tests, I disable the other unit tests, as this particular set of tests will launch the server:

Running the tests reveals that, when attempting to use the test object, a System.Net.WebException is thrown with the Message "Unable to connect to the remote server". Furthermore, there's an obvious delay as .NET discovers that the connection has failed. To get this test to pass, I added the ExpectedException:
[Test, Sequence(3)]
[ExpectedException(typeof(System.Net.WebException))]
public void TestObjectOnDeadProcess()
{
  test.Value1 = 1;
}
The next test verifies that, if the server is restarted, that nothing else is necessary to create new remote objects:
[Test, Sequence(4)]
public void ReInitializeObjectsTest()
{
  p = new Process();
  p.StartInfo.FileName = @"..\..\..\Server\bin\debug\Server.exe";
  p.Start();
  test = RemotingHelper.CreateSingletonTestObject();
}

Events

The next thing I'd like to test is how events (and asynchronous events in particular), fired by the remote object, are handled by the client. Is there anything special that needs to be done? Here's the unit test:
using System;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Threading;

using Vts.UnitTest;

using Interfaces;

namespace ClientTests
{
  [TestFixture]
  public class EventTests
  {
    internal delegate void RemoteAsyncDelegate();
    protected bool fired = false;
    protected ITest test;

    [Test]
    public void EventTest()
    {
      test = RemotingHelper.CreateSingletonTestObject();
      test.MyEvent += new EventHandler(OnMyEvent);
      RemoteAsyncDelegate remoteDlgt = new RemoteAsyncDelegate(
         test.FireEvent);
      remoteDlgt.BeginInvoke(null, null);
      Thread.Sleep(100);
      Assertion.Assert(fired, "Expected event to be fired.");
    }

    protected void OnMyEvent(object sender, EventArgs e)
    {
      fired = true;
    }
  }
}
And the concrete implementation:
public void FireEvent()
{
  if (MyEvent != null)
  {
    MyEvent(this, new EventArgs());
  }
}
You will note that the unit test calling the FireEvent method on the remote object asynchronously. In turn, if the event is wired up, it should call back to the client. This code does not work (to some this will be obvious). A SerializationException "ClientTests is not marked as serializable" is thrown when the event is wired up. Why? Because the event is being wired up across application boundaries--the client and the server--and therefore server, which actually has an instance of ITest, has no knowledge of the instance that is sinking the event. Just for giggles, let's see what happens when the class is marked [Serializable]. This is the mechanism used for reference by value remoting, and creates a clone of the object. I do not expect that marking the client-side class as [Serializable] will solve the event wire-up problem. And it doesn't. In this case I get a System.Security.SecurityException "Type System.DelegateSerializationHolder and the types derived from it (such as System.DelegateSerializationHolder) are not permitted to be deserialized at this security level." And interesting error, isn't it?

What Now?

OK, so obviously something does have to be done to allow events to be wired up between the client and the remote object. I'm sure glad I'm writing tests, otherwise I'd have a lot of the design and code in place before discovering this little problem. Googling for "remoting events" brings up numerous links, the most useful I found being Working With Events Over Remoting by Scott Stewart. And while this example is in VB (I find it almost unreadable, but that's my problem), I Googled for "remoting event shim" based on the phrase "event shim" in Mr. Stewart's article and found the Developmenter C# example. Interestingly, this example does not include something that I felt was important in Mr. Stewart's writeup, namely: ...and it overrides InitializeLifetimeService with a return of nothing. Our helper class is going to be referenced by both our client and server code, so we need to make sure it can be serialized and that the instance we create stays in place forever when created. If we don't do this, we will have problems with the event helper class getting garbage collected. So, things might seem like they are working fine at first, but once the event helper gets garbage collected we would no longer receive the events from the remote object.

Changes To The Unit Tests

The unit tests have to be changed to use the configuration file. A side effect of using a configuration file on the server is that, whatever .NET is doing, makes the client-side code based initialization incompatible with the server. So both need to use a configuration file for the channel to be compatibly initialized. The TestFixtureSetup and TestFixtureTearDown methods have been added to each of the unit test classes where appropriate:
[TestFixtureSetUp]
public void TestFixtureSetup()
{
  RemotingHelper.LoadConfiguration("clientConfig.xml");
}

[TestFixtureTearDown]
public void TestFixtureTearDown()
{
  RemotingHelper.UnloadConfiguration();
}
And the respective helper methods are:
public static void LoadConfiguration(string configFile)
{
  RemotingConfiguration.Configure(configFile, false);
}

public static void UnloadConfiguration()
{
  IChannel[] channels=ChannelServices.RegisteredChannels;

  foreach (IChannel channel in channels)
  {
    ChannelServices.UnregisterChannel(channel);
  }
}

Changes To The Client And Server

Both links above provide examples that use configuration files to register the channel and service types. While I wanted to avoid this approach, there is a lot going on behind the scenes with RemotingConfiguration.Configure that I've not figured out, even using Reflector. So at some point I'll take a look at how to register the channel and service types in code, but to continue the event tests, I've switched over to using the configuration files.

Server Configuration

The server configuration file replaces all the initialization code with XML. The server startup becomes three lines:
static void Main(string[] args)
{
  RemotingConfiguration.Configure("serverConfig.xml", false);
  Console.WriteLine("Press a key to stop the server...");
  Console.Read();
}
And the XML file is:
<!--<span class="code-comment"> server.exe.config --></span>
<configuration>
  <system.runtime.remoting>
    <application>
      <service>
        <wellknown mode="SingleCall" type="Server.Test, server" 
            objectUri="SingleCallTestService" /> 
        <wellknown mode="Singleton" type="Server.Test, server" 
            objectUri="SingletonTestService" />
        <wellknown mode="SingleCall" type="Server.Factory, server" 
            objectUri="FactoryService" />
        <wellknown mode="SingleCall" type="Server.AsyncManagement, server" 
            objectUri="AsyncThreadService" />
        <wellknown mode="Singleton" type="Server.ComplexTest, server" 
            objectUri="ComplexTestService" />
      </service>

      <channels>
        <channel ref="http" port="65100">
          <clientProviders>
            <formatter ref="binary"/>
          </clientProviders>
          <serverProviders>
            <formatter ref="binary" typeFilterLevel="Full"/>
          </serverProviders>
        </channel>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>
What's important here is the attribute typeFilterLevel="Full". Without this attribute, the server generates a security expection.

Client Configuration

The client configuration XML is simpler:
<!--<span class="code-comment"> client.exe.config --></span>
<configuration>
  <system.runtime.remoting>
    <application>
      <channels>
        <channel ref="http" port="0">
          <clientProviders>
            <formatter ref="binary"/>
          </clientProviders>
          <serverProviders>
            <formatter ref="binary" typeFilterLevel="Full"/>
          </serverProviders>
        </channel>
      </channels>
    </application>
  </system.runtime.remoting>
</configuration>
Again, of great importance is that it defines a server provider with the typeFilterLevel="Full". It is necessary for the client to itself act as a server for the event callback to succeed. Furthermore, when the EventHandler signature, which has parameters object sender, EventArgs e, the client's server typeFilterLevel attribute must also be set to Full for the sender object to correctly marshal.

The Event Shim

The EventShim class provides a mechanism to wire up a client side event handler with a server side, remote object:
using System;

using Interfaces;

namespace EventShimHelper
{
  [Serializable]
  public class EventShim : MarshalByRefObject
  {
    private EventHandler target;
  
    private EventShim(EventHandler target)
    {
      this.target += target;
    }

    public void Sink(object sender, EventArgs e)
    {
      target(sender, e);
    }

    public override object InitializeLifetimeService()
    {
      return null;
    }

    public static EventHandler Create(EventHandler target)
    {
      EventShim shim = new EventShim(target);
      return new EventHandler(shim.Sink);
    }
  }
}
Rather than get into the details of how and why the EventShim works, which is not really the purpose of this article, I'd recommend you read Scott Stewart's article and the source code from the Developmentor link provided above.

The Event Unit Tests

We can now get back on track with the unit tests to see what other issues come up that might affect our design.
[Test]
public void EventTest()
{
  test = RemotingHelper.CreateSingletonTestObject();
  EventHandler eh = EventShim.Create(new EventHandler(OnMyEvent));
  test.MyEvent += eh;
  RemoteAsyncDelegate remoteDlgt = new RemoteAsyncDelegate(test.FireEvent);
  remoteDlgt.BeginInvoke(null, null);
  Thread.Sleep(250);
  test.MyEvent -= eh;
  Assertion.Assert(fired, "Expected event to be fired.");
}

protected void OnMyEvent(object sender, EventArgs e)
{
  fired = true;
}

An Exception Occurring In The Event Handler

What happens when an exception occurs in the event handler? Let's test that!
[Test]
public void EventExceptionTest()
{
  test = RemotingHelper.CreateSingletonTestObject();
  EventHandler eh = EventShim.Create(new EventHandler(OnExceptionEvent));
  test.MyEvent += eh;
  test.FireEvent();
  test.MyEvent -= eh;
}

protected void OnExceptionEvent(object sender, EventArgs e)
{
  throw new ApplicationException("Exception Handler Test");
}

In this case, both unit tests pass:

However the server catches the client-side exception because of the try-catch block around the delegate on the server:

public void FireEvent()
{
  try
  {
    if (MyEvent != null)
    {
      Console.WriteLine("Firing event.");
      MyEvent(this, EventArgs.Empty);
    }
  }
  catch (Exception e)
  {
    Console.WriteLine(e.Message);
  }
}
Let's write a test where the delegate invoke is surrounded by a try-catch block. Here's the server-side code:
public void FireEventNoCatch()
{
  if (MyEvent != null)
  {
    Console.WriteLine("Firing event.");
    MyEvent(this, EventArgs.Empty);
  }
}

Now the client sees the exception. Now we have two problems. When running the test again (without restarting the server), the test results are different:

And as with server-side exceptions, most likely, if the client throws an exception that the server does not have the metadata for, it will cause further problems. I'll test that in a minute, but first, I need to understand why the unit tests aren't repeatable. Most likely this has to do with the unhooking of the event. Remember that this is a singleton object, so all these tests get the same instance of the remote object. When the "no server catch" test fails, the event is not unhooked, and therefore a second pass of the EventTest will fail because we now have two events wired up--one that throws an exception (by calling OnExceptionEvent) and one that calls into OnMyEvent. Adding a try-finally block makes the unit tests repeatable:

[Test]
[ExpectedException(typeof(ApplicationException))]
public void EventExceptionNoServerCatchTest()
{
  test = RemotingHelper.CreateSingletonTestObject();
  EventHandler eh = EventShim.Create(new EventHandler(OnExceptionEvent));
  test.MyEvent += eh;

  try
  {
    test.FireEventNoCatch();
  }
  finally
  {
    test.MyEvent -= eh;
  }
}
And the ExpectedException attribute is added to make the unit test turn green.

Client-side Specialized Exceptions

Now let's look at a custom event at the client:
[Test]
public void ThrowSpecializedClientExceptionTest()
{
  test = RemotingHelper.CreateSingletonTestObject();
  EventHandler eh = EventShim.Create(new EventHandler(
      OnSpecializedExceptionEvent));
  test.MyEvent += eh;

  try
  {
    test.FireEventNoCatch();
  }
  finally
  {
    test.MyEvent -= eh;
  }
}
In this case, the exception itself generates a SerializationException "Type 'ClientTests.ClientException' in Assembly 'ClientTests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable." Marking the client specialized exception [Serializable] generates another SerializationException "Unable to find assembly 'ClientTests, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'." So, to get a green unit test, the ExpectedException attribute must be defined:
[Test]
[ExpectedException(typeof(System.Runtime.Serialization.
       SerializationException))]
public void ThrowSpecializedClientExceptionTest()
{
  test = RemotingHelper.CreateSingletonTestObject();
  EventHandler eh = EventShim.Create(new EventHandler(
         OnSpecializedExceptionEvent));
  test.MyEvent += eh;

  try
  {
    test.FireEventNoCatch();
  }
  finally
  {
    test.MyEvent -= eh;
  }
}
What's interesting as well is that, unlike the "no server catch test", which throws an exception type known to both the client and the server, the SerializationException refires after "finally" block. So, the unit test EventExceptionNoServerCatchTest does not need an ExpectedException attribute because the exception is caught and consumed by the unit test's try block. The same is not true for a SerializationException.

Single Call Instantiation Events

Just for giggles, let's see what happens when a single call instantiation is wired up with an event. Does the event fire? One would expect not, because the call to FireEvent results in a new instance of the remote object. And indeed, the following unit test passes:
[Test]
public void SingleCallInstantiationTest()
{
  fired = false;
  test = RemotingHelper.CreatePerCallTestObject();
  EventHandler eh = EventShim.Create(new EventHandler(OnMyEvent));
  test.MyEvent += eh;
  RemoteAsyncDelegate remoteDlgt = new RemoteAsyncDelegate(test.FireEvent);
  remoteDlgt.BeginInvoke(null, null);
  Thread.Sleep(250);
  test.MyEvent -= eh;
  Assertion.Assert(!fired, "Expected event to NOT be fired.");
}

Important Understanding Gained Here

Server-side events that are consumed by the client require an EventShim. Exception handling must be done very carefully--the client-side event handler should not throw exceptions that are not known by the server. This was true with server-side exceptions as well. Working with events and singleton objects can lead to unexpected side effects because the event handlers must be explicitly unwired throughout the lifetime of the client connection. If they are not unwired, they continue to be added to the event multicast delegate list, resulting in all events being fired. There are side effects to this. An event that throws an exception will stop other events in the delegate list from firing. And of course, you may not have intended to have more than one event fire. Another factor in dealing with event exceptions is whether the server's event call is wrapped by an exception handler. If so, the client will not even know that the event threw an exception unless the server explicitly throws an exception that is known by the client. Client callbacks for events wired up to single call instantiation service types will simply not work at all. In my opinion, client-side event handlers should be used when working with factory created remote objects rather than singleton remote objects.

Conclusion

These tests are intended to drive the design of the final application. The tests reveal that the design must consider:
  • Issues of single call activation, singleton, and factory instantiation
  • Working with exception types the client might not know about
  • Using asynchronous calls to prevent the client from locking up
  • A mechanism to alert the client of a "hung" remote method call
  • A mechanism for attempting to kill the hung thread
  • A mechanism for killing and restarting the remote server
  • Resetting the connection and re-establishing remote objects in a coordinated manner
  • Remote object events require an EventShim
  • The server and client channels and service types are probably configured easiest using an XML configuration file
  • Wiring up client event handlers to remote objects is complicated by:
    • singleton remote objects
    • client-side specialized exceptions that may occur in the event handler
    • exception handling that may or may not exist in the server implementation when firing the event
    • single call instantiation objects cannot have events wired up to them.
By using a test-driven approach, many issues were discovered that have a direct impact on the design of any application that is considering utilizing .NET remoting.

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

Share

About the Author

Marc Clifton

United States United States
Marc is the creator of two open source projets, MyXaml, a declarative (XML) instantiation engine and the Advanced Unit Testing framework, and Interacx, a commercial n-tier RAD application suite.  Visit his website, www.marcclifton.com, where you will find many of his articles and his blog.
 
Marc lives in Philmont, NY.

Comments and Discussions

 
QuestionIsnt Remoting Dead? Pinmemberdevnet24723-Apr-07 21:36 
AnswerRe: Isnt Remoting Dead? PinprotectorMarc Clifton24-Apr-07 1:06 
GeneralRe: Isnt Remoting Dead? PinmemberJames Ashley11-May-07 7:44 
GeneralRe: Isnt Remoting Dead? PinprotectorMarc Clifton11-May-07 8:23 
James Ashley wrote:
A big part of the discussion you linked seems to revolve around being able to use the same component on both the server and the client

 
I'm using interfaces, namely IFactory and ITest so the component needs to live only on the server.
 
James Ashley wrote:
as CSLA.NET does

 
Eww. I didn't realize that about CSLA. Definitely is not on my recommend list then.
 
James Ashley wrote:
As for using the same components on client and server, I thought this was always a bad idea anyways

 
I think it is too.
 
James Ashley wrote:
or is this also something that can't be done easily using WCF?).

 
That I'm not sure about. I saw a demo of WCF but now I can't recall whether I saw using interfaces or not. I can't imagine it wouldn't be able to though. That's pretty fundamental nowadays.
 
Marc
 

Thyme In The Country

Interacx

People are just notoriously impossible. --DavidCrow
There's NO excuse for not commenting your code. -- John Simmons / outlaw programmer
People who say that they will refactor their code later to make it "good" don't understand refactoring, nor the art and craft of programming. -- Josh Smith


GeneralRe: Isnt Remoting Dead? PinmemberJames Ashley11-May-07 8:38 

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

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140926.1 | Last Updated 7 Apr 2007
Article Copyright 2007 by Marc Clifton
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid