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 sorts of good articles & blogs, I've reached a nice project which I've decided to share with you in a kind of tutorial structure to help whoever finds this interesting - step by step.
I know the code could and 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 separately, feel free to jump over a few steps or go directly to the final step...
- Simple TCP
- Simple MSMQ
- Simple Duplex
- MSMQ Duplex
- Simple Dynamic proxy
- 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} received Execute request (id {0})", identifier, DateTime.Now);
}
public void GetData(Guid identifier)
{
Console.WriteLine("{1} received GetData request (id {0})", identifier, DateTime.Now);
ISampleContractCallback client = OperationContext.Current.GetCallbackChannel<isamplecontractcallback />();
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 the previously configured TCP-Duplex endpoint.
private static void startListening()
{
serviceHost = new ServiceHost(typeof(testFancyProxyServer.testFancyProxyService));
DuplexMsmqServices.AddToExistingServiceHost(serviceHost,
typeof(testFancyProxyServer.testFancyProxyService),
typeof(ISampleContract),
ConfigurationSettings.AppSettings["MSMQServerURI"]);
serviceHost.Open();
}
In the client, we will see a proxy very similar to the previous step.
The proxy here uses:
- '
NetworkChange.NetworkAvailabilityChanged
' to track the network availability. DynamicTargetProxyInterceptor
which wraps "Castle DynamicProxy" 'ChangeInvocationTarget
' to transparently switch between the 'online' and 'offline' proxies.
public class DynamicTargetProxyInterceptor<t> : 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);
}
}
- 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 the 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;
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<isamplecontractsync> 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
<isamplecontractsync>(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} execute {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 does it notice 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