Cloud Notification

AZURE Notification HUB

I recently had to implement push notifications for a LOB Windows Phone application. After looking at few available options I decided to use Windows Azure Notification Hub (MSDN).

This article is about how I used notification hub in my app and my learning.

Goal

I have a Windows Phone application using which users can view their pending approvals and they can also approve those requests. These approvals are very critical to business and should be done as soon as possible. We needed a way to provide toast and tile notification with pending approval count.

High Level Solution

NotificationHub

We will create a service which will be called by client (Windows Phone) to register and un-register with notification hub. For sending notifications we will create a console application that can be used as a Windows task.

STEP 1 – Set up your notification hub on Azure

  • First step is to login to Azure portal and create a notification hub that you are going to use
  • Follow instructions given on MSDN to achieve this

STEP 2 – Registering for notifications

Challenges

Security

Registration with notification hub can be done either from a device directly or via some backend service. Process of registration is almost same in both cases. Notification hub provides two type of shared keys, one is for only listening and other one is for all operations. All operations include registration, un-registration and sending of notifications.

If you want to register from client then client should be aware of default full access key which is obviously a security concern. Anyone can use a de-compiler and read the key that you are using in your code. Once they have the key they can do anything 🙂

Ideal place for having registration logic is in your backend service.

Channel Uri

HttpNotificationChannel creates a notification channel between the Microsoft Push Notification Service and the Push Client and creates a new subscription for raw notifications. Currently active notification channel Uri is required to register for notifications.

To get ChannelUri on your device you need to look for ChannelUriUpdated event. This event is raised when the application first receives the notification channel URI from the Microsoft Push Notification Service, or when the notification channel URI is changed. This means that there is a possibility that ChannelUri for your device can change in future. We should register every time ChannelUri is changed.

User Specific Notification

There is high chance that your notifications are user specific. For example, if you are developing mail application and you want to send notification for new mail messages, user should only get notification about his email account.

This can be done by identifying an identifier and using it as a tag while registering channel Uri. Same tag can be used later to send notifications. Notification hub API provides methods that let’s you send notification to all devices (channel Uri) using a specific tag.

Solution

  1. Use client to fetch ChannelUri each time application is launched
  2. Call backend service and pass this ChannelUri. Backend service will register ChannelUri with notification hub
  3. Use user credential (domain/alias or it’s hash) as tag while registering. Create a simple DB table that keeps track of tags that are registered for notification. This will be used to send notifications later. Store tag in the table.
  4. Once registration is done, store ChannelUri in client’s local storage. From next time onwards, send both new and old ChannelUri to service. Service will make appropriate checks and complete the registration process.

Code 

Client Code

// Call this method from your splash screen
public static async Task RegisterForNotifications()
{
var channel = HttpNotificationChannel.Find("MyPushChannel");
if (channel == null)
{
channel = new HttpNotificationChannel("MyPushChannel");
channel.Open();

// I want to use toast notification
channel.BindToShellToast();

// I want to use tile notification
channel.BindToShellTile();
}

if (channel.ChannelUri != null)
{
await ChannelUriNotifier(channel.ChannelUri.ToString());
}

channel.ChannelUriUpdated += new EventHandler<NotificationChannelUriEventArgs>(async (o, args) =>
{
await ChannelUriNotifier(args.ChannelUri.ToString());
});
}

// Call backend service with old and new channel Uri
private static async Task ChannelUriNotifier(string channelUri)
{
// Simple POCO DTO which is sent to service operation
var notificationDTO = new NotificationRegistationDTO
{
// Read old channel Uri from local storage. MyProjectStorage setting is a helper class to manage local storage
OldUri = MyProjectStorageSettings.NotificationsOldUri,
NewUri = channelUri,
// In our project we had to support multiple devices and there is slight variation in code required to register each type.
DeviceType = DeviceType.WindowsPhone
};

// store current channel Uri in local storage
MyProjectStorageSettings.NotificationsOldUri = channelUri;
var rtnVal = await ServiceCallHandeler.RegisterForNotifications<NotificationRegistationDTO>(notificationDTO);
}

