By Dino Esposito | August 2018
Social networks and mobile OSes made pop-up, balloon-style notifications incredibly popular and mainstream, but Microsoft Windows 2000 was probably the first software to extensively use them. Balloon notifications make it possible to communicate notable events to the user without requiring immediate action and attention—unlike conventional pop-up windows. Key to these balloon-style notifications is the underlying infrastructure that delivers the message in real time, right to the open window where the user is working.
In this article, you’ll see how to use ASP.NET Core SignalR to produce pop-up notifications. The article presents a sample application that keeps track of logged users and gives each a chance to build and maintain a network of friends. Like in a social-network scenario, any logged user may be added or removed from a friend list at any time. When this happens in the sample application, the logged user receives a context-sensitive notification.
Balloon-style notifications are not plain broadcast notifications sent to whoever is listening to SignalR messages on a Web socket channel. Rather, they’re sent to specific users, logged into the application. Opening and listening to a plain Web socket channel is a good way to approach the problem, but ASP.NET Core SignalR just provides a more abstract programming interface and offers support for alternate network protocols beyond WebSockets.
The first step in the building of the application is adding a layer for user authentication. The user is presented a canonical login form and provides her credentials. Once properly recognized as a valid user of the system, she receives an authentication cookie packed with a number of claims, as shown here:
The sample code shows how to create an ad hoc IPrincipal object in ASP.NET Core around a user name once credentials have been successfully verified. It’s key to note that for authentication to work properly in ASP.NET Core, when used in combination with the newest SignalR library, you should enable authentication in the Configure method of the startup class quite early (and in any case, earlier than completing the SignalR route initialization). I’ll return on this point in a moment; meanwhile, let’s review the infrastructure of friendship in the sample application.Defining Friends in the Application
For the purpose of the sample application, a friend relationship is merely a link between two users of the system. Note that the demo application doesn’t use any database to persist users and relationships. A few users and friend relationships are hardcoded and reset when the application restarts or the current view is reloaded. Here’s the class that represents a friend relationship:
As the user logs in, he’s served an index page that provides the list of friends. Through the UI of the page, the user can both add new friends and remove existing ones (see Figure 1).
Figure 1 The Home Page of a Logged User
When the user types the name of a new friend, an HTML form posts and a new friend relationship is created in memory. If the typed name doesn’t match an existing user, a new User object is created and added to the in-memory list, like so:
Imagine the user holds multiple connections to the same server page. As an example, the user has multiple browser windows opened on the same page and interacts with one of those pages. In a situation in which the call brings back a direct response (whether JSON or HTML), only the page from where the request originated ends up being updated. Any other open browser window remains static and unaffected. To work around the problem, you can leverage a feature of ASP.NET Core SignalR that lets you broadcast changes to all connections related to the same user account.
The ASP.NET MVC controller class that receives calls to add or remove friends incorporates a reference to the SignalR hub context. This code shows how that’s done:
As usual in SignalR programming, the friend hub is defined in the startup class, as shown in Figure 2.
It’s key that the UseSignalR call follows the UseAuthentication call. This ensures when a SignalR connection is established on the given route that information about the logged user and claims is available. Figure 3 offers a deeper look at the code that handles the form post when the user adds a new friend to the list.
The Clients property of the hub context has a property called User, which takes a user ID. When invoked on the User object, the SendAsync method notifies the given message to all currently connected browser windows under the same user name. In other words, SignalR has the ability to group automatically all connections from the same authenticated user to a single pool. In light of this, SendAsync invoked from User has the power to broadcast the message to all windows related to the user, whether they come from multiple desktop browsers, in-app Web views, mobile browsers, desktop clients or whatever else. Here’s the code:
Sending the refreshUI message to all connections under the same user name ensures that all opened windows are in a way synchronized to the add-friend function. In the sample source code, you’ll see that the same thing happens when the currently logged-in user removes a friend from his list.
In SignalR, the User object has nothing to do with the User object associated with the HTTP context. In spite of the same property name, the SignalR User object is a client proxy and a container of claims. Yet, a subtle relationship exists between the SignalR user-specific client proxy and the object representing the authenticated user. As you may have noticed, the User client proxy requires a string parameter. In the code in Figure 3, the string parameter is the name of the currently logged-in user. That’s just a string identifier, though, and can be anything you configure it to be.
By default, the user ID recognized by the user client proxy is the value of the NameIdentifier claim. If the list of claims of the authenticated user doesn’t include NameIdentifier, there’s no chance the broadcast would work. So you have two options: One is to add the NameIdentifier claim when creating the authentication cookie, and the other is to write your own SignalR user ID provider. To add the NameIdentifier claim, you need the following code in the login process:
The value assigned to the NameIdentifier claim doesn’t matter as long as it’s unique to each user. Internally, SignalR uses an IUserIdProvider component to match a user ID to the connection groups rooted to the currently logged user, like so:
The IUserIdProvider interface has a default implementation in the DI infrastructure. The class is DefaultUserIdProvider and is coded as follows:
As you can see, the DefaultUserIdProvider class uses the value of the NameIdentifier claim to group user-specific connection IDs. The Name claim is meant to indicate the name of the user, but not necessarily to provide the unique identifier through which a user is identified within the system. The NameIdentifier claim, instead, is designed to hold a unique value, whether a GUID, a string or an integer. If you switch to User instead of NameIdentifer, make sure any value assigned to Name is unique per user.
All connections coming from an account with a matching name-identifier will be grouped together and will automatically be notified when the User client proxy is used. To switch to using the canonical Name claim, you need a custom IUserIdProvider, as follows:
Needless to say, this component must be registered with the DI infrastructure during the startup phase. Here’s the code to include in the ConfigureServices method of the startup class:
At this point, everything is set up to have all matching user windows synchronized on the same state and view. How about balloon-style notifications?
Adding and removing friends cause the need of refresh notifications being sent to the index page the current user is viewing. If a given user has two browser windows opened on different pages of the same application (index and another page), she will receive refresh notifications only for the index page. However, adding and removing friends also causes add and remove notifications to be sent to the users that have been added or removed from the friend relationship list. For example, if user Dino decides to remove user Mary from his list of friends, user Mary will also receive a Remove notification. Ideally, a Remove (or Add) notification should reach the user regardless of the page being viewed, whether index or any other.
To achieve this, there are two options:
- Use a single SignalR hub with the connection setup moved to the layout page and then inherited by all pages based on that layout.
- Use two distinct hubs—one for refreshing the UI after adding or removing friends, and one to notify added or removed users.
If you decide to go with distinct hubs, the add/remove notification hub must be set up in all the pages where you want notifications to appear—most likely the layout pages you have in the application.
Let’s look at the final code of the Add method in the controller. This method is where the form in Figure 1 ultimately posts. The method makes any necessary changes in the back-end to save friend relationships and then issues two SignalR messages—one to visually refresh the list of friends of the current user that did the operation, and a second to notify the added (or removed) user. The user is actually notified if she’s currently connected to the application and from a page configured to receive and handle those specific notifications. Figure 4 shows this.
The results are shown in Figure 5.
A Word on SignalR Groups
Figure 5 Cross-User Notifications
In this article, I covered the SignalR support for selected notifications sent to a related group of users. SignalR offers a couple of approaches—the User client proxy and groups. The difference is subtle: The User client proxy implicitly generates a number of groups where the name is determined by the user ID and members are all connections opened by the same application user. A group is a more general mechanism that programmatically appends connections to a logical group. Both connections and name of the group are set programmatically.
Balloon-style notifications could have been implemented in both these approaches, but for this particular scenario the User client proxy was the most natural solution. Source code can be found at bit.ly/2HVyLp5.
Dino Esposito has authored more than 20 books and 1,000 articles in his 25-year career. Author of “The Sabbatical Break,” a theatrical-style show, Esposito is busy writing software for a greener world as the digital strategist at BaxEnergy. Follow him on Twitter: @despos.Nguồn: msdn.microsoft.com