Service Worker, Push Notification and Asp.Net MVC – Part 3 of 3 Server Side

I previously discussed about how to configure a web push notification from the client side perspective as well as how to send the notification from an ASP.Net code which could be sent from Azure Webjobs. The remaining part is how do you send to multiple devices of one user. If you have a single browser used by a user, the initial solution is good. However, the reality is that users use multiple devices. Users can use not only different browsers on different machines but also jump from computer to phone and so on. The idea is to register subscription by device and not by user.

Google Firebase documentation explains briefly the “Device group messaging” but the page talks more about Topic. I couldn’t figure out how to use the device group messaging but could use Topic for the same matter. The idea is to use a single topic per user and send a message to this topic.

The first big change is to add a subscribe method and unsubscribe that work with the topic api.

public bool UnRegisterTopic(string userIdentifierForAllDevices, string singleDeviceNoticationKey)
{
	var serverApiKey = ConfigurationManager.AppSettings["FirebaseServerKey"];
	var firebaseGoogleUrl = $"https://iid.googleapis.com/iid/v1/{singleDeviceNoticationKey}/rel/topics/{userIdentifierForAllDevices}";

	var httpClient = new WebClient();
	httpClient.Headers.Add("Content-Type", "application/json");
	httpClient.Headers.Add(HttpRequestHeader.Authorization, "key=" + serverApiKey);

	object data = new { };
	var json = JsonConvert.SerializeObject(data);
	Byte[] byteArray = Encoding.UTF8.GetBytes(json);
	var responsebytes = httpClient.UploadData(firebaseGoogleUrl, "DELETE", byteArray);
	string responsebody = Encoding.UTF8.GetString(responsebytes);
	dynamic responseObject = JsonConvert.DeserializeObject(responsebody);

	return responseObject.success == "1";
}
public bool RegisterTopic(string userIdentifierForAllDevices, string singleDeviceNoticationKey)
{
	var serverApiKey = ConfigurationManager.AppSettings["FirebaseServerKey"];
	var firebaseGoogleUrl = $"https://iid.googleapis.com/iid/v1/{singleDeviceNoticationKey}/rel/topics/{userIdentifierForAllDevices}";

	var httpClient = new WebClient();
	httpClient.Headers.Add("Content-Type", "application/json");
	httpClient.Headers.Add(HttpRequestHeader.Authorization, "key=" + serverApiKey);

	object data = new{};
	var json = JsonConvert.SerializeObject(data);
	Byte[] byteArray = Encoding.UTF8.GetBytes(json);
	var responsebytes = httpClient.UploadData(firebaseGoogleUrl, "POST", byteArray);
	string responsebody = Encoding.UTF8.GetString(responsebytes);
	dynamic responseObject = JsonConvert.DeserializeObject(responsebody);

	return responseObject.success == "1";
}