Service Code

// Service method calls this method. _notificationRegistrationDTO contains the data passed from the client
public void Register()
{
var newChannelUri = this._notificationRegistationDTO.NewUri;
var oldChannelUri = this._notificationRegistationDTO.OldUri;

// Do validations
if (string.IsNullOrEmpty(newChannelUri) && string.IsNullOrEmpty(oldChannelUri))
{
throw new ArgumentException("Both newChannelUri and oldChannelUri can not be empty for registration operation.");
}

if (string.IsNullOrEmpty(newChannelUri))
{
throw new ArgumentException("NewChannelUri can not be empty for registration operation.");
}

// Create notification hub client. Full shared acces key is stored in configuration
// which is read via ConfigReder.ServiceBusNotificationHubEndpoint()
NotificationHubClient hubClient = NotificationHubClient.CreateClientFromConnectionString(ConfigReader.ServiceBusNotificationHubEndpoint(), ConfigReader.ServiceBusNotificationHubName());

// username is used as tag so that correct notification is sent to the user
var tags = new[]
{
Thread.CurrentPrincipal.GetName())
};

// Create or update registration based in simple checks
if (!string.IsNullOrEmpty(oldChannelUri))
{
var existingRegistrations = hubClient.GetRegistrationsByChannelAsync(oldChannelUri, 100);
existingRegistrations.Wait();

// If registration exists for oldUri then update that with newUri and tag
if (existingRegistrations.Result != null && existingRegistrations.Result.Any())
{
foreach (var registrationDescription in existingRegistrations.Result)
{
this._device.UpdateRegistration(registrationDescription, newChannelUri, tags, hubClient);
}

return;
}
}

var regWithNewChannelUri = hubClient.GetRegistrationsByChannelAsync(newChannelUri, 100);
regWithNewChannelUri.Wait();

// if registration do not exists with new channel Uri then Create else Update
if (regWithNewChannelUri.Result == null || !regWithNewChannelUri.Result.Any())
{
this._device.CreateRegistration(newChannelUri, tags, hubClient);
}
else
{
foreach (var registrationDescription in regWithNewChannelUri.Result)
{
this._device.UpdateRegistration(registrationDescription, newChannelUri, tags, hubClient);
}
}
}

STEP 2 – Unregister

Challenges

Security

Just like registration, un-registration also requires access to full access key. Due to this we should not do this action from client. We will again do this from our backend service.

Performance

Ideally, when a user un-registers from notification hub and if there are no other registrations for that specific user (tag) then that tag should be deleted from our database. This is a time taking process.

Solution

  1. Send old channel Uri and current channel Uri to service
  2. Service for available registrations for both Uri’s and cancels registration
  3. Do not delete tag from database as it takes little time. We will do this from our notification task.

Code

Server side code

public void Unregister()
{
var newChannelUri = this._notificationRegistationDTO.NewUri;
var oldChannelUri = this._notificationRegistationDTO.OldUri;

if (string.IsNullOrEmpty(newChannelUri) && string.IsNullOrEmpty(oldChannelUri))
{
throw new ArgumentException("Both newChannelUri and oldChannelUri can not be empty for unregistration operation.");
}

NotificationHubClient hubClient = NotificationHubClient.CreateClientFromConnectionString(ConfigReader.ServiceBusNotificationHubEndpoint(), ConfigReader.ServiceBusNotificationHubName());

// Delete registration matching old channel URI
if (!string.IsNullOrEmpty(oldChannelUri))
{
var existingRegistrations = hubClient.GetRegistrationsByChannelAsync(oldChannelUri, 100);
existingRegistrations.Wait();

if (existingRegistrations.Result != null && existingRegistrations.Result.Any())
{
foreach (var registrationDescription in existingRegistrations.Result)
{
hubClient.DeleteRegistrationAsync(registrationDescription);
}
}
}

// Delete registration matching new channel URI
if (!string.IsNullOrEmpty(newChannelUri))
{
var existingRegistrations = hubClient.GetRegistrationsByChannelAsync(newChannelUri, 100);
existingRegistrations.Wait();

// If registration exists for oldUri then update that with newUri and tag
// else create new using newChannelUri
if (existingRegistrations.Result != null && existingRegistrations.Result.Any())
{
foreach (var registrationDescription in existingRegistrations.Result)
{
hubClient.DeleteRegistrationAsync(registrationDescription);
}
}
}

}

