jeudi 21 juin 2012

A WCF Sub/Pub Duplex Scenario

Recently, i been faced a huge issue in working on a project. I was seeking to make a Sub/Pub mechanism using WCF that i succeed to do. But unfortunately, i was not get out the problem because i noticed an unexpected service behavior. I succeed to subscribe for the service notifications but after closed the client, others subscriptions didn't work. After solved the issue, i decided to share it with community. There is the approach i used.

Service Side

I try to make the Sub/Pub implemenation as generic as possible to be able to re-use it in others service.

Generic Implementation

ISubscribeService
Firstly, i begin to define a ISubscribeService Interface wich will allow me after to have an implementation Class to make subscription on the service for the notifications.
This is the interface definition:

using System.ServiceModel;

[ServiceContract]
public interface ISubscribeService
{
/// <summary>
/// This method should be executed to subscribe to notifications from server.
/// </summary>
[OperationContract(IsOneWay = true)]
void SubscribeToNotifications(string clientId);

/// <summary>
/// This method should be executed to unsubscribe to notifications from server.
/// </summary>
[OperationContract(IsOneWay = true)]
void UnsubscribeToNotifications(string clientId);
}


ServiceBase<T>
This is the generic class used to write the Sub/Pub mechanism for the service.

using System;
using System.ServiceModel;
using System.Timers;

public class ServiceBase<T>
{
#region variables
#endregion

#region Constructors
protected ServiceBase()
{
SyncRoot = new Object();
Timer = new Timer();
}
#endregion

#region Properties
protected Object SyncRoot { get; private set; }
protected Timer Timer { get; private set; }
#endregion

#region methods
/// <summary>
/// This method should be executed to subscribe to notifications from server.
/// </summary>
protected void SubscribeToNotifications(string clientId)
{
var channel = OperationContext.Current.GetCallbackChannel<T>();

//Any message from a client we haven't seen before causes the new client to be added to our list
lock (SyncRoot)
{
if (SessionsController<T>.IsClientConnected(clientId))
return;

SessionsController<T>.AddCallbackChannel(clientId, channel);
}
}

/// <summary>
/// This method should be executed to unsubscribe to notifications from server.
/// </summary>
protected void UnsubscribeToNotifications(string clientId)
{
lock (SyncRoot)
{
SessionsController<T>.DeleteClient(clientId);
}
}
#endregion
}



SessionsController<T>
A geneic Class used to handle and manage the clients sessions for the service.

using System;
using System.Collections.Generic;
using System.Linq;

public static class SessionsController<T>
{
#region Variables

#endregion

#region Constructors
/// <summary>
/// Class constructor.
/// </summary>
static SessionsController()
{
Clients = new Dictionary<string, T>();
}
#endregion

#region Properties
public static Dictionary<string, T> Clients { get; private set; }

/// <summary>
/// Gets the total callback channels.
/// </summary>
public static int TotalCallbackChannels { get { return GetCallbackChannels().Count(); } }

/// <summary>
/// Returns all available callback channels
/// </summary>
/// <returns>Callback channels</returns>
public static IEnumerable<T> AllCallbackChannels
{
get { return Clients.Select(c => c.Value); }
}
#endregion

#region Methods
/// <summary>
/// Stores given client callback channel
/// </summary>
/// <param name="clientId">Id of client's session</param>
/// <param name="callbackChannel">Callback channel for given client</param>
public static void AddCallbackChannel(string clientId, T callbackChannel)
{
if (Clients != null && Clients.Count(c => c.Key == clientId) != 0)
return;

var syncRoot = new Object();
lock (syncRoot)
{
if (Clients != null && Clients.All(c => c.Key != clientId))
{
Clients.Add(clientId, callbackChannel);
}
}
}

/// <summary>
/// Checks whether given client is connected.
/// </summary>s
/// <param name="clientId">Id of client's session</param>
/// <returns>Whether client is connected</returns>
public static bool IsClientConnected(string clientId)
{
return Clients != null && Clients.Any(c => c.Key == clientId);
}

/// <summary>
/// Returns all available callback channels except sender
/// </summary>
/// <returns>Callback channels</returns>
public static IEnumerable<T> GetCallbackChannels(string sessionId)
{
var filteredChannels = Clients.Where(channel => channel.Key != sessionId);

return filteredChannels.Select(c => c.Value);
}

/// <summary>
/// Returns callback channel for given client
/// </summary>
/// <param name="clientId">Id of client's session</param>
/// <returns>Callback channel for given client</returns>
public static T GetCallbackChannel(string clientId)
{
return Clients != null ? Clients.Single(c => c.Key == clientId).Value : default(T);
}

/// <summary>
/// Returns all available callback channels
/// </summary>
/// <returns>Callback channels</returns>
public static IEnumerable<T> GetCallbackChannels()
{
return Clients.Select(c => c.Value);
}

/// <summary>
/// Deletes callback channel for given client
/// </summary>
/// <param name="clientId">Id of client's session</param>
public static void DeleteClient(string clientId)
{
if (Clients == null || Clients.All(c => c.Key != clientId))
return;

var syncRoot = new Object();
lock (syncRoot)
{
if (Clients.Any(c => c.Key == clientId))
{
Clients.Remove(clientId);
}
}
}
#endregion
}



