Reputation: 893
I am writing a WCF service in a Publish-Subscribe pattern.
When someone publishes an event, I don't want to straight away send it to all the clients.
I want to be able to, for each client, check if that client needs to be notified about that publish.
Basically this will be done by accessing a database, and checking if that client has subscribed for that specific event with those parameters (cannot be done in advance, needs to be checked only against database).
Currently I am working using this List-Based Publish-Subscriber sample, but it works in such a way - that when an event is published - client session is triggered separatly to send the message.
So for now, I am changing this :
public void PriceChangeHandler(object sender, PriceChangeEventArgs e)
{
_callback.PriceChange(e.Item, e.Price, e.Change);
}
to this :
public void PriceChangeHandler(object sender, PriceChangeEventArgs e)
{
// Perform some database checks using BL if this client needs to be notified about this event
// Only if answer is YES - call the callback function on that client
_callback.PriceChange(e.Item, e.Price, e.Change);
// Also - send the client an EMAIL + SMS
_emailServer.SendEmail(e.Item);
_smsServer.SendSMS(e.Item);
}
Two Questions :
Is this the right way ? and how can I know what 'this' client is ? should the client send me credentials in the 'subscribe' method that I will store ? Or should I implement a custom 'UsernameValidator' that will store the Principal ?
And shouldn't I have a static list of all the clients, that I will send to my BL, and the BL will return me only the ones I have to send the message to ?
Upvotes: 2
Views: 1640
Reputation: 893
The answer I have come up with is to implement the 'Custom UsernamePasswordValidator', and so each service instance now KNOWS what client is connected to it (this way I don't have to pass anything in Subscribe).
When a 'publish' event arrives - I would check which user it is intended to (the same user might connect from several machines).
I would then raise a 'PriceChangeEvent' with the targeted user, and the 'PriceChangeHandler' event would be raised for all client instances.
Then, inside the event - I would check if the logged principal is the targeted user, and if so - I would call the callback function on the client machine.
This saves me the trouble of saving a list of connected clients, and also I don't need to pass anything in the 'Subscribe' method.
Upvotes: 0
Reputation: 17492
I think answering this question first will make life a whole lot easier:
and how can I know what 'this' client is ?
OperationContext.Current.GetCallbackChannel<T>
For each call the service receives there will be a client channel through which the call is made, this will give you the callback channel of the client that made that call only, this is a simple way in which you're able to distinguish your clients.
Regarding the approach to your scenario as a whole, I would first store a list of subscribers
in a static dictionary
as you suggested yourself, but also keep each clients callback instance along with their username:
private static Dictionary<IPriceChangeCallback, string> subscribers = new Dictionary<IPriceChangeCallback, string>();
Where IPriceChangeCallback
is your callback contract and the string could be a unique Username or any identifier. So you now have the basic ability to distinguish your clients, for example say you want to publish the last received message to every client except the one who sent it, you would:
lock (subscribers)
{
foreach (var _subscriber in subscribers)
{
if (OperationContext.Current.GetCallbackChannel<IPriceChangeNotification>() == _subscriber.Key)
{
//if the person who sent the last message is the current subscriber, there is no need to
//publish the message to him, so skip this iteration
continue;
}
else
{
//GetCurrrentClient is a handy method, you can optionally include this
//in your callbacks just to let your clients know who exactly sent the publication
_subscriber.Key.PriceChangeCallback(e.Item, e.Price, e.Change, GetCurrentClient());
}
}
}
or distinguish your clients based on their usernames, which you should ideally have in your databse as well:
lock (subscribers)
{
foreach (var _subscriber in subscribers)
{
if(_subscriber.Value == "Jimmy86"))
{
//Identify a specific client by their username and don't send the notification to him
//here we send the notification to everyone but jimmy86
continue;
}
else
{
_subscriber.Key.PriceChangeCallback(e.Item, e.Price, e.Change, GetCurrentClient());
}
}
}
And again, whenever you want to find out who called the service operation, and tell your clients who sent that particular message, use the GetCurrentClient()
method I mentioned earlier:
private string GetCurrentClient()
{
return clients[OperationContext.Current.GetCallbackChannel<IPriceChangeNotification>()];
}
Is this the right way ?
I'm not sure how advisable the approach above is, but I've done it before whenever I've wanted to keep a list of clients and call some method on them.
should the client send me credentials in the 'subscribe' method that I will store ?
Yes this is one common way of doing it. Have a Subscribe()
operation on your service, which will be the first method your clients will call when they want to join your service:
[OperationContract(IsOneWay = true)]
public void Subscribe(string username)
{
lock (subscribers)
{
subscribers.Add(OperationContext.Current.GetCallbackChannel<IPriceChangeNotification>(), username);
}
}
I was working on a Pub/Sub Silverlight service a couple months ago, and I found this article and it's accompanying video to be invaluable.
Upvotes: 3