Skip to content

A simple notification with SignalR in Serene StartSharp (.NET Core version)

Victor Tomaili edited this page May 3, 2021 · 1 revision

This article is to demonstrate how to use SignalR in Serene/StartSharp (.NET core version)

Server Side

1. Add SignalR Hub NotificationHub.cs

What is a SignalR hub

The SignalR Hubs API enables you to call methods on connected clients from the server. In the server code, you define methods that are called by client. In the client code, you define methods that are called from the server. SignalR takes care of everything behind the scenes that makes real-time client-to-server and server-to-client communications possible.

Source

using Microsoft.AspNetCore.SignalR;

namespace Serene5.SignalR
{
    public class NotificationHub : Hub
    {

    }
}

2. Add NameBasedUserIdProvider.cs

By default, SignalR uses the ClaimTypes.NameIdentifier from the ClaimsPrincipal associated with the connection as the user identifier.

Source

In Serene, A login user User.Identity has only one Claim, and the type is ClaimTypes.Name. Therefore, we need to add NameBasedUserIdProvider.cs to tell SignalR to use ClaimTypes.Name instead.

ClaimTypes.Name

Source

using Microsoft.AspNetCore.SignalR;
using System.Security.Claims;

namespace Serene5.SignalR
{
    public class NameBasedUserIdProvider : IUserIdProvider
    {
        public string GetUserId(HubConnectionContext connection)
        {
            return connection.User?.FindFirst(ClaimTypes.Name)?.Value;
        }
    }
}

3. Modify Startup.cs

services.AddLogging(loggingBuilder =>
{
    loggingBuilder.AddConfiguration(Configuration.GetSection("Logging"));
    loggingBuilder.AddConsole();
    loggingBuilder.AddDebug();
});

// add below 2 lines
services.AddSignalR(); 
services.AddSingleton<IUserIdProvider, NameBasedUserIdProvider>(); 

services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
app.UseEndpoints(endpoints =>
{
    endpoints.MapHub<NotificationHub>("/notificationhub"); //<--- add this line
    endpoints.MapControllers();
});

Client Side

1. Add SignalR JavaScript Client Library

