Phone + Cloud Series: Using a WCF Web Role To Register Phone Apps for Push Notifications

In the last post, we learned how to write an Azure worker role to poll stocks from the Yahoo! Finance service for a particular user and send them a push notification. It works, trust me; it’s just that we have no way of knowing because there is no data in your database. Only your phone / emulator knows the push notification URI that is unique to that combination of phone and app, so it’s the phone’s job to tell our system what exactly that is.

To make that possible, we’ve created a WCF Web Role which will expose an endpoint to the greater Internets™. This way, your phone will always have a way of registering with our service, as long as it has a data connection. We’ll be running and testing this on our local machines, but it’s just as easy to move it to the real world because all you need to do is change a configuration setting in the phone app.

This article focuses on the PushRegService WCF Web Role we created when we set up the Azure solution, so go ahead and open that project now.

Creating the Registration Service

  1. The first thing to do is rename the generated code. I guess somewhere along the line I decided I wanted the service to be called UserService since it also handles the creation of user accounts, not just registration for push notifications. So, rename the interface (IService1) to IUserService and rename Service1 to UserService. This should be reflected both in the filenames and the code just for the sake of completeness.

  2. Replace the code in IUserService.cs to the interface below. This lays out the data and operation contracts for the service. The data contract is explicitly identical to the table format of the Users table in our SQL Azure database. We have three methods here: RegisterForPush, UnregisterForPush and CreateNewUser. The only method we’ll really be testing, or caring about for the purposes of this exercise, is RegisterForPush.

        1: using System.Runtime.Serialization;
        2: using System.ServiceModel;
        3:  
        4: namespace PushRegService
        5: {    
        6:     [ServiceContract]
        7:     public interface IUserService
        8:     {
        9:         [OperationContract]
       10:         bool RegisterForPush(string username, string keywords, string passwordHash, string pushNotificationURI);
       11:  
       12:         [OperationContract]
       13:         bool UnregisterForPush(string username, string passwordHash);
       14:  
       15:         [OperationContract]
       16:         bool CreateNewUser(string username, string passwordHash, string alertKeywords);
       17:     }
       18:  
       19:  
       20:     // Use a data contract as illustrated in the sample below to add composite types to service operations.
       21:     [DataContract]
       22:     public class UserData
       23:     {
       24:         [DataMember]
       25:         public int UserID { get; set; }
       26:  
       27:         [DataMember]
       28:         public string Username { get; set; }
       29:  
       30:         [DataMember]
       31:         public string PushNotificationURI { get; set; }
       32:  
       33:         [DataMember]
       34:         public string AlertKeywords { get; set; }
       35:  
       36:         [DataMember]
       37:         public bool UsePushNotifications { get; set; }
       38:  
       39:         [DataMember]
       40:         public string PasswordHash { get; set; }
       41:     }
       42: }
    
  3. Make sure you add a project reference to the Utilities project as well as a reference to System.Data.Entity. In PushRegService.svc.cs add a using directive for Utilities.ORM. Also, delete what is already there in the class and re-implement the interface using IntelliSense. This will give you all the method stubs.

    It’s possible that you’ll get an error later when you debug this service, along the lines of “this connection is not meant to be used by the EntityProvider” or something like that. That might mean you need to add the connection string from the Utilities project’s App.Config into the web.config for the WCF service. Alternatively you can abandon your hopes of reuse and just create another edmx locally if you just want to see this work.

Implementing the Registration Functionality

I’ve chosen to do this in as bare-bones a manner as possible, mainly so that we can just see this whole mess working together as quickly as possible. In the real world, you’d have an elaborate user account system, where you have an interface to create a new account, then verify the email, then log-in, etc. We don’t have the time or the space on this blog to do all that (Winking smile) so, instead, we’re just going to do it all-at-once. What that means is that the user will be presented (in the WP7 app) with a single screen that has fields for user, password, stocks to watch and whether or not to receive notifications. When the user hits Register, it will hit the service. If the account doesn’t exist, we’ll create one and set those preferences. If the account does exist, we log in with the provided password and update the existing fields.

The first thing we’ll do is implement the RegisterForPush method, since that’s all we are really going to test at this time. In code you can’t see, the UserService class is marked with a ServiceBehavior attribute that allows exceptions to pass through (IncludeExceptionDetailsInFaults = true). This makes debugging WCF services a lot easier especially when you are debugging them by way of a WP7 application.

    1: public bool RegisterForPush(string username, string keywords, string passwordHash, string pushNotificationUri)
    2: {
    3:     try
    4:     {
    5:         AzureAlertsEntities entities = new AzureAlertsEntities();
    6:         
    7:         // determine if a new account needs to be created
    8:         if (entities.Users.Count(u => u.Username == username) == 0)
    9:         {
   10:             // create a new user
   11:             entities.AddToUsers(new User()
   12:             {
   13:                 AlertKeywords = keywords,
   14:                 PasswordHash = passwordHash,
   15:                 Username = username,
   16:                 UsePushNotifications = true,
   17:                 PushNotificationURI = pushNotificationUri
   18:             });                    
   19:             entities.SaveChanges();
   20:             return true;
   21:         }
   22:         else
   23:         {
   24:             // find the existing user and update
   25:             if (entities.Users.Count(u => u.Username == username && u.PasswordHash == passwordHash) == 0)
   26:             {
   27:                 // if no matches, then the login is wrong
   28:                 throw new Exception("The username or password is incorrect.");
   29:             }
   30:  
   31:             // otherwise, just update
   32:             User user = entities.Users.Where(u => u.Username == username && u.PasswordHash == passwordHash).Single();
   33:             user.PushNotificationURI = pushNotificationUri;
   34:             user.UsePushNotifications = true;
   35:             entities.SaveChanges();
   36:             return true;
   37:         }
   38:     }
   39:  
   40:     catch(Exception ex)
   41:     {
   42:         throw ex;
   43:     }        
   44: }
   45:  

