-
Notifications
You must be signed in to change notification settings - Fork 805
A simple notification with SignalR in Serene StartSharp (.NET Core version)
This article is to demonstrate how to use SignalR in Serene/StartSharp (.NET core version)
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.
using Microsoft.AspNetCore.SignalR;
namespace Serene5.SignalR
{
public class NotificationHub : Hub
{
}
}
By default, SignalR uses the
ClaimTypes.NameIdentifier
from theClaimsPrincipal
associated with the connection as the user identifier.
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.
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;
}
}
}
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();
});
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
.
"Site": [
"~/Scripts/microsoft/signalr/dist/browser/signalr.js", //<--- add this line
"~/Scripts/jquery.autoNumeric.js",
"~/Scripts/jquery.colorbox.js",
...
],
"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/**/*"
]
/// <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
}
Run your project, and open the DevTool console. You should see some debug message as below.
For example, we want to send notification to admin, when a region has been created.
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;
}
...
}
}
[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.
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.
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.
Copyright © Serenity Platform 2017-present. All rights reserved.
Documentation | Serene Template | Live Demo | Premium Support | Issues | Discussions