Friday, June 19, 2009

Disconnect the client from WCF service and notifiy the client before disconnecting.

A colleague of mine was asking me to create a Client-Server demo application using WCF. The following are the requirements:

1>Service instance mode is “PerSession”.
2>Session mode has to be “Required”.
3>WCF Server will be able to terminate a session at any time. In other words, WCF Server can disconnect a WCF client.
4>Before terminating the session, the WCF client will be notified.

In order to meet the above requirement, I have used the WCF Callback for notifying the Client and disconnect the client by closing the incoming channel. Let’s see step by step of the implementation.

Step 1> Create the WCF Contact:

[ServiceContract(CallbackContract=typeof(INotifyClientCallback), SessionMode=SessionMode.Required)]
public interface IService1
{
[OperationContract]
bool StartingService();
}
public interface INotifyClientCallback
{
[OperationContract(IsOneWay = true)]
void Disconnecting();
}

INotifyClientCallback interface for Callback.

Step 2> Implementation of the Contact:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession)]
public class Service1 : IService1
{

private static readonly Dictionary subscribers = new Dictionary();
public static event EventHandler onClientAdded;

///
/// Returns the IP Address of the Client
///

///
public string GetAddressAsString()
{
if (!OperationContext.Current.IncomingMessageProperties.ContainsKey(RemoteEndpointMessageProperty.Name))
{
return "127.0.0.1";
}
RemoteEndpointMessageProperty clientEndpoint =
OperationContext.Current.IncomingMessageProperties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;
return clientEndpoint.Address;
}


public bool StartingService()
{
//Get the callback reference
INotifyClientCallback callback = OperationContext.Current.GetCallbackChannel();

string IPAddress = GetAddressAsString();
lock (subscribers)
{
if (!subscribers.ContainsKey(IPAddress))
{
subscribers[IPAddress] = new CommunicationStore() { NotifyCallback = callback, IService = OperationContext.Current.InstanceContext };
if (onClientAdded != null)
{
onClientAdded(IPAddress, null);
}
}
}
return true;
}


public static void Disconnect(string ipAddress)
{
if (subscribers.ContainsKey(ipAddress))
{
CommunicationStore com = subscribers[ipAddress];
if (((ICommunicationObject)com.NotifyCallback).State == CommunicationState.Opened)
{
try
{
//fires the callback method
com.NotifyCallback.Disconnecting();
com.IService.IncomingChannels.FirstOrDefault().Close();
}
catch (Exception)
{
throw;
}
}
}
}

}
public class CommunicationStore
{
public InstanceContext IService { get; set; }
public INotifyClientCallback NotifyCallback { get; set; }
}

Some important points of the service implementation:
a>Use a static dictionary to keep the Client’s IP and callback channel. Before writing on the share object, lock the object.

b>Gets the IP address of the client using the GetAddressAsString method. You can get the IP of the client from the incoming message. The following statement shows how can we get the IP adddress of the Client in WCF:

RemoteEndpointMessageProperty clientEndpoint = OperationContext.Current.IncomingMessageProperties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;
String ipAddress = clientEndpoint.Address;

If you are using the namepipe binding, you will not get the RemoteEndpointMessageProperty.

c>When the client creates the proxy of the service, it will call StartingService method immediately. Inside the StartingService method, I am keeping the callback channel of the client and current instance into the dictionary.

d>When the user of WCF service wants to disconnect a client, he/she will call the Disconnect method with the IP Address of the client.

e>The Disconnect method uses the IP Address to get the callback channel of the client and associate service instance of the client from the dictionary. Eventually, it notifies the client by using callback channel and close the incoming channel.

I am done with the service implementation.

Step 3> Implementation of the callback and create the proxy of the service:

public class Proxy
{
public static DuplexChannelFactory factory;
static IService1 _IService1;
private static bool Connect()
{

try
{
WSDualHttpBinding binding = new WSDualHttpBinding();

EndpointAddress address1 = new EndpointAddress(@"http://localhost:9008/Service1");

EndpointAddress address = new EndpointAddress(@"net.tcp://localhost:9002/");
NetTcpBinding nettcp = new NetTcpBinding();
nettcp.ReliableSession.Enabled = true;
nettcp.ReliableSession.InactivityTimeout = new TimeSpan(0, 0, 30);

NetNamedPipeBinding namepipe = new NetNamedPipeBinding();
EndpointAddress addressPipe = new EndpointAddress(@"net.pipe://localhost/");


InstanceContext context = new InstanceContext(new NotifyCallback());

binding.ClientBaseAddress = new Uri(@"http://localhost:9010/");

factory = new DuplexChannelFactory(context, binding, address1);

_IService1 = factory.CreateChannel();
_IService1.StartingService();
}
catch (Exception ex)
{

}
return true;
}

}

public static IService1 ServiceProxy
{
get
{
if (_IService1 == null)
{
Connect();
}

return _IService1;
}
}
}


public class NotifyCallback : INotifyClientCallback
{

#region INotifyClientCallback Members

public void Disconnecting()
{
MessageBox.Show("You are about to disconnect from the service");
}

#endregion
}
Points to ponder of the above code snippet:

a>When you use the WCF callback, you have to use the DuplexChannelFactory rather then ChannelFactory to create the proxy.Othewise, you can generate the proxy.

b>WCF callback supports the following binding: WSDualHttpBinding, NetTcpBinding and NetNamedPipeBinding. When you use the WSDualHttpBinding, don’t forget to set the ClientBaseAddress.

c>Implement the callback interface.

That’s it.

If you need the source code of this demo, you can email me.

Happy Programming!

2 comments:

Unknown said...

That's a very good article! Thanks for the help :)

Turjo

Priyanka said...

RemoteEndpointMessageProperty clientEndpoint = OperationContext.Current.IncomingMessageProperties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty;
String ipAddress = clientEndpoint.Address;


ClientEndpoint.Address gives me blank. I am trying to run my service in Visual studio 2010 ultimate.