STEP 3 – Test registrations

This one is pretty easy :). Login to your azure notification hub portal and you will see debug option on top of the page.

From this screen you can send notification to all channel URIs or use specific tags to send notifications. This is pretty handy if you don’t have notification send task set up yet.

STEP 4 – Send Notifications

I think once you figure our first two steps, this one is a cake walk. I did not find any challenge in making this work, so I’ll simply go to the code.

  • Create a console application which will for each tag saved in your database (during registration) get notification hub registrations
  • If there are no registrations for a specific tag then remove that tag from database
  • If there is any registration for that tag then prepare appropriate tile and toast template
  • Use template and send notification using tag
public async void NotifyUsers()
{
Logger.Log.Info("Started notification task.");

// Get a list of tags registered. I get this from my database table
var notificationData = new NotificationData();
var registeredUsers = notificationData.GetRegisteredUsers();

foreach (var user in registeredUsers)
{
try
{
Logger.Log.Info(string.Format("Sending notifications for {0}.", user ));
var registrations = await HubClient.GetRegistrationsByTagAsync(user, 100);

// if there are no registrations for this tag (user) then delete from database
if (registrations == null || !registrations.Any())
{
Logger.Log.Info(string.Format("No existing registrations found for {0}. Deleting record from database.", user));
notificationData.DeleteNotificationRecipient(user);
continue;
}

// This is the number that I show in notification.
var notificationForUser = notificationData.GetPendingActionsCount(user);
if (notificationForUser == 0)
{
Logger.Log.Info(string.Format("No pending action found for {0}. Notification is not required.", user));
continue;
}

//Windows Phone 8
await SendWindowsPhoneNotifications(notificationForUser, user);

Logger.Log.Info(string.Format("Updating last notification sent time for {0}.", user));
notificationData.UpdateLastNotificationSentTime(user);
}
catch (Exception ex)
{
var message = string.Format("Sending notifications to user {0} failed. Error: {1}", user, ex);
Logger.Log.Error(message);
continue;
}
}
}

private async Task SendWindowsPhoneNotifications(int notificationForUser, string user)
{
Logger.Log.Info(string.Format("Sending Windows Phone 8 notifications for {0}.", user));

await HubClient.SendMpnsNativeNotificationAsync(
string.Format(NotificationTemplates.WP8Tile, notificationForUser),
user);

await HubClient.SendMpnsNativeNotificationAsync(
string.Format(NotificationTemplates.WP8Toast, notificationForUser),
user);
}

public const string WP8Toast = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<wp:Notification xmlns:wp=\"WPNotification\">" +
"<wp:Toast>" +
"<wp:Text1>MyProject</wp:Text1>" +
"<wp:Text2>You have {0} pending actions</wp:Text2>" +
"</wp:Toast>" +
"</wp:Notification>";

public const string WP8Tile = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<wp:Notification xmlns:wp=\"WPNotification\">" +
"<wp:Tile>" +
"<wp:Count>" + "{0}" + "</wp:Count>" +
"<wp:Title>" + "MyProject" + "</wp:Title>" +
"<wp:BackTitle>" + "MyProject" + "</wp:BackTitle>" +
"<wp:BackContent>" + "You have {0} pending actions." + "</wp:BackContent>" +
"</wp:Tile> " +
"</wp:Notification>";

Make sure you provide easy way for users to register and unregister from within your app. I’ll try to upload a working sample soon.