There is quite repetition in that code and you can improve it easily. The biggest change is the URL. Not only the URL domain is different (before https://fcm.googleapis.com/fcm/send and now https://iid.googleapis.com/), it has different route portions. The first part is the device notification key which is the token generated by the client side from the method “getToken”. The second portion of the route is the user identifier which I use as topic. If you really need topic across users, you can just use a string with the category needed. In my case, it is just the unique GUID of the user. This POST HTTP call will register the device for the user by a topic which is the user ID.

To send a message to the user, on all devices, the code needs also to change.

public bool QueueMessage(string to, string title, string message, string urlNotificationClick)
{
	if (string.IsNullOrEmpty(to))
	{
		return false;
	}
	var serverApiKey = ConfigurationManager.AppSettings["FirebaseServerKey"];
	var firebaseGoogleUrl = "https://fcm.googleapis.com/fcm/send";

	var httpClient = new WebClient();
	httpClient.Headers.Add("Content-Type", "application/json");
	httpClient.Headers.Add(HttpRequestHeader.Authorization, "key=" + serverApiKey);
	var timeToLiveInSecond = 24 * 60; // 1 day
	var data = new
	{
		to = "/topics/" + to ,
		data = new
		{
			notification = new
			{
				body = message,
				title = title,
				icon = "/Content/Images/Logos/BourseVirtuelle.png",
				url = urlNotificationClick,
				sound = "default"
			}
		},
		time_to_live = timeToLiveInSecond
	};

	var json = JsonConvert.SerializeObject(data);
	Byte[] byteArray = Encoding.UTF8.GetBytes(json);
	var responsebytes = httpClient.UploadData(firebaseGoogleUrl, "POST", byteArray);
	string responsebody = Encoding.UTF8.GetString(responsebytes);
	dynamic responseObject = JsonConvert.DeserializeObject(responsebody);

	return responseObject.success == "1";
}

What has changed from sending to a single user? The field “to” which is sending to topics. The “to” in the method signature is still the user unique identifier, but instead of sending directly to it, we use it has a topic. We do not use the token generated by the front end since a new one got generated per device, we only use the user id which is the topic.

C# Localize Properties by String Pattern with Resource File

Imagine the scenario where you have several classes that inherit a base class and all theses children classes have an unique title, description and other string properties defined in a resource file. If you need the current language, the standard way to access the resource is fine. However, in the scenario where you need to have all defined localized string, you need to call the resource manager and explicitly use the string for the key as well as for the culture. Going in that direction is a step in redundancy where every child class needs to have a reference to the resource file manager, and repeat the property string that is defined in the base class for all languages. In this article, I’ll show you a way to handle by convention, with a string pattern, a way to get the localized string for all desired language once (in the base class) instead of in all children.

At the end, we want something clean like the following code:

public class ChildA: Parent{
    public ChildA(){}
}

To do so, we need the parent to take care to fill the localized properties accordingly from the child’s type.

public class Parent
{
	public LocalizedString Name { get; set; }
	public LocalizedString Description { get; set; }

	protected FeatureFlag()
	{
		this.Name = ParentResourceManager.Instance.GetLocalizationNameByClassName(this);
		this.Description = ParentResourceManager.Instance.GetLocalizationDescriptionByClassName(this);
	}
}

You can skip the detail about the LocalizedString type or if you are curious can go look at this article. It’s just a class that has a French and English string. The important piece if that the constructor invokes the ParentResourceManager to retrieve the proper string. This resource manager looks like this:

public class ParentResourceManager : ClassResourceManager
{
   private static readonly Lazy<ParentResourceManager> lazy = new Lazy<ParentResourceManager>(() => new ParentResourceManager());

   public static ParentResourceManager Instance { get { return lazy.Value; } }
   private ParentResourceManager(): base(ApplicationTier.ParentResource.ResourceManager)
   {
   }
}

This class uses the generic ClassResourceManager that define which resource file to look for the string and also be sure to have a single unique instance in the application. The generic class ClassResourceManager handles the two methods used to retrieve the title and description. These methods could be simplified into a single one, but for the purpose of this article, let’s keep it this way. The reason behind having this generic class is that you can reuse it for every type that has a different resource file. In short, it sets a pointer to the resource manager generated by the resource file. Indeed, the resource file needs to be set at auto-generated.

public class ClassResourceManager
{
	public static string ERROR_MESSAGE_FORMAT = "[{0}] not found for language [{1}] in file [{2}]";

	public ClassResourceManager(ResourceManager resourceManager)
	{
		this.ResourceManager = resourceManager;
	}

	public ResourceManager ResourceManager { get; set; }

	public string GetLocalizationFor(string key, LanguageType language)
	{
		string languageTwoLetters = "";
		switch (language)
		{
			case LanguageType.English:
				languageTwoLetters = "en";
				break;
			case LanguageType.French:
				languageTwoLetters = "fr";
				break;
		}
		try
		{
			var resourceString = this.ResourceManager.GetString(key, CultureInfo.CreateSpecificCulture(languageTwoLetters));
			if (resourceString == null)
			{
				return string.Format(ERROR_MESSAGE_FORMAT, key, language, this.GetResourceFileName());
			}
			return resourceString;
		}
		catch (Exception)
		{
			return string.Format(ERROR_MESSAGE_FORMAT, key, language, this.GetResourceFileName());
		}

	}

	public LocalizedString GetLocalizationFor(string key)
	{
		return new LocalizedString { French = this.GetLocalizationFor(key, LanguageType.French), English = this.GetLocalizationFor(key, LanguageType.English) };
	}

	public LocalizedString GetLocalizationNameByClassName(object objectReference)
	{
		var objectType = objectReference.GetType();
		var name = objectType.Name + "_Name";
		return new LocalizedString { French = this.GetLocalizationFor(name, LanguageType.French), English = this.GetLocalizationFor(name, LanguageType.English) };
	}

	public LocalizedString GetLocalizationDescriptionByClassName(object objectReference)
	{
		var objectType = objectReference.GetType();
		var name = objectType.Name + "_Description";
		return new LocalizedString { French = this.GetLocalizationFor(name, LanguageType.French), English = this.GetLocalizationFor(name, LanguageType.English) };
	}

	private string GetResourceFileName()
	{
		return this.ResourceManager.BaseName;
	}
}

The ClassResourceManager has a pointer to the resource file and use the name of the child class to concatenate with the property string name. For example, with ClassA, the developer must defined in the resource file ClassA_Title and ClassA_Description. If the developer forget, the code will thrown an exception telling exactly which resource name is missing which is convenient and pretty clear.

The whole idea is to stop having manual entries for localized string at the cost of depending on a pattern for properties that are shared across all children. Since we know which properties the parent needs to be localized, it’s easy to have this one handle how to retrieve the localized string from a specific resource file.

Service Worker, Push Notification and Asp.Net MVC – Part 2 of 3 Server Side

In the part one, we saw how to register a service worker and how to handle incoming messages if the user is actively on the website. However, we didn’t touch how to send a message through Google Firebase to receive the message. In this article, I’ll show how to send a message from an Azure Webjob, written in C#. This is a common scenario where you have a backend job running and executing some logics that needs to have the user to an action. Since the user may or may not be on the website (or the wrong page), the push notification is great to indicate that something must be done. The other big advantage is that the push notification with Google Firebase offers an almost instant messaging service. Within few milliseconds, the message goes from the server to Google Firebase server to the service worker that will use the push notification API of the browser to display the message.

The first thing, is to define a generic contract with an interface. I decided to create a simple one that return a simple boolean to indicate if the message sent is a success or a failure. The method signature allows to pass the “to” token, which is the unique identifier of the user for Firebase (the token saved from the Ajax call in the part 1). The remaining parameters are self explanatory with the title, message and url when the user click the notification.

public interface IPushNotification
{
    bool QueueMessage(string to, string title, string message, string urlNotificationClick);
}

The implementation is also very simple. It relies on the REST endpoint of Google Firebase.

public class GoogleFirebaseNotification:IPushNotification
{
    public bool QueueMessage(string to, string title, string message, string urlNotificationClick)
    {
        if (string.IsNullOrEmpty(to))
        {
            return false;
        }
        var serverApiKey = "SuperLongKeyHere";
        var firebaseGoogleUrl = "https://fcm.googleapis.com/fcm/send";

        var httpClient = new WebClient();
        httpClient.Headers.Add("Content-Type", "application/json");
        httpClient.Headers.Add(HttpRequestHeader.Authorization, "key=" + serverApiKey);
        var timeToLiveInSecond = 24 * 60; // 1 day
        var data = new
        {
            to = to,
            data = new
            {
                notification = new
                {
                    body = message,
                    title = title,
                    icon = "/Content/Images/Logos/BourseVirtuelle.png",
                    url = urlNotificationClick
                }
            },
            time_to_live = timeToLiveInSecond
        };

        var json = JsonConvert.SerializeObject(data);
        Byte[] byteArray = Encoding.UTF8.GetBytes(json);
        var responsebytes = httpClient.UploadData(firebaseGoogleUrl, "POST", byteArray);
        string responsebody = Encoding.UTF8.GetString(responsebytes);
        dynamic responseObject = JsonConvert.DeserializeObject(responsebody);

        return responseObject.success == "1";
    }
}

The first piece of puzzle is to use the right server api key. It’s under the Firebase console, under the setting’s cog and under the Cloud Messaging.

The remaining of the code is configuring the WebClient. You must use a specific content-type to be json. The second header that must be defined is the authorization key. This is where you set the cloud messaging server key. Finally, we setup the data from the signature. Some information are hardcoded like the icon to display as well as the time that we want Firebase to hold the message if the user doesn’t have a browser to collect the message. The last step is to retrieve the response and looks to see if the message got delivered successfully to the Firebase’s server.

When using with a webjob, you just need to use this implementation and pass the desired parameters. You can get from the token from a new column create in the AspNetUsers table and define a specific title and description depending of what the user must do.

Service Worker, Push Notification and Asp.Net MVC – Part 1 of 3 Client Side

Browsers involve very rapidly and since few years it’s possible to write JavaScript that runs in the background of the browser. That means that it’s possible to run code even if the user is not on the website. This is useful for many scenarios and today we will see one feature which is the push notification. The particular environment that we will describe is to use a service worker that wait a message from a Asp.Net Azure web job written in C# that will push a message at a particular time depending of some value to a specific user. The end result will be that the browser will popup a message box at the bottom right if the user is not on the website or if the user is on the website will display a HTML notification directly on the page.

This article is the part one of two which concentrates on the front-end, not the C# code that runs on the server. We will cover the registration of Google Firebase, the service worker’s code and the JavaScript code to add on your website.

The first step if to register an account with Google Firebase. This is not very obvious since almost all example on the web (at this date) uses the raw Service Worker + Push Notification API with the legacy Google system instead of Firebase. Both are pretty compatible in term of server contracts to generate the message, however, on the client side, it’s pretty different. Firebase acts as a wrapper on the native Service Worker API and Push Notification API. You can still use the API directly, and in some case, it’s the only way to access advanced feature.

To create a Firebase account, you need to https://console.firebase.google.com and create an account and a project.

Firebase is a library that is accessible via API keys and JavaScript API. You can also invoke the API through a Rest API which we will see in the second part. The first challenge is to figure out where to get the right API key because the system has many. The first step is to create the Service Worker. This is registered when the user goes into your website to run in the background of the browser. The default is to create a file called “firebase-messaging-sw.js” and to put that file at the root of your website. The location of the file is important because the service worker can only access assert that are sibling or child to the script registered.

Here is the full code that I have for my Service Worker:

importScripts('https://www.gstatic.com/firebasejs/3.5.0/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/3.5.0/firebase-messaging.js');

var config = {
    apiKey: "AIzaSyDe0Z0NtygDUDySNMRtl2MIV5m4Hp7IAm0",
    authDomain: "bourse-virtuelle.firebaseapp.com",
    messagingSenderId: "555061918002",
};
firebase.initializeApp(config);

var messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function (payload) {
    var dataFromServer = JSON.parse(payload.data.notification);
    var notificationTitle = dataFromServer.title;
    var notificationOptions = {
        body: dataFromServer.body,
        icon: dataFromServer.icon,
        data: {
            url:dataFromServer.url
        }
    };
    return self.registration.showNotification(notificationTitle,
        notificationOptions);
});