Use Case in an example service

In my case, i used a service named  StockService

[ServiceContract(CallbackContract = typeof(IClientCallBack))]
public interface IStockService : ISubscribeService //The service interface inherit the interface defined early.
{
//You can define your Operations Contract
}



i passed  IClientCallBack as value for the  CallbackContract property for the  ServiceContract Attribute defined below: 

public interface IClientCallBack
{
[OperationContract(IsOneWay = true)]
void SendProductsList(string[] list);
}



Once all those done, i describe the StockService implementation:


public class StockService : ServiceBase<IClientCallBack>, IStockService
{
#region Notification
/// <summary>
/// This method should be executed to subscribe to notifications from server.
/// </summary>
void ISubscribeService.SubscribeToNotifications(string clientId)
{
SubscribeToNotifications(clientId);
}

/// <summary>
/// This method should be executed to unsubscribe to notifications from server.
/// </summary>
void ISubscribeService.UnsubscribeToNotifications(string clientId)
{
UnsubscribeToNotifications(clientId);
}
#endregion

public StockService()
{
Timer.Elapsed += Process;
Timer.Interval = 2000;
Timer.Enabled = true;
Timer.Start();
}

private void Process(object sender, EventArgs e)
{
var list = new List<string>();

for(var i = 0; i <100; i++)
{
list.Add("Item" + i);
}

if (SessionsController<IClientCallBack>.TotalCallbackChannels <= 0)
return;

lock(SyncRoot)
{
var allChannels = SessionsController<IClientCallBack>.GetCallbackChannels();

allChannels.ToList().ForEach(c => c.SendProductsList(list.ToArray()));
}
}
}



This is the ServiceModel configuration used in my web.config:
<system.serviceModel>
<services>
<service behaviorConfiguration="StockServiceTypeBehavior" name="Stock.StockService">
<endpoint address="" binding="wsDualHttpBinding" contract="Stock.IStockService"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="StockServiceTypeBehavior">
<!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="true"/>
<!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>

After that, all it's done for the service. let's go to check out for the Client.


Client Side


I begin to define a  CallbackController

internal class CallbackController : IStockServiceCallback
{
public void SendProductsList(string[] list)
{
MessageMediator.SendMessage(list, "Tag"); //You have to use a Message Mediator implementation
}



I used a Catel ViewModel in my client:

/// <summary>
/// MainWindow view model.
/// </summary>
[CallbackBehavior(UseSynchronizationContext = false)]
public class MainWindowViewModel : ViewModelBase
{
#region Variables
private StockServiceClient Proxy { get; set; }
private string ClientId { get; set; }
private StockServiceClient Client { get; set; }
#endregion

#region Constructor & destructor
/// <summary>
/// Initializes a new instance of the <see cref="MainWindowViewModel"/> class.
/// </summary>
public MainWindowViewModel()
{
MessageMediator .Register<string[]>(this,
(x) =>
{ Liste = new ObservableCollection<string>(x); },
"Tag");
ClientId = Guid.NewGuid().ToString();
Client = new StockServiceClient(new InstanceContext(new CallbackController()));
Client.SubscribeToNotifications(ClientId);
}
#endregion

#region Properties
/// <summary>
/// Gets the title of the view model.
/// </summary>
/// <value>The title.</value>
public override string Title { get { return "View model title"; } }

/// <summary>
/// Gets or sets the property value.
/// </summary>
public ObservableCollection<string> Liste
{
get { return GetValue<ObservableCollection<string>>(ListeProperty); }
set { SetValue(ListeProperty, value); }
}

/// <summary>
/// Register the Liste property so it is known in the class.
/// </summary>
public static readonly PropertyData ListeProperty = RegisterProperty("Liste", typeof(ObservableCollection<string>));
#endregion

#region Commands
// TODO: Register commands with the vmcommand or vmcommandwithcanexecute codesnippets
#endregion

#region Methods

protected override void Close()
{
Client.UnsubscribeToNotifications(ClientId);
}

#endregion
}



This is the ServiceModel configuration in my app.config:
<system.serviceModel>
<bindings>
<wsDualHttpBinding>
<binding name="WSDualHttpBinding_IStockService" />
</wsDualHttpBinding>
</bindings>
<client>

<endpoint address="http://localhost:807/StockService.svc" binding="wsDualHttpBinding"
bindingConfiguration="WSDualHttpBinding_IStockService" contract="Service.IStockService"
name="WSDualHttpBinding_IStockService">
<identity>
<userPrincipalName value="Host\Rajiv" />
</identity>
</endpoint>
</client>
</system.serviceModel>

That's all.

I just hope that help someone (really sorry for the presentation, i will try to improve :)).
Waiting yours feedback.

samedi 28 avril 2012

Just Blogged

Hi all,
this is my first message on my brand new blog. Here i will try to share experience that i gain during solving different issue that I'm facing all days in my developer life.

Warm regards.