-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
15116a8
commit c3e89ed
Showing
10 changed files
with
411 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.SignalR; | ||
using Signum.Entities.Basics; | ||
using Signum.React.Alerts; | ||
using Signum.React.Authorization; | ||
using System.Threading.Tasks; | ||
|
||
namespace Signum.React.Facades; | ||
|
||
public interface IAlertsClient | ||
{ | ||
Task AlertsChanged(); | ||
} | ||
|
||
public class AlertsHub : Hub<IAlertsClient> | ||
{ | ||
public override Task OnConnectedAsync() | ||
{ | ||
var user = GetUser(Context.GetHttpContext()!); | ||
|
||
AlertsServer.Connections.Add(user.ToLite(), Context.ConnectionId); | ||
|
||
return base.OnConnectedAsync(); | ||
} | ||
|
||
public override Task OnDisconnectedAsync(Exception? exception) | ||
{ | ||
AlertsServer.Connections.Remove(Context.ConnectionId); | ||
return base.OnDisconnectedAsync(exception); | ||
} | ||
|
||
IUserEntity GetUser(HttpContext httpContext) | ||
{ | ||
var tokenString = httpContext.Request.Query["access_token"]; | ||
if (tokenString.Count > 1) | ||
throw new InvalidOperationException($"{tokenString.Count} values in 'access_token' query string found"); | ||
|
||
if (tokenString.Count == 0) | ||
{ | ||
tokenString = httpContext.Request.Headers["Authorization"]; | ||
|
||
if (tokenString.Count != 1) | ||
throw new InvalidOperationException($"{tokenString.Count} values in 'Authorization' header found"); | ||
} | ||
|
||
var token = AuthTokenServer.DeserializeToken(tokenString.SingleEx()); | ||
|
||
return token.User; | ||
} | ||
} | ||
|
||
public class ConnectionMapping<T> where T : class | ||
{ | ||
private readonly Dictionary<T, HashSet<string>> userToConnection = new Dictionary<T, HashSet<string>>(); | ||
private readonly Dictionary<string, T> connectionToUser = new Dictionary<string, T>(); | ||
|
||
public int Count => userToConnection.Count; | ||
|
||
public void Add(T key, string connectionId) | ||
{ | ||
lock (this) | ||
{ | ||
HashSet<string>? connections; | ||
if (!userToConnection.TryGetValue(key, out connections)) | ||
{ | ||
connections = new HashSet<string>(); | ||
userToConnection.Add(key, connections); | ||
} | ||
|
||
connections.Add(connectionId); | ||
|
||
connectionToUser.Add(connectionId, key); | ||
} | ||
} | ||
|
||
public IEnumerable<string> GetConnections(T key) => userToConnection.TryGetC(key) ?? Enumerable.Empty<string>(); | ||
|
||
public void Remove(string connectionId) | ||
{ | ||
lock (this) | ||
{ | ||
var user = connectionToUser.TryGetC(connectionId); | ||
if (user != null) | ||
{ | ||
HashSet<string>? connections = userToConnection.TryGetC(user); | ||
if (connections != null) | ||
{ | ||
connections.Remove(connectionId); | ||
if (connections.Count == 0) | ||
userToConnection.Remove(user); | ||
} | ||
|
||
connectionToUser.Remove(connectionId); | ||
} | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,93 @@ | ||
using Microsoft.AspNetCore.Builder; | ||
using Microsoft.AspNetCore.Routing; | ||
using Microsoft.AspNetCore.SignalR; | ||
using Signum.Entities.Alerts; | ||
using Signum.Entities.Authorization; | ||
using Signum.Entities.Basics; | ||
using Signum.React.Facades; | ||
|
||
namespace Signum.React.Alerts; | ||
|
||
public static class AlertsServer | ||
{ | ||
internal static ConnectionMapping<Lite<IUserEntity>> Connections = null!; | ||
|
||
public static IHubContext<AlertsHub, IAlertsClient> AlertsHub { get; private set; } | ||
|
||
public static void Start(IApplicationBuilder app) | ||
{ | ||
SignumControllerFactory.RegisterArea(MethodInfo.GetCurrentMethod()); | ||
} | ||
|
||
public static void MapAlertsHub(IEndpointRouteBuilder endpoints) | ||
{ | ||
endpoints.MapHub<AlertsHub>("/api/alertshub"); | ||
Connections = new ConnectionMapping<Lite<IUserEntity>>(); | ||
AlertsHub = (IHubContext<AlertsHub, IAlertsClient>)endpoints.ServiceProvider.GetService(typeof(IHubContext<AlertsHub, IAlertsClient>))!; | ||
|
||
var alertEvents = Schema.Current.EntityEvents<AlertEntity>(); | ||
|
||
alertEvents.Saved += AlertEvents_Saved; | ||
alertEvents.PreUnsafeDelete += AlertEvents_PreUnsafeDelete; | ||
alertEvents.PreUnsafeUpdate += AlertEvents_PreUnsafeUpdate; | ||
alertEvents.PreUnsafeInsert += AlertEvents_PreUnsafeInsert; | ||
} | ||
|
||
private static IDisposable? AlertEvents_PreUnsafeUpdate(IUpdateable update, IQueryable<AlertEntity> entityQuery) | ||
{ | ||
NotifyOnCommitQuery(entityQuery); | ||
return null; | ||
} | ||
|
||
private static LambdaExpression AlertEvents_PreUnsafeInsert(IQueryable query, LambdaExpression constructor, IQueryable<AlertEntity> entityQuery) | ||
{ | ||
NotifyOnCommitQuery(entityQuery); | ||
return constructor; | ||
} | ||
|
||
private static IDisposable? AlertEvents_PreUnsafeDelete(IQueryable<AlertEntity> entityQuery) | ||
{ | ||
NotifyOnCommitQuery(entityQuery); | ||
return null; | ||
} | ||
|
||
private static void AlertEvents_Saved(AlertEntity ident, SavedEventArgs args) | ||
{ | ||
if (ident.Recipient != null) | ||
NotifyOnCommit(ident.Recipient); | ||
} | ||
|
||
private static void NotifyOnCommitQuery(IQueryable<AlertEntity> alerts) | ||
{ | ||
var recipients = alerts.Where(a => a.Recipient != null && a.State == AlertState.Saved).Select(a => a.Recipient!).Distinct().ToArray(); | ||
if (recipients.Any()) | ||
NotifyOnCommit(recipients); | ||
} | ||
private static void NotifyOnCommit(params Lite<IUserEntity>[] recipients) | ||
{ | ||
var hs = (HashSet<Lite<IUserEntity>>)Transaction.UserData.GetOrCreate("AlertRecipients", new HashSet<Lite<IUserEntity>>()); | ||
hs.AddRange(recipients); | ||
|
||
Transaction.PostRealCommit -= Transaction_PostRealCommit; | ||
Transaction.PostRealCommit += Transaction_PostRealCommit; | ||
} | ||
|
||
private static void Transaction_PostRealCommit(Dictionary<string, object> dic) | ||
{ | ||
var hashSet = (HashSet<Lite<IUserEntity>>)dic["AlertRecipients"]; | ||
foreach (var user in hashSet) | ||
{ | ||
foreach (var connectionId in Connections.GetConnections(user)) | ||
{ | ||
try | ||
{ | ||
AlertsServer.AlertsHub.Clients.Client(connectionId).AlertsChanged(); | ||
} | ||
catch(Exception ex) | ||
{ | ||
ex.LogException(); | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.
c3e89ed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add SignalR to AlertDropdown
This commit (plus a few fixes 1, 2) add SignalR to
AlertDropdown
in order to show real-time notifications to the user created by your business logic.This means that
"@microsoft/signalr"
is now a dependency inSignum.React.Extensions
and is needed if you want to use this control. If you want to check what is necessary to activate it check this commit.If you need notifications in your application, or you have any other use case that could benefit from a real-time refresh, continue reading.
How it works
There is an
AlertHub
, an strongly-typed SignalRHub<IAlertsClient>
, like a controller that keeps a connection open.In this case the notifications have to be sent to all the SignalR connectionIDs that are logged with a particular user, so when connecting the
connectionId
is associated with the user using anAuthToken
when creating the connection. We keep this UserEntity <-> ConnectionId relationship in memory (from here)Then there is a
AlertServer
that listens to the EntityEvents ofAlertEntity
and registers all the users that will be needed to notify.When the transaction is commited, sends the necessary notifications to all the connectionIDs using
IHubContext<AlertsHub, IAlertsClient>
Finally there is a
CacheLogic.BroadcastReceivers
.... but this will be explained tomorrow ;)c3e89ed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect 👍
c3e89ed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.