self.addEventListener("notificationclick", function (event)
{
    var urlToRedirect = event.notification.data.url;
    event.notification.close();
    event.waitUntil(self.clients.openWindow(urlToRedirect));
});

In short, it uses 2 Firebase scripts. One for the Firebase and one for the messaging which is the wrapper around the push notification api. The configuration is tricky. The apiKey is taken from the Firebase’s console, under the desired project, under the project settings gear, in the Generaltab.

The messagingSenderId is an id that is available tab next to the General tab, called Cloud Messaging.

The initialize command will connect the service worker to the server to listen to new messages. The setBackgroundMessageHandler function is called when a new message occurs when the user is not having the website in focus. It means that if the user has the website in a browser’s tab that it not the current one, or if the user is not having the website open at all, or if the browser is minimized that this message will be invoked. The case about if the user is having focus will be treated later.

This code get the data from the server. In my case, it’s under the property data and notification. I set the title, the main message, the icon. The URL is there but didn’t work at this time. That is why the second method, which use directly the push notification api to hook on notificationclick. This method handles when the user click the notification to open a specific URL. For example, in my case, the notification occurs when a specific event occurs and clicking the notification opens a specific page where the user can see the result of the action.

The next step is to have a page where the user can subscribe to the push notification. In my case, it’s done in the user’s profile. I have a checkbox, if checked, the browser will request the authorization to the user to install the service worker. So, in my profile.js page I have the following code:

$(document).ready(function()
{
    initialiseUI();
});
function initialiseUI() {
    $(document).on("click", "#" + window.Application.Variables.IsHavingNotification,
        function requestPushNotification()
        {
            var $ctrl = $(this);
            if ($ctrl.is(":checked"))
            {
                console.log("checked");
                subscribeUser();

            } else
            {
                console.log("unchecked");
                unsubscribeUser();
            }
        });
}

function subscribeUser() {
    var isSubscribed = false;
    var messaging = firebase.messaging();
    messaging.requestPermission()
      .then(function () {
          messaging.getToken()
          .then(function (currentToken) {
              if (currentToken) {
                  updateSubscriptionOnServer(currentToken);
                  isSubscribed = true;
              } else {
                  updateSubscriptionOnServer(null);
              }
              $("#" + window.Application.Variables.IsHavingNotificationt).prop('checked', isSubscribed);
          })
          .catch(function (err) {
              isSubscribed = false;
              updateSubscriptionOnServer(null);
          });
      })
      .catch(function (err) {
          console.log('Unable to get permission to notify.', err);
      });
}

function unsubscribeUser() {
    var messaging = firebase.messaging();
    messaging.getToken()
    .then(function (currentToken) {
        messaging.deleteToken(currentToken)
        .then(function () {
            updateSubscriptionOnServer(null);
        })
        .catch(function (err) {
            console.log('Unable to delete token. ', err);
        });
    })
    .catch(function (err) {
        console.log('Error retrieving Instance ID token. ', err);
    });
}

