Monday, February 15, 2010

The "Fancy Proxy" - having fun with WCF - 6-DynamicDuplexProxy

A little project I was doing at home lately lead me to search for a solution to disconnected application.

In my imagination I imagined an application that knows to work both "online" and "offline"...not just that but also knows how to switch between them when needed and of course - controlled from WCF configuration or other infrastructure that will be as "transparent" as possible for the programmer who writes the application.

Sounds a little like science fiction? not quite...

Gathering information from all sort of good articles & blogs I've reach to a nice project which I've decided to share with you in a kind of tutorial structure to help who ever finds this interesting - step by step.
I know the code could & should pass a bit of polish, but hey! remember it's just an idea not production material :-)

Each step in this "tutorial" represents a step in the way for the full solution, this is only to help understand each concept seperately, feel free to jump over a few steps or go directly to the final step...

1- Simple TCP
2- Simple MSMQ
3- Simple Duplex
4- MSMQ Duplex
5- Simple Dynamic proxy
6- Dynamic & Duplex proxy

This is the final step of this tutorial.

Let's review the main modifications from previous steps and test it.

The final step shows a combination of two previously reviewed steps -
TCP-Duplex and MSMQ-Duplex.
The TCP-Duplex will work when the client is connected to the server and the MSMQ-Duplex will work when it's disconnected.
We'll start with the contract, here we can see a regular duplex contract built from two "one-way" interfaces, the 2nd interface is the callback interface of the 1st one.

[ServiceContract(CallbackContract = typeof(ISampleContractCallback))]
public interface ISampleContract
{
 [OperationContract(IsOneWay = true)]
 void GetData(Guid identifier);

 [OperationContract(IsOneWay = true)]
 void Execute(Guid identifier);
}

public interface ISampleContractCallback
{
 [OperationContract(IsOneWay = true)]
 void SendData(Guid identifier, string answer);
}


The server implementation of this contract is very simple, the only thing worth mentioning here is the 'GetCallbackChannel' call which allows us to retrieve the client's endpoint & allows the server to send back an 'answer' (see 3- Simple Duplex step for more information on this).

public void Execute(Guid identifier)
{
 Console.WriteLine("{1} recieved Execute request (id {0})", identifier, DateTime.Now);
}

public void GetData(Guid identifier)
{
 Console.WriteLine("{1} recieved GetData request (id {0})", identifier, DateTime.Now);

 ISampleContractCallback client = OperationContext.Current.GetCallbackChannel();

 //get data by identifier
 string answer = "hi! from server";

 client.SendData(identifier, answer);
}


The host didn't change much from previous steps, the only change is the call for 'AddToExistingServiceHost', this helper method adds the MSMQ-Duplex endpoint to previously configured TCP-Duplex endpoint.

private static void startListening()
{

 serviceHost = new ServiceHost(typeof(testFancyProxyServer.testFancyProxyService));

          //msmq duplex server
 DuplexMsmqServices.AddToExistingServiceHost(serviceHost,
 typeof(testFancyProxyServer.testFancyProxyService),
 typeof(ISampleContract),
 ConfigurationSettings.AppSettings["MSMQServerURI"]);


 // Open the ServiceHostBase to create listeners and start 
 // listening for messages.
 serviceHost.Open();
}


In the client we will see a proxy very similar to previous step.
The proxy here uses:

1. 'NetworkChange.NetworkAvailabilityChanged' to track the network availability.

2. DynamicTargetProxyInterceptor which wraps "Castle DynamicProxy" 'ChangeInvocationTarget' to transparently switch between the 'online' and 'offline' proxies.

/// 
/// this class intercepts each call to proxy
/// and check if it needs to switch to secondary target
/// 
public class DynamicTargetProxyInterceptor : IInterceptor
{
 private readonly T _secondaryTarget;

 public DynamicTargetProxyInterceptor(T secondaryTarget)
 {
 _secondaryTarget = secondaryTarget;
 }     

 public void Intercept(IInvocation invocation)
 {
  var primaryTarget = invocation.InvocationTarget as IProxySelectorDesicion;

  if (primaryTarget.IsOnline == false)
  {
   ChangeToSecondaryTarget(invocation);
  }
 invocation.Proceed();
 }

 private void ChangeToSecondaryTarget(IInvocation invocation)
 {
  var changeProxyTarget = invocation as IChangeProxyTarget;
  changeProxyTarget.ChangeInvocationTarget(_secondaryTarget);
 }
}


