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 DynamicTargetProxyFactoryThe 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 Downloaddp; 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); } }
No comments:
Post a Comment