function updateSubscriptionOnServer(subscription) {
    var subscriptionDetail = { key: "" };
    if (subscription)
    {
        subscriptionDetail = { key: subscription };
    } else {
        console.log("delete on the server the token");
    }
    
    var apiUrl = window.Application.Url.UrlNotifications;
    var dateToSent = subscriptionDetail;
    $.ajax({
        url: apiUrl,
        type: 'POST',
        data: dateToSent,
        cache: true,
        dataType: 'json',
        success: function (json) {
            if (json.IsValid) {
            } else {
            }
        },
        error: function (xmlHttpRequest, textStatus, errorThrown) {
            console.log('some error occured', textStatus, errorThrown);
        },
        always: function () {
        }
    });

}

We allow to subscribe and unsubscribe. When subscribing, we request the permission to send the notification by the browser. Then, we get the token provided my Firebase. This is needed to be able to save the token back to the server to have targeted message from the server later. With this token, we will be able to send specific message to specific user. This is where the updateSubscriptionOnServer come to play. It sends by Ajax the token, and it’s saved in the database. In my case, I added a column in the user’s table to keep track of the token. The unsubscribe sends a null value and set null in the column. This way, the server can look and see if the user has or not a Firebase token and only send a message when a token is defined.

To verify that all the previous steps are well executed, you can look in Chrome developer tool under Application and see for the service worker.

It’s important to understand that what we are doing only work under localhost or with HTTPS’ website. From Chrome’S debug panel, you can unregister, or click “Update on reload” to force a reinstallation of the service worker. This can be handy when developing to be sure to always have the latest version of your service worker.

The next step is to have your website listen to incoming messages. This cover the scenario when the user is on the website and that we do not want to use the browser notification. To do, we need to use some code that we already used in the service worker concerning Firebase’s initialization. In my case, I added in the master page (_layout.cshtml) a reference to Firebase script to initialize the library. It looks like that:

    <script src="https://www.gstatic.com/firebasejs/3.6.2/firebase.js"></script>
    </script>
    <script>
      var config = {
        apiKey: "AIzaSyDe0Z0NtygDUDySNMRtl2MIV5m4Hp7IAm0",
        authDomain: "bourse-virtuelle.firebaseapp.com",
        messagingSenderId: "555061918002",
      };
      firebase.initializeApp(config);
    </script>

I also have a global JavaScript file where I added the listener to message which are used in every page I have.

$(document).ready(function()
{
    var messaging = firebase.messaging();
    messaging.onMessage(function(payload)
    {
        var dataFromServer = JSON.parse(payload.data.notification);
        var myMessageBar = new MessageBar();
        myMessageBar.setMessage(dataFromServer.title + " : " + dataFromServer.body);
    });
});

The listener is onMessage is fired when the user has the focus on the website. So, instead of having the service worker to handle the message, this handler is receiving the data. This give the advantage to be able to add the message directly in the webpage Dom, something that the service worker cannot do. It also has the convenience of having the notification in the field of view of the user instead of having a notification outside.

At this point, you can use any HTTP tools to send a message to Firebase. You can use console.log to output the token and forge a HTTP request with your web api and sender id. I won’t give detail in this post and will give how to do it in a future post about how to handle it with a webjob in C# which will send a HTTP request.

Service worker allows you do to a lot more than just using the push notification. This article covered the basis of how to use Google Firebase has a backbone to have your own backend infrastructure (covered in a future article) to send a message and to have your client receiving the message. Several pieces of code is needed in specific places.