GM.RealTime
1.1.0
dotnet add package GM.RealTime --version 1.1.0
NuGet\Install-Package GM.RealTime -Version 1.1.0
<PackageReference Include="GM.RealTime" Version="1.1.0" />
<PackageVersion Include="GM.RealTime" Version="1.1.0" />
<PackageReference Include="GM.RealTime" />
paket add GM.RealTime --version 1.1.0
#r "nuget: GM.RealTime, 1.1.0"
#:package GM.RealTime@1.1.0
#addin nuget:?package=GM.RealTime&version=1.1.0
#tool nuget:?package=GM.RealTime&version=1.1.0
<p align="center"> <img src="https://raw.githubusercontent.com/gmetskhvarishvili/GM.RealTime/master/icon.png" alt="GM.RealTime" width="140" height="140" /> </p>
GM.RealTime
SignalR-based real-time communication for the GM.* ecosystem. Push to users, connections, and
groups through a clean IRealTimeSender instead of wiring IHubContext<T> by hand; track presence
in a shared, lock-guarded connection registry (backed by
GM.Caching +
GM.DistributedLock, so it's Redis-ready across
nodes); and authenticate the WebSocket handshake with a JWT from the access_token query string
(pairs with GM.Identity). Targets .NET 10.
Packages
The three packages version and release together (lockstep):
| Package | What it gives you |
|---|---|
GM.RealTime |
IRealTimeSender, IRealTimeClient, INotificationHub, the NotificationHub, the JWT-from-query handshake, and AddGMRealTime() / MapGMRealTimeHub(). |
GM.RealTime.Domain |
Presence models (Connection, UserPresence, RealTimeMessage) and the IConnectionRegistry abstraction. No infrastructure dependencies. |
GM.RealTime.Persistence |
CacheConnectionRegistry — the registry over ICacheService, with every read-modify-write guarded by IDistributedLock. |
dotnet add package GM.RealTime
Quick start
using GM.RealTime;
// Presence is shared across nodes when the cache + lock are Redis-backed — register those first:
builder.Services.AddGMRedisCaching(o => o.ConnectionString = "localhost:6379");
builder.Services.AddGMRedisDistributedLock(o => o.ConnectionString = "localhost:6379");
builder.Services.AddGMRealTime(o =>
{
o.HubPath = "/hubs/realtime";
// Set this and hub messages fan out across every instance (SignalR Redis backplane).
o.RedisBackplaneConnectionString = "localhost:6379";
});
// JWT is validated by your existing setup (e.g. GM.Identity); GM.RealTime only teaches it to read
// the token from ?access_token=... on the WebSocket handshake.
builder.Services.AddAuthentication().AddJwtBearer(/* your issuer/audience/key */);
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGMRealTimeHub(); // maps NotificationHub at RealTimeOptions.HubPath
app.Run();
Without the Redis registrations, AddGMRealTime() falls back to the in-memory cache and lock
(single-process) — perfect for development.
Push messages
Inject IRealTimeSender anywhere — no IHubContext in your app code:
public class OrderNotifier(IRealTimeSender realtime)
{
public Task OrderShipped(string userId, Guid orderId) =>
realtime.SendToUserAsync(userId, "order.shipped", new { orderId });
}
SendToUserAsync looks the user's live connections up in the shared registry, so it targets every
device they have open — on any node. Also available: SendToConnectionAsync, SendToGroupAsync,
SendToAllAsync.
Presence
public class PresenceEndpoint(IConnectionRegistry registry)
{
public Task<bool> IsOnline(string userId) => registry.IsOnlineAsync(userId);
}
The NotificationHub records connects/disconnects automatically (keyed by the authenticated user),
and mirrors JoinGroup / LeaveGroup into both SignalR and the registry.
Client contract
The hub is strongly typed (Hub<IRealTimeClient>), so clients listen for one method:
connection.on("ReceiveMessage", m => console.log(m.event, m.payload));
How presence stays correct under load
Every connect/disconnect is a read-modify-write on a user's connection set. Under a burst of concurrent connections (multiple tabs, reconnects) those would race and lose ids. The registry takes a per-user distributed lock (GM.DistributedLock) around each mutation, and stores the set in GM.Caching — so it is both race-free and shared across every server instance.
Multiple instances (scale-out)
GM.RealTime is built to run behind a load balancer with many instances (and separate sender processes). Two independent things must be shared, and both are one setting each:
- Presence & targeting — register the Redis-backed cache + lock (
AddGMRedisCaching/AddGMRedisDistributedLock) so the connection registry lives in Redis. Now every node sees the same presence, and the per-user lock serializes connect/disconnect cluster-wide. - Message fan-out — set
RealTimeOptions.RedisBackplaneConnectionString.AddGMRealTimethen wires the SignalR Redis backplane, soSendToUserAsync/SendToGroupAsync/SendToAllAsyncreach clients on any node — including from a background worker that holds no connections itself.
With both, a message queued on one machine and dispatched by a worker on another still lands on the user's browser. Without the backplane, a send only reaches connections on the local process.
Reconnection
AddGMRealTime enables SignalR stateful reconnect on the hub by default
(RealTimeOptions.AllowStatefulReconnects), so a short network blip resumes the same connection
and replays buffered messages instead of dropping it. Clients opt in:
const connection = new signalR.HubConnectionBuilder()
.withUrl("/hubs/realtime", { accessTokenFactory: () => token })
.withAutomaticReconnect() // retry the connection on drop
.withStatefulReconnect() // resume the same connection + replay missed messages
.build();
The connection registry also puts a TTL on every presence entry (ConnectionRegistryOptions.EntryTtl)
as a safety net, so a hard crash can't leak "online" forever even if OnDisconnectedAsync never runs.
Roadmap: a Herald (GM.Notifications) real-time channel
GM.RealTime is designed to drop into GM.Notifications as a new delivery channel next to Email / SMS / Push / Slack / WhatsApp. The sketch:
GM.Notifications.RealTime/
IRealTimeSenderService : (channel contract, like IEmailSenderService)
RealTimeSenderService : wraps GM.RealTime's IRealTimeSender, mapping a
RealTimeNotification -> realtime.SendToUserAsync(userId, "notification", dto)
AddRealTimeNotificationServices(this IServiceCollection) // mirrors AddEmailNotificationServices
- Add a
RealTimeNotification : NotificationBaseentity (channel = RealTime) inGM.Notifications.Domain, with a matching EF configuration inGM.Notifications.Persistence. - A
RealTimeWorker(like the Email/SMS workers) polls pendingRealTimeNotifications and callsIRealTimeSenderService.SendAsync, which delegates toIRealTimeSender.SendToUserAsync— marking the notificationSent, orFailed(with retry) if the user is offline, exactly like the other channels. Presence (IConnectionRegistry.IsOnlineAsync) lets the worker skip or defer delivery to offline users.
That layering keeps GM.RealTime standalone while making it a first-class Herald channel.
Repository layout
GM.RealTime/ # IRealTimeSender, hub, JWT handshake, AddGMRealTime / MapGMRealTimeHub
GM.RealTime.Domain/ # Connection, UserPresence, RealTimeMessage, IConnectionRegistry
GM.RealTime.Persistence/ # CacheConnectionRegistry (GM.Caching + GM.DistributedLock)
tests/GM.RealTime.Tests/ # xUnit tests for the connection registry
Building & testing
dotnet build -c Release
dotnet test -c Release
Releasing
Versioning is automated from Conventional Commits —
see CONTRIBUTING.md. All three packages share one version
(Directory.Build.props) and publish together to nuget.org on each release.
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- GM.RealTime.Domain (>= 1.1.0)
- GM.RealTime.Persistence (>= 1.1.0)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 10.0.0)
- Microsoft.AspNetCore.SignalR.StackExchangeRedis (>= 10.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.