Line 5 declares our Entity Framework instance that allows easy access to the database.

First we check to see if the user exists; if not, we create a new user. Then, we check the password, which is hashed with SHA1. If they don’t match, we throw an exception. If they do, we set that user’s preferences and save the changes. Congratulations! We’ve successfully registered a user for push notifications.

Here is the full code for the User Service:

 using System;
 using System.Linq;
 using System.ServiceModel;
 using Utilities.ORM;
  
 namespace PushRegService
 {
     [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
     public class UserService : IUserService
     {    
         public bool CreateNewUser(string username, string passwordHash, string alertKeywords)
         {
             try
             {                
                 AzureAlertsEntities entities = new AzureAlertsEntities();
                 User newUser = new User()
                 {
                     Username = username,
                     PasswordHash = passwordHash,
                     AlertKeywords = alertKeywords,
                     UsePushNotifications = false,
                     PushNotificationURI = ""
                 };
  
                 entities.AddToUsers(newUser);
                 entities.SaveChanges();
                 return true;
             }
             catch
             {
                 return false;
             }
         }        
  
         public bool UnregisterForPush(string username, string passwordHash)
         {
             try
             {
                 AzureAlertsEntities entities = new AzureAlertsEntities();
  
                 // determine if a new user needs to be created
                 if (entities.Users.Count(u => u.Username == username) == 0)
                 {
                     // create a new user
                     entities.AddToUsers(new User()
                     {
                         AlertKeywords = "",
                         PasswordHash = passwordHash,
                         Username = username,
                         UsePushNotifications = false,
                         PushNotificationURI = ""
                     });
                     entities.SaveChanges();
                     return true;
                 }
                 else
                 {
                     // find the existing user and update
                     if (entities.Users.Count(u => u.Username == username && u.PasswordHash == passwordHash) == 0)
                     {
                         // if no matches, then the login is wrong
                         throw new Exception("The username or password is incorrect.");
                     }
  
                     // otherwise, just update
                     User user = entities.Users.Where(u => u.Username == username && u.PasswordHash == passwordHash).Single();
                     user.PushNotificationURI = "";
                     user.UsePushNotifications = false;
                     entities.SaveChanges();
                     return true;
                 }
             }
             catch(Exception ex)
             {
                 throw ex;
             }        
         }
         
         public bool RegisterForPush(string username, string keywords, string passwordHash, string pushNotificationUri)
         {
             try
             {
                 AzureAlertsEntities entities = new AzureAlertsEntities();
                 
                 // determine if a new account needs to be created
                 if (entities.Users.Count(u => u.Username == username) == 0)
                 {
                     // create a new user
                     entities.AddToUsers(new User()
                     {
                         AlertKeywords = keywords,
                         PasswordHash = passwordHash,
                         Username = username,
                         UsePushNotifications = true,
                         PushNotificationURI = pushNotificationUri
                     });                    
                     entities.SaveChanges();
                     return true;
                 }
                 else
                 {
                     // find the existing user and update
                     if (entities.Users.Count(u => u.Username == username && u.PasswordHash == passwordHash) == 0)
                     {
                         // if no matches, then the login is wrong
                         throw new Exception("The username or password is incorrect.");
                     }
  
                     // otherwise, just update
                     User user = entities.Users.Where(u => u.Username == username && u.PasswordHash == passwordHash).Single();
                     user.PushNotificationURI = pushNotificationUri;
                     user.UsePushNotifications = true;
                     entities.SaveChanges();
                     return true;
                 }
             }
  
             catch(Exception ex)
             {
                 throw ex;
             }        
         }
  
     }
 }

UserService.svc.cs

What does “registering for notifications” really mean?

You’ll notice one of the arguments to this operation is the push notification URI. This URI is generated when the phone attempts to subscribe to a notification channel (more on that later). That just means that we get this value from code magic, and store it in the database for use later, kind of like you would with an OAuth access token. We don’t care what exactly it is, just that we have it; it’s the “key.” Once we have our push notification URI stored in a database and associated to a user’s record, we can send them push notifications whenever we like.

Application Certification and user options

One final note for this post is that you must inform the user that push notifications are being used in the app and give them an option to decline receiving them. If you want your app to pass certification, that is.

What’s Next

Up next, we’ll talk about building the Windows Phone mini-app itself, followed by a wrap-up post. We’re almost there!