GM.RealTime.Domain 1.1.0

dotnet add package GM.RealTime.Domain --version 1.1.0
                    
NuGet\Install-Package GM.RealTime.Domain -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="GM.RealTime.Domain" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GM.RealTime.Domain" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="GM.RealTime.Domain" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add GM.RealTime.Domain --version 1.1.0
                    
#r "nuget: GM.RealTime.Domain, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package GM.RealTime.Domain@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=GM.RealTime.Domain&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=GM.RealTime.Domain&version=1.1.0
                    
Install as a Cake Tool

<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

CI NuGet License: MIT

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:

  1. 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.
  2. Message fan-out — set RealTimeOptions.RedisBackplaneConnectionString. AddGMRealTime then wires the SignalR Redis backplane, so SendToUserAsync / SendToGroupAsync / SendToAllAsync reach 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 : NotificationBase entity (channel = RealTime) in GM.Notifications.Domain, with a matching EF configuration in GM.Notifications.Persistence.
  • A RealTimeWorker (like the Email/SMS workers) polls pending RealTimeNotifications and calls IRealTimeSenderService.SendAsync, which delegates to IRealTimeSender.SendToUserAsync — marking the notification Sent, or Failed (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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on GM.RealTime.Domain:

Package Downloads
GM.RealTime.Persistence

Cache-backed IConnectionRegistry for GM.RealTime: stores user↔connection and group membership in GM.Caching (Redis-ready) and guards the read-modify-write with GM.DistributedLock, so presence is correct and shared across server nodes. Register with AddGMRealTimeCacheStore().

GM.RealTime

SignalR-based real-time communication for the GM.* ecosystem. IRealTimeSender pushes to users/connections/groups without touching IHubContext directly; a presence-tracking hub registers connections in a shared, lock-guarded registry (GM.Caching + GM.DistributedLock); and the WebSocket handshake reads the JWT from the access_token query string (pairs with GM.Identity). One call: AddGMRealTime().

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 125 8/2/2026
1.0.0 121 8/2/2026