Send Push Notifications from an OData Service using Windows Azure Notification Hubs
There are three main benefits provided to mobile device apps by cloud-based services: data access and storage, authentication, and push notifications—note that Windows Azure Mobile Services is a single Windows Azure service that supports all three. What about cases where Mobile Services isn’t the right solution, most likely because you have an existing set of data or have already published OData feeds. How to provide your customers with the goodness of push notification functionality in your mobile device app when that data comes from a standard OData service?
Windows Azure Notification Hubs is a Windows Azure service that makes it easier to send notifications to mobile apps running on all major device platforms from a single backend service, be it Mobile Services or any OData service. In this article, I focus adding push notifications to a OData service, and in particular to a WCF Data Services project. (If you are using ASP.NET Web API to host your OData feeds, then you should check-out the tutorial Notify users with Notification Hubs).
Why Windows Azure Notification Hubs
Because each major mobile device platform has a service that sends push notifications to apps running in its client OS, it has always been possible for an OData service to generate push notifications. However, this required your service to be able to communicate with each platform-specific push notification service (WNS, APNS, GCM, MPNS). You had to track device identifying markers (e.g. channel URIs for Windows Devices and device tokens in iOS) and use these identifiers, which periodically expire, when sending out notifications. Sending a notification to multiple platforms required your backend call to each of these disparate platforms. Doable…but painful.
Enter a new Notification Hubs service offering, a scalable, cloud-based backend for sending push notifications to devices on all major platforms. Notification Hubs supports tags and templates, which you will see really come in handy. You can read more about Notification Hubs on WindowsAzure.com. On WA.com, you will find lots of good content about how to leverage Notification Hubs directly from client apps, as well as how to integrate with both Mobile Services and ASP.NET Web API. However, what about an OData service implemented by WCF Data Services (which may or may not be deployed to Windows Azure).
This post discusses how to use Notification Hubs to send notifications from an OData service. The associated sample solution Send Push Notifications from an OData Service by Using Notification Hubs demonstrates sending feed change notifications to a Windows Store app. This solution contains two projects: 1) an ASP.NET application that hosts an OData feed implemented by using WCF Data Services and 2) a Windows Store app that consumes the OData feed and registers for feed change notifications. Feed change notifications are performed by the OData service and client registration is enabled by service operations.
The Scenario: Subscribe to Feed Changes
This post covers some of the interesting tasks involved in adding feed change push notifications to the classic Northwind sample OData service, which you get when you complete the WCF Data Services quickstart. The basic scenario is a case where a user, in the case of Northwind—possibly a sales person or sales manager, wants to subscribe to a specific Northwind feed, such as Orders, to see on a mobile device when orders are being updated in the sales process. To simplify notification registration (and because I just don’t like the idea of distributing Notification Hubs credentials in client apps), we want to have the OData service do the work of registering clients with Notification Hubs. To do this, we simply need to have the OData service expose operations to register (and unregister) clients to receive notification. Because we use tags for the given feeds, we can have the service send out tag-based broadcast notifications whenever a feed is changed. These are the basic steps needed to get it all working:
- Create the classic Northwind service by using WCF Data Services.
- Create a new Windows Store app to consume the Northwind service, register it with the Store, and get a set of WNS authentication credentials.
- Create a new Notification Hub in Windows Azure and store the WNS credentials for my new app (you would need to do the same for iOS, Windows Phone, and the other client platforms).
- Create service operations in the Northwind OData service that enables clients to subscribe to individual feeds by using tags, and also lets them unsubscribe.
- To the client app, add code that access these registration operations by supplying tags for the specific feeds they want to be notified about.
- Define change interceptors for CRUD operations on the various feeds (entity sets) in the OData service, which use Notification Hubs to send notifications to all clients registered for that feed.
This scenario is strictly for a WCF Data Services-based OData service. For a similar scenario of using Notification Hubs with an ASP.NET Web API or a Mobile Services backend, see the tutorial Use Notification Hubs to push notifications to users.
Create the Notification Hub
With the Northwind OData service up and running and the client app running (steps #1 and #2 above), the next step was to create a new Notification Hub to send notifications from the OData service to the app. Because an individual notification hub maintains the app-specific credentials obtained from the various platform-specific push services (WNS in the case of Windows Store apps), you basically need one notification hub for each app across all client platforms. I created the new notification hub by following the steps in the Configure your Notification Hub section of the Getting Started with Notification Hubs tutorial—the 101 tutorial for this service (note that there are different versions of this tutorial for each of the major device platforms). To test out the sample, you will need to follow the same steps to create your own hub.
Add Registration Methods in the OData Service
The most elegant and conceptually correct way to enable clients to register for notifications in an OData v3 service would have been to define service actions on each feed, so that, for example, registration against the Orders feed could be accomplished as a POST request to the URI: https://myNorthwindService/northwind.svc/Orders/Notify. The body of the POST request would then contain the JSON representation of a registration class.
Alas, built-in support for service actions in WCF Data Services v5.x is, let’s say, “very rudimentary,” leaving the creating of actions as hard as writing any custom provider—a real PITA. Until I get a chance to really figure out actions, let’s just use plain ol’ service operations to register clients for notifications.
Just for completeness, here’s the Registration type, which is used both in the backend and in the client projects:
public class Registration
{
public string ChannelUri { get; set; }
public string DeviceToken { get; set; }
public string Platform { get; set; }
public string RegistrationId { get; set; }
public ISet<string> Tags { get; set; }
}
Also, the backend ASP.NET project leverages the Windows Azure Service Bus NuGet package to simplify access to Notification Hubs functionality. The NotificationHubClient object is instantiated, as follows, by using the connection string generated by Notification Hubs (the tutorial shows how), which is stored as AppSettings elements in the web.config:
// Create the client in the constructor.
public Northwind()
{
// Create a new Notification Hub client using the stored info.
hubClient = NotificationHubClient
.CreateClientFromConnectionString(
ConfigurationManager.AppSettings["Microsoft.ServiceBus.ConnectionString"],
ConfigurationManager.AppSettings["Microsoft.ServiceBus.NotificationHubName"]
);
}
The NotificationHubClient object provides async access the Notification Hubs REST API, which lets us manage registration and send notifications.
RegisterForPushNotifications Method
The RegisterForPushNotifications method exposes a POST service operation that registers a client using client-supplied registration information, where the method looks like the following:
// Public-facing service operation method for registration.
[WebInvoke(Method = "POST")]
public string RegisterForPushNotifications(string registration)
{
// Get the registration info that we need from the request.
var registrationObject = JsonConvert.DeserializeObject<Registration>(registration);
// Call the async registration method.
var task = RegisterForPushNotificationsAsync(registrationObject);
try
{
// Wait for the async task to complete.
task.Wait();
if (task.IsCompleted)
{
return task.Result;
}
else
{
throw new DataServiceException(500, "Registration timeout.");
}
}
catch (Exception ex)
{
throw ex;
}
}
Note that in OData, you must pass data to service operations (even POST ones) serialized into query parameters—actions let you use the body of a POST, which is more correct IMHO.
The RegisterForPushNotifications also was a little tricky because I wanted to use the new Task-based async programming model, but a service operation can only return null, a primitive type, or an entity/entity collection. The actually interesting code that calls Notification Hubs for registration looked like this:
// Private async method that performs the actual registration,
// since we can't return Task<T> from a service operation.
private async Task<string> RegisterForPushNotificationsAsync(
Registration clientRegistration)
{
// If the client gave us a registration ID, use it to try and get the registration.
RegistrationDescription currentRegistration = null;
if (!string.IsNullOrEmpty(clientRegistration.RegistrationId))
{
// Try to get the current registration by ID.
currentRegistration =
await hubClient.GetRegistrationAsync<RegistrationDescription>(
clientRegistration.RegistrationId);
}
// Update a current registration.
if (currentRegistration != null)
{
// Update the current set of tags.
foreach (string tag in clientRegistration.Tags)
{
if (!currentRegistration.Tags.Contains(tag))
{
currentRegistration.Tags.Add(tag);
}
}
// We need to update each platform separately.
switch (clientRegistration.Platform)
{
case "windows":
case "win8":
var winReg = currentRegistration as WindowsRegistrationDescription;
// Update tags and channel URI.
winReg.ChannelUri = new Uri(clientRegistration.ChannelUri);
clientRegistration.RegistrationId =
(await hubClient.UpdateRegistrationAsync(winReg)).RegistrationId;
break;
case "ios":
var iosReg = currentRegistration as AppleRegistrationDescription;
// Update tags and device token.
iosReg.DeviceToken = clientRegistration.DeviceToken;
clientRegistration.RegistrationId =
(await hubClient.UpdateRegistrationAsync(iosReg)).RegistrationId;
break;
}
}
// Create a new registration.
else
{
// Create an ISet<T> of the supplied tags.
HashSet<string> tags = new HashSet<string>(clientRegistration.Tags);
// We need to create each platform separately.
switch (clientRegistration.Platform)
{
case "windows":
case "win8":
var template = @"<toast>
<visual>
<binding template=""ToastText01"">
<text id=""1"">$(message)</text>
</binding>
</visual>
</toast>";
clientRegistration.RegistrationId =
(await hubClient.CreateWindowsTemplateRegistrationAsync(
clientRegistration.ChannelUri, template, tags)).RegistrationId;
break;
case "ios":
template = "{\"aps\":{\"alert\":\"$(message)\"}, \"inAppMessage\":\"$(message)\"}";
clientRegistration.RegistrationId =
(await hubClient.CreateAppleTemplateRegistrationAsync(
clientRegistration.DeviceToken, template, tags)).RegistrationId;
break;
}
}
return clientRegistration.RegistrationId;
}
If an existing registration exists, this code updates it with the new information (device/channel ID and tags). Otherwise, a new registration is created. This code supports both iOS and Windows Store clients, and the registration methods called depend on the specific platform of the client being registered.
DeleteRegistrations Method
It’s is always “good form” to allow clients to unsubscribe to notifications. While the ideal functionality would be to use actions to unregister from one feed at a time, I found it easier for the sample to just remove the entire Notification Hubs registration for the client. This is done in the DeleteRegistrations service operation.
[WebInvoke(Method = "POST")]
public bool DeleteRegistrations(string registrationId)
{
// Call the async registration method.
var task = DeleteRegistrationsAsync(registrationId);
// Wait for the async task to complete.
task.Wait();
if (task.IsCompleted)
{
return task.Result;
}
else
{
throw new DataServiceException(500, "Registration timeout.");
}
}
As before, this method cannot return a task, so we also need an async private method that calls Notification Hubs to remove the registration.
private async Task<bool> DeleteRegistrationsAsync(string registrationId)
{
bool success = true;
try
{
// Try to delete the registration by ID.
await hubClient.DeleteRegistrationAsync(registrationId);
}
catch (Exception)
{
success = false;
}
return success;
}
When I get this working with OData service actions, each feed can expose an /Unregister action.
Note that the error handling in this method may be overkill—the DeleteRegistrationAsyncmethod doesn’t seem to raise any exception even when a 404 (Not Found) error is returned by the service—although it probably should (bug?).
Add Change Interceptors to Send Notifications
Two of the coolest features of Notification Hubs is 1) the ability to use tags to broadcast push notifications to any registration that is “subscribed” to a given tag and 2) the ability to define platform-specific templates to make it simpler to send cross-platform notifications. WCF Data Services lets us define interceptors that are invoked when there are changes in a feed. These change interceptors, one for each feed, are a great hook from which we can send notifications when there are changes in that feed. The following is the change interceptor defined on the Orders feed:
// Define a change interceptor for the Orders entity set.
[ChangeInterceptor("Orders")]
public void OnChangeOrders(Order order, UpdateOperations operations)
{
SendFeedNotification<Order>("Orders", order,
new int[] { order.OrderID }, operations);
}
This interceptor method calls the following generic SendFeedNotification<T> method, which builds the notification text for the specific entity, feed, and change type:
private async void SendFeedNotification<TEntity>(
string feed, TEntity entity, int[] entityKeys, UpdateOperations operations)
{
var entityType = entity.GetType();
string baseTypeName = entityType.BaseType.Name;
string operationString = string.Empty;
switch (operations)
{
case UpdateOperations.Change:
operationString = "updated";
break;
case UpdateOperations.Add:
operationString = "added";
break;
case UpdateOperations.Delete:
operationString = "deleted";
break;
}
string keysAsString = string.Empty;
foreach (int key in entityKeys)
{
keysAsString += string.Format("{0},", key);
}
keysAsString = keysAsString.TrimEnd(',');
var message = string.Format("The entity '{0}({3})' was {2} in the '{1}' feed.",
baseTypeName, feed, operationString, keysAsString);
await sendNotification(message, feed);
}
Once the message is composed, the SendTemplateNotificationAsync method is called. This method uses the registered template to send a broadcast notification with the provided message string to the specific (feed name) tag:
// Send a cross-plaform notification by using templates.
private async Task sendNotification(string notificationText, string tag)
{
var notification =
new Dictionary<string, string> { { "message", notificationText } };
var outcome =
await hubClient.SendTemplateNotificationAsync(notification, tag);
}
That’s basically it for the WCF Data Services-side of the sample. Hey OData guys—it would be cool to have a way to define a generic interceptor rather than having to decorate methods with the ChangeInterceptorAttribute (maybe there’s a way to do this, but I‘m not a reflection savant). I won’t bore you here with a discussion of the Windows Store sample (maybe later on my blog).
What’s Next
I do plan to work a little more on this sample in the near future, more specifically:
- Investigate using OData service actions for registration, as I think that this is a more correct way to go.
- Add a Windows Phone app client (if I can find the time).
I will probably post any future updates to my own MSDN blog: Writing...Data Services
Well, that’s it for now—back to more Mobile Services work.
Cheers,