3. Nicolas Dorier's MSMQ Duplex to implement the 'offline' proxy - using the same duplex contract over MSMQ on both client & server - allowing a duplex dialog between them.

Playing a bit with previous dynamic proxy sample I've noticed that when switching from the 'offline' proxy back to previously 'used' TCP proxy I get a CommunicationException - the solution for this included registering to ICommunicationObject.Faulted event to handle this exception by recreating a new 'online' proxy:
void SampleContractProxy_Faulted(object sender, EventArgs e)
{
 ((ICommunicationObject)sender).Abort();

 if (sender is ISampleContract)
 {
  sender = CreateProxy(currentEndPoint);
 } 
}


Another modification is the 'OfflineWaitTimeOut' property which allows the proxy's consumer to wait for the MSMQ-Duplex message to arrive getting a sync-like behavior, this way the code's flow is cleaner but it has an obvious cost - the client actually waits for the answer (go figure...:-)).
Anyway, like in previous sample the proxy also contains the 'AnswerArrived' event which will trigger when the server 'answers' immediately if we set the 'OfflineWaitTimeOut' to 0 or when we reach the 'OfflineWaitTimeOut' if set (it can also be set to infinite time out - not really recommended, but the option exists..).

public string GetDataSync(Guid identifier)
{
 Console.WriteLine("enter GetDataSync {0}", DateTime.Now.ToString("hh:MM:ss"));
 GetData(identifier);

 wait4Signal();

 Console.WriteLine("leave GetDataSync {0}", DateTime.Now.ToString("hh:MM:ss"));

 return Answer;
}

public void SendData(Guid identifier, string answer)
{
 wait4Event.Set();
 Answer = answer;

 //this event can be usefull to recieve answers
 //in offline mode when time-out is defined
 if (AnswerArrived != null)
 {
  AnswerArrived(this, new AnswerArrivedArgs(identifier, answer));
 }
}

private void wait4Signal()
{
 if (wait4Event == null)
 { 
  wait4Event = new ManualResetEvent(false);
 }

 wait4Event.WaitOne(offlineWaitTimeOut);
 wait4Event.Reset();
}


Testing the solution...

Looking at the proxy's consumer code:

- We create two proxies one represents the 'online' endpoint & the other one the 'offline' endpoint.
- We send both to the 'DynamicTargetProxyFactory'.
- We call the server's methods in a loop while connecting and disconnecting from the network.
public class testFancyProxyConsumer
{
 private const string ONLINE_ENDPOINT = "Online";
 private const string OFFLINE_ENDPOINT = "Offline";

 private SampleContractProxy onlineProxy;
 private SampleContractProxy offlineProxy;
 private ISampleContractSync proxy;
 private DynamicTargetProxyFactory dp;


 public void Run()
 {
  onlineProxy = new SampleContractProxy(ONLINE_ENDPOINT, true);
  offlineProxy = new SampleContractProxy(OFFLINE_ENDPOINT, true);

  offlineProxy.OfflineWaitTimeOut = 1000;
  offlineProxy.AnswerArrived += new SampleContractProxy.AnswerArrivedHandler(offlineProxy_AnswerArrived);


  dp = new DynamicTargetProxyFactory(onlineProxy, offlineProxy);

  proxy = dp.GetCurrentTarget();

  Guid testGuid;

  for (int i = 0; i < 10; i++)
  {
   testGuid = Guid.NewGuid();

   proxy.Execute(testGuid);
 
   Console.WriteLine(string.Format("{1} excute {0}", testGuid, DateTime.Now));

   Console.WriteLine(string.Format("{3} GetDataSync {0} on '{1}' proxy result:{2}",
      testGuid, proxy.CurrentEndPoint,
      proxy.GetDataSync(testGuid), DateTime.Now));


   Console.ReadLine();
  }
 }

 private void offlineProxy_AnswerArrived(object sender, AnswerArrivedArgs args)
 {
  Console.WriteLine("answer finally arrived, identifier={0}, answer={1}", args.Identifier, args.Answer);
 }
}
The result: The proxy handles everything and switches between the two proxies, the proxy's consumer doesn't need to do anything about this nor it notices the switches and even more important - all calls reached the server - isn't it a fancy proxy ??! :-) That's it on this subject. Feel free to ask or comment... Diego PS: Source Download

No comments:

Post a Comment