Use any methods in this page to install the client library. (https://docs.microsoft.com/en-us/aspnet/core/signalr/javascript-client?view=aspnetcore-3.1)

I used Visual Studio 2019 built-in Library Manager (LibMan) and put the files in wwwroot/Scripts/microsoft/signalr.

LibMan

2. Modify ScriptBundle.json

  "Site": [
    "~/Scripts/microsoft/signalr/dist/browser/signalr.js",   //<--- add this line
    "~/Scripts/jquery.autoNumeric.js",
    "~/Scripts/jquery.colorbox.js",
    ...
  ],

3. Modify tsconfig.json

"include": [
    "./wwwroot/Scripts/microsoft/signalr/dist/esm/index.d.ts",  //<--- add this line
    "./typings/serenity/Serenity.CoreLib.d.ts",
    "./typings/jspdf/jspdf.autotable.d.ts",
    "./Imports/**/*",
    "./Modules/**/*"
]

4. Modify ScriptInitialization.ts

/// <reference path="../Common/Helpers/LanguageList.ts" />

namespace Serene6.ScriptInitialization {
    Q.Config.responsiveDialogs = true;
    Q.Config.rootNamespaces.push('Serene6');
    Serenity.EntityDialog.defaultLanguageList = LanguageList.getValue;

    if ($.fn['colorbox']) {
        $.fn['colorbox'].settings.maxWidth = "95%";
        $.fn['colorbox'].settings.maxHeight = "95%";
    }

    window.onerror = Q.ErrorHandling.runtimeErrorHandler;

    //add signalR BEGIN
    
    // The HubConnectionBuilder class creates a new builder for 
    // configuring the server connection. 
    // The withUrl function configures the hub URL.
    // Hub URL was defined in Startup.cs 
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("/notificationhub")
        .withAutomaticReconnect()
        .build();

    // Listening to a message with the name 'notifyMessage'
    // and display the message using Q.notifyInfo
    connection.on("notifyMessage", (message) => {
        Q.notifyInfo(message);
    });

    try {
        connection.start();
    } catch (e) {
        console.error(e.toString());
    }
    //add signalR END
}

Check if SignalR is working

Run your project, and open the DevTool console. You should see some debug message as below.

SignalR connected

How to send notifications

For example, we want to send notification to admin, when a region has been created.

1. Inject an instance of IHubContext<NotificationHub> in RegionEndpoint.cs

namespace Serene6.Northwind.Endpoints
{
    using Microsoft.AspNetCore.SignalR;

    [Route("Services/Northwind/Region/[action]")]
    [ConnectionKey(typeof(MyRow)), ServiceAuthorize(typeof(MyRow))]
    public class RegionController : ServiceEndpoint
    {
        private IHubContext<NotificationHub> _hubContext;

        public RegionController(IHubContext<NotificationHub> hubContext)
        {
            _hubContext = hubContext;
        }
        ...
    }
}

2. Modify Create method in RegionEndpoint.cs

[HttpPost, AuthorizeCreate(typeof(MyRow))]
public SaveResponse Create(IUnitOfWork uow, SaveRequest<MyRow> request)
{
    var response = new MyRepository().Create(uow, request);

    // send a message with the name 'notifyMessage' to admin with Notification Hub.
    var message = $"Region [{request.Entity.RegionDescription}] has been created.";
    _hubContext.Clients.User("admin").SendAsync("notifyMessage", message);
    
    return response;
}

After a region has been created, a notification will be displayed on admin's browser as below.

notification showed

The example above is not a good one, but it shows you how to use SignalR in Serene/StartSharp.

Typically, you can use SignalR with job scheduler system, such as Hangfire or Quartz.Net.

One more thing - Authorization

By default, all methods in a hub can be called by an unauthenticated user. To require authentication, apply the Authorize attribute to the hub

Like this.

using Microsoft.AspNetCore.SignalR;

namespace Serene5.SignalR
{
    [Authorize]
    public class NotificationHub : Hub
    {

    }
}

However, it does not work in Serene/StartSharp. The reason why it doesn't work is that the User.Identity.IsAuthenticated is always false, even after login. And the reason why User.Identity.IsAuthenticated is always false is because the AuthenticationType is null or empty.

Stackoverflow - User.Identity.IsAuthenticated always false in .net core custom authentication

To fix the issue, you need to modify the SetAuthenticationTicket method in WebSecurityHelper, which is not possible. :P

So I choose the modify Login method in AccoutPage.cs instead.

[HttpPost, JsonFilter]
public Result<ServiceResponse> Login(LoginRequest request)
{
    return this.ExecuteMethod(() =>
    {
        request.CheckNotNull();

        if (string.IsNullOrEmpty(request.Username))
            throw new ArgumentNullException("username");

        var username = request.Username;

        // Original
        //if (WebSecurityHelper.Authenticate(ref username, request.Password, false))
        //    return new ServiceResponse();

        // Modified
        if (Dependency.Resolve<Serenity.Abstractions.IAuthenticationService>().Validate(ref username, request.Password))
        {
            var principal = new GenericPrincipal(new GenericIdentity(username, "Password"), new string[0]);
            var httpContext = Dependency.Resolve<IHttpContextAccessor>().HttpContext;
            httpContext.SignInAsync("Cookies", principal).Wait();
            return new ServiceResponse();
        }

        throw new ValidationError("AuthenticationError", Texts.Validation.AuthenticationError);
    });
}

If you compare code above with the source WebSecurityHelper.SetAuthenticationTicket. There is no much difference. The only difference is that I specified the authentication type in GenericIdentity ctor. As long as the authentication type is not null or empty string, User.Identity.IsAuthenticated will return correct value.

true

GenericIdentity ctor

Clone this wiki locally