From 0d8439bd4a12c91889ed1eb28236b1fb8bf8f768 Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 00:04:10 +0530 Subject: [PATCH 01/17] 996403: updated content --- .../diagram/collaborative-editing/overview.md | 2 +- .../using-redis-cache-asp-net.md | 514 +++++++----------- 2 files changed, 186 insertions(+), 330 deletions(-) diff --git a/blazor/diagram/collaborative-editing/overview.md b/blazor/diagram/collaborative-editing/overview.md index 41d779881e..e0de3298bd 100644 --- a/blazor/diagram/collaborative-editing/overview.md +++ b/blazor/diagram/collaborative-editing/overview.md @@ -14,7 +14,7 @@ Collaborative editing enables multiple users to work on the same diagram at the ## Prerequisites - *Real-time Transport Protocol*: Enables instant communication between clients and the server, ensuring that updates during collaborative editing are transmitted and reflected immediately. -- *Distributed Cache or Database*: Serves as temporary storage for the queue of editing operations, helping maintain synchronization and consistency across multiple users. +- *Distributed Cache or Database*: Serves as temporary storage for the queue of diagram editing operations, helping maintain synchronization and consistency across multiple users. ### Real time transport protocol diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index 7ae76439d7..759ec85b95 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -9,7 +9,8 @@ documentation: ug # Collaborative Editing with Redis in Blazor Diagram -Collaborative editing enables multiple users to work on the same diagram at the same time. Changes are reflected in real-time, allowing collaborators to see updates as they happen. This approach significantly improves efficiency by eliminating the need to wait for others to finish their edits, fostering seamless teamwork. +Collaborative editing allows multiple users to work on the same diagram simultaneously. All changes are synchronized in real-time, ensuring that collaborators can instantly see updates as they occur. This feature enhances productivity by removing the need to wait for others to finish their edits and promotes seamless teamwork. +By leveraging Redis as the real-time data store, the application ensures fast and reliable communication between clients, making collaborative diagram editing smooth and efficient. ## Prerequisites @@ -34,43 +35,9 @@ In collaborative editing, real-time communication is essential for users to see To make SignalR work in a distributed environment (with more than one server instance), it needs to be configured with either AspNetCore SignalR Service or a Redis backplane. -### Scale-out SignalR using AspNetCore SignalR service - -AspNetCore SignalR Service is a scalable, managed service for real-time communication in web applications. It enables real-time messaging between web clients (browsers) and your server-side application(across multiple servers). - -Below is a code snippet to configure SignalR in a Blazor application using AddSignalR - -```csharp -builder.Services.AddSignalR(options => -{ - options.EnableDetailedErrors = true; -}); -``` - - - -### Scale-out SignalR using Redis - -Using a Redis backplane, you can achieve horizontal scaling of your SignalR application. The SignalR leverages Redis to efficiently broadcast messages across multiple servers. This allows your application to handle large user bases with minimal latency. - -In the SignalR app, install the following NuGet package: - -`Microsoft.AspNetCore.SignalR.StackExchangeRedis` - -Below is a code snippet to configure Redis backplane in an ASP.NET Core application using the AddStackExchangeRedis method - -```csharp -builder.Services.AddSingleton(provider => -{ - var connectionString = builder.Configuration.GetConnectionString("Redis") ?? "localhost:6379,abortConnect=false"; - return ConnectionMultiplexer.Connect(connectionString); -}); -builder.Services.AddScoped(); -``` - ## Redis -Redis is used as a temporary data store to manage real-time collaborative editing operations. It helps queue editing actions and resolve conflicts through versioning mechanisms. +Redis is used as a temporary data store to manage real-time diagram collaborative editing operations. It helps queue editing actions and resolve conflicts through versioning mechanisms. All diagram editing operations performed during collaboration are cached in Redis. To prevent excessive memory usage, old versioning data is periodically removed from the Redis cache. @@ -80,10 +47,13 @@ Redis imposes limits on concurrent connections. Select an appropriate Redis conf ### Step 1: Configure SignalR to send and receive changes -To broadcast the changes made and receive changes from remote users, configure SignalR like below. +To enable real-time collaboration, you need to configure SignalR to broadcast changes made by one user and receive updates from other users. Below is an example implementation: ```csharp +@using Microsoft.AspNetCore.SignalR.Client + @code { + HubConnection? connection; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) @@ -91,7 +61,6 @@ To broadcast the changes made and receive changes from remote users, configure S await InitializeSignalR(); } } - private async Task InitializeSignalR() { if (connection == null) @@ -104,74 +73,55 @@ To broadcast the changes made and receive changes from remote users, configure S }) .WithAutomaticReconnect() .Build(); - connection.On("OnSaveDiagramState", OnSaveDiagramState); - connection.On("ShowConflict", ShowConflict); - connection.On("RevertCurrentChanges", RevertCurrentChanges); connection.On("OnConnectedAsync", OnConnectedAsync); - connection.On("UpdateVersion", UpdateVersion); - connection.On>("CurrentUsers", CurrentUsers); - connection.On("LoadDiagramData", OnLoadDiagramData); - connection.On, long, SelectionEvent>("ReceiveData", async (diagramChanges, serverVersion, selectionEvent) => - { - await InvokeAsync(() => OnReceiveDiagramUpdate(diagramChanges, serverVersion, selectionEvent)); - }); - connection.On("UserJoined", ShowUserJoined); - connection.On("UserLeft", ShowUserLeft); - connection.On("UpdateSelectionHighlighter", SendCurrentSelectorBoundsToOtherClient); - connection.On>("PeerSelectionsBootstrap", async list => - { - foreach (var evt in list) - _peerSelections[evt.ConnectionId] = (evt.UserName ?? "User", evt.ElementIds?.ToHashSet() ?? new(), evt.SelectorBounds); - await InvokeAsync(StateHasChanged); - }); - - connection.On("PeerSelectionChanged", async (evt) => + connection.On>("ReceiveData", async (diagramChanges) => { - await InvokeAsync(() => - { - PeerSelectionChanged(evt); - StateHasChanged(); - } - ); - }); - - connection.On("PeerSelectionCleared", async evt => - { - if (evt != null) - { - _peerSelections.Remove(evt.ConnectionId); - await InvokeAsync(StateHasChanged); - } + await ApplyRemoteDiagramChanges(diagramChanges); }); await connection.StartAsync(); } } + private async Task OnReceiveDiagramUpdate(List diagramChanges, long newVersion) + { + await DiagramInstance.SetDiagramUpdatesAsync(diagramChanges); + this.clientVersion = newVersion; + } +} ``` -### Step 2: Join SignalR room while opening the diagram +**Explanation** +* **OnConnectedAsync:** Triggered when the client successfully connects to the server and receives a unique connection ID. This ID can be used to join specific rooms for collaborative sessions. +* **ReceiveData:** Invoked whenever another user makes changes to the diagram. The received data contains the updates, which you can apply to your local diagram instance using SetDiagramUpdatesAsync(modifiedData). -When opening a diagram, we need to generate a unique ID for each diagram. These unique IDs are then used to create rooms using SignalR, which facilitates sending and receiving data from the server. +By wiring these methods during the connection setup, you enable the server to broadcast updates to all connected clients, ensuring real-time synchronization. + +### Step 2: Join SignalR room when opening the diagram + +When a diagram is opened, you can join a SignalR group (room) to enable collaborative editing. This allows sending and receiving updates only within that specific group, ensuring that changes are shared among users working on the same diagram. ```csharp - string diagramId = "diagram"; - string currentUser = string.Empty; - string roomName = "diagram_group"; + string roomName = "syncfusion"; private async Task OnConnectedAsync(string connectionId) { if(!string.IsNullOrEmpty(connectionId)) { - this.ConnectionId = connectionId; - currentUser = string.IsNullOrEmpty(currentUser) ? $"{userCount}" : currentUser; // Join the room after connection is established - await connection.SendAsync("JoinDiagram", roomName, diagramId, currentUser); + await connection.SendAsync("JoinDiagram", roomName); } } ``` +**Explanation** + +* **roomName:** Represents the unique group name for the diagram session. All users editing the same diagram should join this group. +* **OnConnectedAsync:** This method is triggered after the client successfully connects to the server and receives a unique connection ID. +* **JoinDiagram:** Invokes the server-side method to add the client to the specified SignalR group. Once joined, the client can send and receive updates within this group. -### Step 3: Broadcast current editing changes to remote users +Using SignalR groups ensures that updates are scoped to the relevant diagram, preventing unnecessary broadcasts to all connected clients. -Changes made on the client-side need to be sent to the server-side to broadcast them to other connected users. To send the changes made to the server, use the method shown below from the diagram using the `HistoryChange` event. +### Step 3: Broadcast Current Editing Changes to Remote Users + +To keep all collaborators in sync, changes made on the client-side must be sent to the server, which then broadcasts them to other connected users. This can be achieved using the `HistoryChanged` event of the Blazor Diagram component and the `GetDiagramUpdates` method. ```razor @@ -181,58 +131,52 @@ Changes made on the client-side need to be sent to the server-side to broadcast @code { public async void OnHistoryChange(HistoryChangedEventArgs args) { - if (args != null && DiagramInstance != null && !isLoadDiagram && !isRevertingCurrentChanges) + if (args != null) { - bool isUndo = args.ActionTrigger == HistoryChangedAction.Undo; - bool isStartGroup = args.EntryType == (isUndo ? HistoryEntryType.EndGroup : HistoryEntryType.StartGroup); - bool isEndGroup = args.EntryType == (isUndo ? HistoryEntryType.StartGroup : HistoryEntryType.EndGroup); - - if (isStartGroup) { editedElements = new(); isGroupAction = true; } List parsedData = DiagramInstance.GetDiagramUpdates(args); - editedElements.AddRange(GetEditedElementIds(args).ToList()); if (parsedData.Count > 0) { - var (selectedElementIds, selectorBounds) = await UpdateOtherClientSelectorBounds(); - SelectionEvent currentSelectionDetails = new SelectionEvent() { ElementIds = selectedElementIds, SelectorBounds = selectorBounds }; - if (connection.State != HubConnectionState.Disconnected) - await connection.SendAsync("BroadcastToOtherClients", parsedData, clientVersion, editedElements, currentSelectionDetails, roomName); + // Send changes to the server for broadcasting + await connection.SendAsync("BroadcastToOtherClients", parsedData, roomName); } - if (isEndGroup || !isGroupAction) { editedElements = new(); isGroupAction = false; } } } } ``` +**Explanation** +* **HistoryChanged Event:** Triggered whenever a change occurs in the diagram (e.g., adding, deleting, or modifying shapes/connectors). +**GetDiagramUpdates:** Serializes the diagram changes into a JSON format suitable for transmission to the server. This ensures that updates can be easily processed and applied by other clients. +**BroadcastToOtherClients:** A server-side SignalR method that sends updates to all clients in the same SignalR group (room). -## How to enable collaborative editing in Blazor +**Grouped Interactions** +To optimize broadcasting during grouped actions (e.g., multiple changes in a single operation): -### Step 1: Configure SignalR hub to create room for collaborative editing session +Enable `EnableGroupActions` in DiagramHistoryManager +```razor + +``` +This ensures `StartGroupAction` and `EndGroupAction` notifications are included in `HistoryChangedEvent`, allowing you to broadcast changes only after the group action completes. + +## Server configuration -To manage groups for each diagram, create a folder named “Hubs” and add a file named “DiagramHub.cs” inside it. Add the following code to the file to manage SignalR groups using room names. +### Step 1: Configure SignalR Hub to Create Rooms for Collaborative Editing Sessions -Join the group by using unique id of the diagram by using `JoinGroup` method. +To manage SignalR groups for each diagram, create a folder named Hubs and add a file named DiagramHub.cs inside it. This hub will handle group management and broadcasting updates to connected clients. + +Use the `JoinDiagram` method to join a SignalR group (room) based on the unique connection ID. ```csharp using Microsoft.AspNetCore.SignalR; -using System.Collections.Concurrent; namespace DiagramServerApplication.Hubs { public class DiagramHub : Hub { - private readonly IDiagramService _diagramService; - private readonly IRedisService _redisService; private readonly ILogger _logger; - private readonly IHubContext _diagramHubContext; - private static readonly ConcurrentDictionary _diagramUsers = new(); - public DiagramHub( - IDiagramService diagramService, IRedisService redis, - ILogger logger, IHubContext diagramHubContext) + public DiagramHub(ILogger logger) { - _diagramService = diagramService; - _redisService = redis; _logger = logger; - _diagramHubContext = diagramHubContext; } public override Task OnConnectedAsync() @@ -242,150 +186,44 @@ namespace DiagramServerApplication.Hubs return base.OnConnectedAsync(); } - public async Task JoinDiagram(string roomName, string diagramId, string userName) + public async Task JoinDiagram(string roomName) { try { string userId = Context.ConnectionId; - if (_diagramUsers.TryGetValue(userId, out var existingUser)) - { - if (existingUser != null) - { - _diagramUsers.TryRemove(userId, out _); - await Groups.RemoveFromGroupAsync(userId, roomName); - _logger.LogInformation("Removed existing connection for user {UserId} before adding new one", userId); - } - } // Add to SignalR group await Groups.AddToGroupAsync(userId, roomName); - - // Store connection context - Context.Items["DiagramId"] = diagramId; - Context.Items["UserId"] = userId; - Context.Items["UserName"] = userName; - Context.Items["RoomName"] = roomName; - - // Track user in diagram - var diagramUser = new DiagramUser - { - ConnectionId = Context.ConnectionId, - UserName = userName, - }; - bool userExists = _diagramUsers?.Count > 0; - if (!userExists) - await ClearConnectionsFromRedis(); - _diagramUsers.AddOrUpdate(userId, diagramUser, - (key, existingValue) => diagramUser - ); - await RequestAndLoadStateAsync(roomName, diagramId, Context.ConnectionId, Context.ConnectionAborted); - - long currentServerVersion = await GetDiagramVersion(); - await Clients.Caller.SendAsync("UpdateVersion", currentServerVersion); - await Clients.OthersInGroup(roomName).SendAsync("UserJoined", userName); - await SendCurrentSelectionsToCaller(); - List activeUsers = GetCurrentUsers(); - await Clients.Group(roomName).SendAsync("CurrentUsers", activeUsers); - _logger.LogInformation("User {UserId} ({UserName}) joined diagram {DiagramId}. Total users: {UserCount}", userId, userName, diagramId, _diagramUsers.Count); } catch (Exception ex) { - _logger.LogError(ex, "Error joining diagram {DiagramId} for user {UserId}", diagramId, Context.ConnectionId); - await Clients.Caller.SendAsync("JoinError", "Failed to join diagram"); + _logger.LogError(ex); } } - public async Task BroadcastToOtherClients(List payloads, long clientVersion, List? elementIds, SelectionEvent currentSelection, string roomName) + public async Task BroadcastToOtherClients(List payloads, string roomName) { - var connId = Context.ConnectionId; - var gate = GetConnectionLock(connId); - await gate.WaitAsync(); try { - var versionKey = "diagram:version"; - - var (acceptedSingle, serverVersionSingle) = await _redisService.CompareAndIncrementAsync(versionKey, clientVersion); - long serverVersionFinal = serverVersionSingle; - - if (!acceptedSingle) - { - var recentUpdates = await GetUpdatesSinceVersionAsync(clientVersion, maxScan: 200); - var recentlyTouched = new HashSet(StringComparer.Ordinal); - foreach (var upd in recentUpdates) - { - if (upd.ModifiedElementIds == null) continue; - foreach (var id in upd.ModifiedElementIds) - recentlyTouched.Add(id); - } - - var overlaps = elementIds?.Where(id => recentlyTouched.Contains(id)).Distinct().ToList(); - if (overlaps?.Count > 0) - { - await Clients.Caller.SendAsync("RevertCurrentChanges", elementIds); - await Clients.Caller.SendAsync("ShowConflict"); - return; - } - - var (_, newServerVersion) = await _redisService.CompareAndIncrementAsync(versionKey, serverVersionSingle); - serverVersionFinal = newServerVersion; - } - - var update = new DiagramUpdateMessage - { - SourceConnectionId = connId, - Version = serverVersionFinal, - ModifiedElementIds = elementIds - }; - - await StoreUpdateInRedis(update, connId); - SelectionEvent selectionEvent = BuildSelectedElementEvent(currentSelection.ElementIds, currentSelection.SelectorBounds); - await UpdateSelectionBoundsInRedis(selectionEvent, currentSelection.ElementIds, currentSelection.SelectorBounds); - await Clients.OthersInGroup(roomName).SendAsync("ReceiveData", payloads, serverVersionFinal, selectionEvent); - await RemoveOldUpdates(serverVersionFinal); + await Clients.OthersInGroup(roomName).SendAsync("ReceiveData", payloads); } - finally + catch (Exception ex) { - gate.Release(); + _logger.LogError(ex); } } public override async Task OnDisconnectedAsync(Exception? exception) { try { - string roomName = Context.Items["RoomName"]?.ToString(); - string userName = Context.Items["UserName"]?.ToString(); - - await Clients.OthersInGroup(roomName) - .SendAsync("PeerSelectionCleared", new SelectionEvent - { - ConnectionId = Context.ConnectionId, - ElementIds = new() - }); - await Clients.OthersInGroup(roomName).SendAsync("UserLeft", userName); - // Remove from SignalR group await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName); - await _redisService.DeleteAsync(SelectionKey(Context.ConnectionId)); - - // Remove from diagram users tracking - if (_diagramUsers.TryGetValue(Context.ConnectionId, out var user)) - { - if (user != null) - _diagramUsers.TryRemove(Context.ConnectionId, out _); - } - List activeUsers = GetCurrentUsers(); - await Clients.Group(roomName).SendAsync("CurrentUsers", activeUsers); - // Clear context - Context.Items.Remove("DiagramId"); - Context.Items.Remove("UserId"); - Context.Items.Remove("UserName"); await base.OnDisconnectedAsync(exception); } catch (Exception ex) { - _logger.LogError(ex, "Error during disconnect cleanup for connection {ConnectionId}", Context.ConnectionId); + _logger.LogError(ex); } - await base.OnDisconnectedAsync(exception); } } } @@ -427,10 +265,10 @@ Notes: - Ensure WebSockets are enabled on the host/proxy, or remove SkipNegotiation on the client to allow fallback transports. - Use a singleton IConnectionMultiplexer to respect Redis connection limits. -### Step 3: Configure Redis cache connection string in application level - -Configure the Redis that stores temporary data for the collaborative editing session. Provide the Redis connection string in `appsettings.json` file. +### Step 3: Configure Redis Cache Connection String at the Application Level +To enable collaborative editing with real-time synchronization, configure Redis as the temporary data store. Redis ensures fast and reliable communication between multiple server instances when scaling the application. +Add your Redis connection string in the `appsettings.json` file: ```json { "Logging": { @@ -446,99 +284,145 @@ Configure the Redis that stores temporary data for the collaborative editing ses } ``` +## Conflict policy (optimistic concurrency) -## Model types used in the sample (minimal) - -Define these models used by the snippets: +To handle conflicts during collaborative editing, we use an optimistic concurrency approach based on versioning: -```csharp -public sealed class SelectionEvent -{ - public string ConnectionId { get; set; } = string.Empty; - public string? UserName { get; set; } - public List? ElementIds { get; set; } - public Rect? SelectorBounds { get; set; } // define Rect for your app -} +* **Versioning**: Each update is associated with a clientVersion and the list of editedElementIds. +* **Client Update:** The client sends its changes along with the current clientVersion and the affected element IDs. -public sealed class DiagramUser -{ - public string ConnectionId { get; set; } = string.Empty; - public string UserName { get; set; } = "User"; -} +* **Server Validation:** The server compares the incoming clientVersion with the latest version stored in Redis. + * If the update is stale and elements overlap: + * The server rejects the update and instructs the client to revert changes. + * A conflict notification is shown to the user. -public sealed class DiagramUpdateMessage -{ - public string SourceConnectionId { get; set; } = string.Empty; - public long Version { get; set; } - public List? ModifiedElementIds { get; set; } - public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.UtcNow; -} + * If the update is stale but no overlap exists: + * The server accepts the update and increments the version. -public sealed class DiagramData -{ - public string DiagramId { get; set; } = string.Empty; - public string? SerializedState { get; set; } - public long Version { get; set; } -} -``` +* **Client Synchronization:** After an update is accepted, the client must set its clientVersion to the latest version provided by the server. -## Client essentials (versioning, reconnect, and revert) +This approach ensures consistency while allowing multiple users to edit collaboratively without locking resources. ```csharp -long clientVersion = 0; -bool isRevertingCurrentChanges = false; +@using Microsoft.AspNetCore.SignalR.Client -private void UpdateVersion(long serverVersion) -{ - clientVersion = serverVersion; -} - -private async Task RevertCurrentChanges(List elementIds) -{ - isRevertingCurrentChanges = true; - try +@code { + HubConnection? connection; + protected override async Task OnAfterRenderAsync(bool firstRender) { - await ReloadElementsFromServerOrCache(elementIds); + if (firstRender) + { + await InitializeSignalR(); + } } - finally + + private async Task InitializeSignalR() { - isRevertingCurrentChanges = false; + if (connection == null) + { + connection = new HubConnectionBuilder() + .WithUrl(NavigationManager.ToAbsoluteUri("/diagramHub"), options => + { + options.SkipNegotiation = true; + options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets; + }) + .WithAutomaticReconnect() + .Build(); + connection.On("OnConnectedAsync", OnConnectedAsync); + connection.On("ShowConflict", ShowConflict); + connection.On("UpdateVersion", UpdateVersion); + connection.On>("ReceiveData", async (diagramChanges) => + { + await ApplyRemoteDiagramChanges(diagramChanges); + }); + await connection.StartAsync(); + } } -} - -// Rejoin the diagram room if connection drops and reconnects -connection.Reconnected += async _ => -{ - await connection.SendAsync("JoinDiagram", roomName, diagramId, currentUser); -}; -``` - -When using HistoryChange, ensure you declare: - -```csharp -List editedElements = new(); -bool isGroupAction = false; ``` -## Per-diagram versioning keys (server) - -Avoid a global version key. Use per-diagram keys: - ```csharp -private static string VersionKey(string diagramId) => $"diagram:{diagramId}:version"; -private static string UpdateKey(long version, string diagramId) => $"diagram:{diagramId}:update:{version}"; -private static string SelectionKey(string connectionId, string diagramId) => $"diagram:{diagramId}:selection:{connectionId}"; -``` +using Microsoft.AspNetCore.SignalR; +public class DiagramHub : Hub +{ + private readonly IDiagramService _diagramService; + private readonly IRedisService _redisService; + private readonly ILogger _logger; + private readonly IHubContext _diagramHubContext; + private static readonly ConcurrentDictionary _diagramUsers = new(); + + public DiagramHub( + IDiagramService diagramService, IRedisService redis, + ILogger logger, IHubContext diagramHubContext) + { + _diagramService = diagramService; + _redisService = redis; + _logger = logger; + _diagramHubContext = diagramHubContext; + } -Read diagramId from Context.Items["DiagramId"] inside hub methods and use it for all keys. + public override Task OnConnectedAsync() + { + // Send session id to client. + Clients.Caller.SendAsync("OnConnectedAsync", Context.ConnectionId); + return base.OnConnectedAsync(); + } + public async Task BroadcastToOtherClients(List payloads, long clientVersion, List? elementIds, string roomName) + { + try + { + var versionKey = "diagram:version"; + var (accepted, serverVersion) = await _redisService.CompareAndIncrementAsync(versionKey, clientVersion); -## Conflict policy (optimistic concurrency) + if (!accepted) + { + var recentUpdates = await GetUpdatesSinceVersionAsync(clientVersion, maxScan: 200); + var recentlyTouched = new HashSet(StringComparer.Ordinal); + foreach (var upd in recentUpdates) + { + if (upd.ModifiedElementIds == null) continue; + foreach (var id in upd.ModifiedElementIds) + recentlyTouched.Add(id); + } -- Client sends payload with clientVersion and edited elementIds. -- Server compares with Redis version. If stale and elements overlap, ask client to revert and show conflict. -- If stale but no overlap, server increments and accepts. -- Clients must set clientVersion to the server version after each accepted update. + var overlaps = elementIds?.Where(id => recentlyTouched.Contains(id)).Distinct().ToList(); + if (overlaps?.Count > 0) + { + await Clients.Caller.SendAsync("ShowConflict"); + return; + } + var (_, newServerVersion) = await _redisService.CompareAndIncrementAsync(versionKey, serverVersion); + serverVersion = newServerVersion; + } + var update = new DiagramUpdateMessage + { + SourceConnectionId = connId, + Version = serverVersion, + ModifiedElementIds = elementIds + }; + await StoreUpdateInRedis(update); + await Clients.OthersInGroup(roomName).SendAsync("ReceiveData", payloads, serverVersion); + } + catch (Exception ex) + { + _logger.LogError(ex); + } + } + private async Task StoreUpdateInRedis(DiagramUpdateMessage updateMessage) + { + try + { + // Store in updates history list + var historyKey = "diagram_updates_history"; + await _redisService.ListPushAsync(historyKey, updateMessage); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error storing update in Redis for diagram"); + } + } +} +``` ## Cleanup strategy for Redis - Keep only the last K versions (e.g., 200), or @@ -556,34 +440,6 @@ Read diagramId from Context.Items["DiagramId"] inside hub methods and use it for - Consider authentication/authorization to join rooms. - Rate-limit BroadcastToOtherClients if necessary. -## App settings example - -```json -{ - "ConnectionStrings": { - "RedisConnectionString": "<>" - } -} -``` - -## CollaborationServer helper methods (required in the sample) - -Implement or verify these server helpers exist in the Hub or related services; they are invoked in the snippets above: - -- GetConnectionLock(string connectionId): returns a per-connection SemaphoreSlim for serializing updates. -- RequestAndLoadStateAsync(string roomName, string diagramId, string connectionId, CancellationToken abort): loads existing diagram state (from DB/Redis) and sends to caller via LoadDiagramData. -- GetDiagramVersion(): reads current version from Redis for the current diagram (use VersionKey(diagramId)); return 0 if missing. -- GetUpdatesSinceVersionAsync(long sinceVersion, int maxScan): reads recent DiagramUpdateMessage entries from Redis for conflict checks. -- StoreUpdateInRedis(DiagramUpdateMessage update, string connectionId): stores update under UpdateKey(update.Version, diagramId). -- UpdateSelectionBoundsInRedis(SelectionEvent evt, List? elementIds, Rect? selectorBounds): persists the caller’s selection snapshot under SelectionKey(connectionId, diagramId). -- SendCurrentSelectionsToCaller(): gathers SelectionEvent for active peers in this diagram and sends PeerSelectionsBootstrap to the caller. -- GetCurrentUsers(): returns display names of users in _diagramUsers for this diagram/group. -- RemoveOldUpdates(long latestVersion): trims old updates (keep last K versions or apply TTL) for this diagram. -- ClearConnectionsFromRedis(): clears stale selection keys for all users when the first user joins. -- SelectionKey(string connectionId): if you keep this overload, ensure it internally resolves diagramId; otherwise prefer SelectionKey(connectionId, diagramId). -- BuildSelectedElementEvent(IEnumerable? elementIds, Rect? selectorBounds): constructs SelectionEvent with Context.ConnectionId and current user name. - - -The full version of the code discussed can be found in the GitHub location below. +The full version of the code can be found in the GitHub location below. GitHub Example: Collaborative editing examples From e7ed1ff630807646da6ec233844406812fff9617 Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 11:48:46 +0530 Subject: [PATCH 02/17] 996403: content updated --- .../using-redis-cache-asp-net.md | 301 +++++++++++++++--- 1 file changed, 251 insertions(+), 50 deletions(-) diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index 759ec85b95..d3cc78f7b8 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -76,16 +76,12 @@ To enable real-time collaboration, you need to configure SignalR to broadcast ch connection.On("OnConnectedAsync", OnConnectedAsync); connection.On>("ReceiveData", async (diagramChanges) => { - await ApplyRemoteDiagramChanges(diagramChanges); + await DiagramInstance.SetDiagramUpdatesAsync(diagramChanges); + }); await connection.StartAsync(); } } - private async Task OnReceiveDiagramUpdate(List diagramChanges, long newVersion) - { - await DiagramInstance.SetDiagramUpdatesAsync(diagramChanges); - this.clientVersion = newVersion; - } } ``` @@ -124,9 +120,9 @@ Using SignalR groups ensures that updates are scoped to the relevant diagram, pr To keep all collaborators in sync, changes made on the client-side must be sent to the server, which then broadcasts them to other connected users. This can be achieved using the `HistoryChanged` event of the Blazor Diagram component and the `GetDiagramUpdates` method. ```razor - - - + + + @code { public async void OnHistoryChange(HistoryChangedEventArgs args) @@ -286,28 +282,29 @@ Add your Redis connection string in the `appsettings.json` file: ## Conflict policy (optimistic concurrency) -To handle conflicts during collaborative editing, we use an optimistic concurrency approach based on versioning: +To handle conflicts during collaborative editing, we use an optimistic concurrency strategy with versioning: -* **Versioning**: Each update is associated with a clientVersion and the list of editedElementIds. -* **Client Update:** The client sends its changes along with the current clientVersion and the affected element IDs. +* **Versioning**: Each update carries the client’s clientVersion and the list of editedElementIds. +* **Client Update:** The client sends serialized diagram changes, clientVersion, and editedElementIds to the server. * **Server Validation:** The server compares the incoming clientVersion with the latest version stored in Redis. - * If the update is stale and elements overlap: - * The server rejects the update and instructs the client to revert changes. - * A conflict notification is shown to the user. - - * If the update is stale but no overlap exists: - * The server accepts the update and increments the version. - -* **Client Synchronization:** After an update is accepted, the client must set its clientVersion to the latest version provided by the server. + * **If stale and overlapping elements exist:** reject the update, instruct the client to revert, and show a conflict notice. + * **If stale but no overlap:** accept the update and increment the version atomically. +* **Client Synchronization:** After acceptance, the client must update its local clientVersion to the server version. -This approach ensures consistency while allowing multiple users to edit collaboratively without locking resources. +This approach keeps collaborators in sync without locking, while ensuring deterministic conflict handling. -```csharp +**Client (Blazor) – Send updates & apply remote changes** +```razor @using Microsoft.AspNetCore.SignalR.Client + + + @code { HubConnection? connection; + private long clientVersion = 0; + protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) @@ -315,7 +312,6 @@ This approach ensures consistency while allowing multiple users to edit collabor await InitializeSignalR(); } } - private async Task InitializeSignalR() { if (connection == null) @@ -329,19 +325,65 @@ This approach ensures consistency while allowing multiple users to edit collabor .WithAutomaticReconnect() .Build(); connection.On("OnConnectedAsync", OnConnectedAsync); + // Show conflict notification connection.On("ShowConflict", ShowConflict); connection.On("UpdateVersion", UpdateVersion); - connection.On>("ReceiveData", async (diagramChanges) => + // Receive remote changes with the new server version + connection.On>("ReceiveData", async (diagramChanges, serverVersion) => { await ApplyRemoteDiagramChanges(diagramChanges); }); await connection.StartAsync(); } } -``` + private async Task ApplyRemoteDiagramChanges(List diagramChanges, long newVersion) + { + // Apply remote diagram changes to local diagram + await DiagramInstance.SetDiagramUpdatesAsync(diagramChanges); + // Update to latest server version + this.clientVersion = newVersion; + } + public async Task OnHistoryChange(HistoryChangedEventArgs args) + { + if (args != null) + { + List parsedData = DiagramInstance.GetDiagramUpdates(args); + if (parsedData.Count > 0) + { + List editedElements = GetEditedElements(args); + // Send changes to the server for broadcasting + await connection.SendAsync("BroadcastToOtherClients", parsedData, clientVersion, editedElements, roomName); + } + } + } + private void UpdateVersion(long newVersion) + { + this.clientVersion = newVersion; + } + private void ShowConflict() + { + // Show a dialog telling the user their change was rejected due to overlap + } + + private List GetEditedElements(HistoryChangedEventArgs args) + { + // TODO: extract IDs from args (nodes/connectors edited) + return new List(); + } +} +``` +**Server (SignalR Hub) – Validate with Redis and broadcast** ```csharp using Microsoft.AspNetCore.SignalR; + +public class DiagramUpdateMessage +{ + public string SourceConnectionId { get; set; } = ""; + public long Version { get; set; } + public List? ModifiedElementIds { get; set; } +} + public class DiagramHub : Hub { private readonly IDiagramService _diagramService; @@ -350,31 +392,22 @@ public class DiagramHub : Hub private readonly IHubContext _diagramHubContext; private static readonly ConcurrentDictionary _diagramUsers = new(); - public DiagramHub( - IDiagramService diagramService, IRedisService redis, - ILogger logger, IHubContext diagramHubContext) + public DiagramHub(IRedisService redis, ILogger logger) { - _diagramService = diagramService; _redisService = redis; _logger = logger; - _diagramHubContext = diagramHubContext; - } - - public override Task OnConnectedAsync() - { - // Send session id to client. - Clients.Caller.SendAsync("OnConnectedAsync", Context.ConnectionId); - return base.OnConnectedAsync(); } public async Task BroadcastToOtherClients(List payloads, long clientVersion, List? elementIds, string roomName) { try { var versionKey = "diagram:version"; + // Try to accept based on expected version (CAS via Lua) var (accepted, serverVersion) = await _redisService.CompareAndIncrementAsync(versionKey, clientVersion); if (!accepted) { + // Check for overlaps since client's version var recentUpdates = await GetUpdatesSinceVersionAsync(clientVersion, maxScan: 200); var recentlyTouched = new HashSet(StringComparer.Ordinal); foreach (var upd in recentUpdates) @@ -387,13 +420,16 @@ public class DiagramHub : Hub var overlaps = elementIds?.Where(id => recentlyTouched.Contains(id)).Distinct().ToList(); if (overlaps?.Count > 0) { + // Reject & notify caller of conflict await Clients.Caller.SendAsync("ShowConflict"); return; } + // Accept non-overlapping stale update: increment once more var (_, newServerVersion) = await _redisService.CompareAndIncrementAsync(versionKey, serverVersion); serverVersion = newServerVersion; } + // Store update in Redis history var update = new DiagramUpdateMessage { SourceConnectionId = connId, @@ -401,6 +437,7 @@ public class DiagramHub : Hub ModifiedElementIds = elementIds }; await StoreUpdateInRedis(update); + // Broadcast to others in the same room, including serverVersion await Clients.OthersInGroup(roomName).SendAsync("ReceiveData", payloads, serverVersion); } catch (Exception ex) @@ -408,11 +445,33 @@ public class DiagramHub : Hub _logger.LogError(ex); } } + private async Task> GetUpdatesSinceVersionAsync(long sinceVersion, int maxScan = 200) + { + var historyKey = "diagram_updates_history"; + var length = await _redisService.ListLengthAsync(historyKey); + if (length == 0) return Array.Empty(); + + long start = Math.Max(0, length - maxScan); + long end = length - 1; + + var range = await _redisService.ListRangeAsync(historyKey, start, end); + + var results = new List(range.Length); + foreach (var item in range) + { + if (item.IsNullOrEmpty) continue; + var update = JsonSerializer.Deserialize(item.ToString()); + if (update is not null && update.Version > sinceVersion && update.SourceConnectionId != Context.ConnectionId) + results.Add(update); + } + results.Sort((a, b) => a.Version.CompareTo(b.Version)); + return results; + } private async Task StoreUpdateInRedis(DiagramUpdateMessage updateMessage) { try { - // Store in updates history list + // Store updates in redis var historyKey = "diagram_updates_history"; await _redisService.ListPushAsync(historyKey, updateMessage); } @@ -423,23 +482,165 @@ public class DiagramHub : Hub } } ``` -## Cleanup strategy for Redis +**Redis service interface & implementation** +```csharp +using StackExchange.Redis; -- Keep only the last K versions (e.g., 200), or -- Set TTL on update keys to bound memory usage. +public interface IRedisService +{ + Task ListPushAsync(string key, T value); + Task<(bool accepted, long version)> CompareAndIncrementAsync(string key, long expectedVersion); + Task ListLengthAsync(string key); + Task ListRangeAsync(string key, long start = 0, long stop = -1); +} +``` +```csharp +using Microsoft.Extensions.Logging; +using StackExchange.Redis; +using System.Text.Json; -## Hosting, transport, and serialization + public class RedisService : IRedisService + { + private readonly IDatabase _database; + private readonly ILogger _logger; + + public RedisService(IConnectionMultiplexer redis, ILogger logger) + { + _database = redis.GetDatabase(); + _logger = logger; + } + + public async Task<(bool accepted, long version)> CompareAndIncrementAsync(string key, long expectedVersion) + { + const string lua = @" +local v = redis.call('GET', KEYS[1]) +if not v then + v = 0 +else + v = tonumber(v) or 0 +end + +local expected = tonumber(ARGV[1]) or -1 + +if v == expected then + local newv = redis.call('INCR', KEYS[1]) + return {1, newv} +else + return {0, v} +end +"; + try + { + var result = (StackExchange.Redis.RedisResult[])await _database.ScriptEvaluateAsync( + lua, + keys: new StackExchange.Redis.RedisKey[] { key }, + values: new StackExchange.Redis.RedisValue[] { expectedVersion.ToString() }); + + bool accepted = (int)result[0] == 1; + + long version; + if (result[1].Type == StackExchange.Redis.ResultType.Integer) + version = (long)result[1]; + else + version = long.Parse((string)result[1]); + + return (accepted, version); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error in CompareAndIncrementAsync for key {Key}", key); + } + } + public async Task ListPushAsync(string key, T value) + { + try + { + var serializedValue = JsonSerializer.Serialize(value); + return await _database.ListLeftPushAsync(key, serializedValue); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error pushing to list {Key}", key); + } + } + public async Task ListLengthAsync(string key) + { + try + { + return await _database.ListLengthAsync(key); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting list length {Key}", key); + return 0; + } + } + public async Task ListRangeAsync(string key, long start = 0, long stop = -1) + { + try + { + return await _database.ListRangeAsync(key, start, stop); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting list range {Key}", key); + } + } + } +``` + +## Cleanup strategy for Redis + +To prevent unbounded memory growth and maintain optimal performance, implement one or both of the following strategies: +* **Keep Only the Last K Versions** + * Maintain a fixed-size history list (e.g., last 200 updates) by trimming older entries after each push. + * This ensures that only recent updates are retained for conflict resolution. +* **Set TTL (Time-to-Live) on Update Keys** + * Apply an expiration policy to Redis keys storing version and history data: +```csharp +// In IRedisService +Task SetAsync(string key, T value, TimeSpan? expiry = null); +// In RedisService +public async Task SetAsync(string key, T value, TimeSpan? expiry = null) +{ + try + { + var serializedValue = JsonSerializer.Serialize(value); + return await _database.StringSetAsync(key, serializedValue, expiry); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error setting key {Key}", key); + return false; + } +} -- Enable WebSockets on your host/reverse proxy; consider keep-alives. -- If WebSockets aren’t available, remove SkipNegotiation on the client to allow fallback transports. -- For large payloads, enable MessagePack on server (and client if applicable) and consider sending diffs. +// Applying TTL to the version key +const string versionKey = "diagram:version"; +long version = 5; +await _redisService.SetAsync(versionKey, version, TimeSpan.FromHours(1)); +``` + * This bounds memory usage and automatically cleans up stale sessions. -## Security and rooms +## Hosting, transport, and serialization -- Derive roomName from diagramId (e.g., "diagram:" + diagramId) and validate/normalize on server. -- Consider authentication/authorization to join rooms. -- Rate-limit BroadcastToOtherClients if necessary. +To ensure reliable and efficient collaborative editing, consider the following best practices: +**1. Hosting** + * Enable WebSockets on your hosting environment and reverse proxy (e.g., Nginx, IIS, Azure App Service). + * Configure keep-alives to maintain long-lived connections and prevent timeouts during idle periods. +**2. Transport** + * **Preferred:** WebSockets for low-latency, full-duplex communication. + * **Fallback:** If WebSockets are unavailable, remove SkipNegotiation on the client to allow SignalR to fall back to Server-Sent Events (SSE) or Long Polling. +**3. Serialization** + * For large payloads, enable MessagePack on both server and client for efficient binary serialization. + * Consider sending diffs (incremental changes) instead of full diagram state to reduce bandwidth usage. + +## Limitations +* **Single-Server Hosting Only** + * The current implementation does not support multiple server instances. SignalR connections and Redis-based versioning are designed for a single-node setup. Scaling out requires configuring a SignalR backplane (e.g., Redis backplane) and ensuring consistent state across nodes. +* **Zoom and Pan Not Collaborative** + * Zoom and pan actions are local to each client and are not synchronized across users. This means collaborators may view different portions of the diagram independently. The full version of the code can be found in the GitHub location below. -GitHub Example: Collaborative editing examples +GitHub Example: [Collaborative editing examples](https://github.com/syncfusion/blazor-showcase-diagram-collaborative-editing). From 6a2936ddd4155f1eafb377d3eddf887385bdf469 Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 12:18:17 +0530 Subject: [PATCH 03/17] 9906403: changes added --- .../using-redis-cache-asp-net.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index d3cc78f7b8..9d414a3171 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -141,8 +141,8 @@ To keep all collaborators in sync, changes made on the client-side must be sent ``` **Explanation** * **HistoryChanged Event:** Triggered whenever a change occurs in the diagram (e.g., adding, deleting, or modifying shapes/connectors). -**GetDiagramUpdates:** Serializes the diagram changes into a JSON format suitable for transmission to the server. This ensures that updates can be easily processed and applied by other clients. -**BroadcastToOtherClients:** A server-side SignalR method that sends updates to all clients in the same SignalR group (room). +* **GetDiagramUpdates:** Serializes the diagram changes into a JSON format suitable for transmission to the server. This ensures that updates can be easily processed and applied by other clients. +* **BroadcastToOtherClients:** A server-side SignalR method that sends updates to all clients in the same SignalR group (room). **Grouped Interactions** To optimize broadcasting during grouped actions (e.g., multiple changes in a single operation): @@ -596,7 +596,8 @@ To prevent unbounded memory growth and maintain optimal performance, implement o * Maintain a fixed-size history list (e.g., last 200 updates) by trimming older entries after each push. * This ensures that only recent updates are retained for conflict resolution. * **Set TTL (Time-to-Live) on Update Keys** - * Apply an expiration policy to Redis keys storing version and history data: + * Apply an expiration policy to Redis keys storing version and history data. + * This bounds memory usage and automatically cleans up stale sessions. ```csharp // In IRedisService Task SetAsync(string key, T value, TimeSpan? expiry = null); @@ -620,18 +621,17 @@ const string versionKey = "diagram:version"; long version = 5; await _redisService.SetAsync(versionKey, version, TimeSpan.FromHours(1)); ``` - * This bounds memory usage and automatically cleans up stale sessions. ## Hosting, transport, and serialization To ensure reliable and efficient collaborative editing, consider the following best practices: -**1. Hosting** +* **1. Hosting** * Enable WebSockets on your hosting environment and reverse proxy (e.g., Nginx, IIS, Azure App Service). * Configure keep-alives to maintain long-lived connections and prevent timeouts during idle periods. -**2. Transport** +* **2. Transport** * **Preferred:** WebSockets for low-latency, full-duplex communication. * **Fallback:** If WebSockets are unavailable, remove SkipNegotiation on the client to allow SignalR to fall back to Server-Sent Events (SSE) or Long Polling. -**3. Serialization** +* **3. Serialization** * For large payloads, enable MessagePack on both server and client for efficient binary serialization. * Consider sending diffs (incremental changes) instead of full diagram state to reduce bandwidth usage. From 5a8d8eea13e08f58cfc91dcbf3a04af247924738 Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 12:30:14 +0530 Subject: [PATCH 04/17] 996403: changes added --- .../diagram/collaborative-editing/using-redis-cache-asp-net.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index 9d414a3171..e186c21226 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -87,7 +87,7 @@ To enable real-time collaboration, you need to configure SignalR to broadcast ch **Explanation** * **OnConnectedAsync:** Triggered when the client successfully connects to the server and receives a unique connection ID. This ID can be used to join specific rooms for collaborative sessions. -* **ReceiveData:** Invoked whenever another user makes changes to the diagram. The received data contains the updates, which you can apply to your local diagram instance using SetDiagramUpdatesAsync(modifiedData). +* **ReceiveData:** Invoked whenever another user makes changes to the diagram. The received data contains the updates, which you can apply to your local diagram instance using `SetDiagramUpdatesAsync(diagramChanges)`. By wiring these methods during the connection setup, you enable the server to broadcast updates to all connected clients, ensuring real-time synchronization. From 440c04130454a26e0115000d9a0a9aaf9899328b Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 14:37:20 +0530 Subject: [PATCH 05/17] 996403: changes added --- .../using-redis-cache-asp-net.md | 157 +++--------------- 1 file changed, 25 insertions(+), 132 deletions(-) diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index e186c21226..5c33295279 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -45,15 +45,18 @@ Redis imposes limits on concurrent connections. Select an appropriate Redis conf ## How to enable collaborative editing in client side -### Step 1: Configure SignalR to send and receive changes +### Step 1: Configure SignalR Connection +To enable real-time collaboration, you need to establish a SignalR connection that can send and receive diagram updates. This connection will allow the client to join a SignalR group (room) for collaborative editing, ensuring changes are shared only among users working on the same diagram. -To enable real-time collaboration, you need to configure SignalR to broadcast changes made by one user and receive updates from other users. Below is an example implementation: +The **RoomName** represents the unique group name for the diagram session, and all users editing the same diagram should join this group to share updates within that session. The **OnConnectedAsync** method is triggered after the client successfully connects to the server and receives a unique connection ID, confirming the connection. After that, the **JoinDiagram** method is called to add the client to the specified SignalR group, enabling the client to send and receive real-time updates with other users in the same room. ```csharp @using Microsoft.AspNetCore.SignalR.Client @code { HubConnection? connection; + string RoomName = "Syncfusion"; + protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) @@ -74,50 +77,27 @@ To enable real-time collaboration, you need to configure SignalR to broadcast ch .WithAutomaticReconnect() .Build(); connection.On("OnConnectedAsync", OnConnectedAsync); - connection.On>("ReceiveData", async (diagramChanges) => - { - await DiagramInstance.SetDiagramUpdatesAsync(diagramChanges); - - }); await connection.StartAsync(); } } -} -``` - -**Explanation** -* **OnConnectedAsync:** Triggered when the client successfully connects to the server and receives a unique connection ID. This ID can be used to join specific rooms for collaborative sessions. -* **ReceiveData:** Invoked whenever another user makes changes to the diagram. The received data contains the updates, which you can apply to your local diagram instance using `SetDiagramUpdatesAsync(diagramChanges)`. - -By wiring these methods during the connection setup, you enable the server to broadcast updates to all connected clients, ensuring real-time synchronization. - -### Step 2: Join SignalR room when opening the diagram - -When a diagram is opened, you can join a SignalR group (room) to enable collaborative editing. This allows sending and receiving updates only within that specific group, ensuring that changes are shared among users working on the same diagram. - -```csharp - string roomName = "syncfusion"; - private async Task OnConnectedAsync(string connectionId) { if(!string.IsNullOrEmpty(connectionId)) { // Join the room after connection is established - await connection.SendAsync("JoinDiagram", roomName); + await connection.SendAsync("JoinDiagram", RoomName); } } +} ``` -**Explanation** -* **roomName:** Represents the unique group name for the diagram session. All users editing the same diagram should join this group. -* **OnConnectedAsync:** This method is triggered after the client successfully connects to the server and receives a unique connection ID. -* **JoinDiagram:** Invokes the server-side method to add the client to the specified SignalR group. Once joined, the client can send and receive updates within this group. +### Step 3: Broadcast Current Editing Changes to Remote Users -Using SignalR groups ensures that updates are scoped to the relevant diagram, preventing unnecessary broadcasts to all connected clients. +To keep all collaborators in sync, changes made on the client-side must be sent to the server, which then broadcasts them to other connected users. This is done by handling the HistoryChanged event of the Blazor Diagram component and using the `GetDiagramUpdates` method to serialize changes into JSON format for transmission. The server-side method BroadcastToOtherClients then sends these updates to all clients in the same SignalR group (room). -### Step 3: Broadcast Current Editing Changes to Remote Users +The `HistoryChanged` event is triggered whenever a change occurs in the diagram, such as adding, deleting, or modifying shapes or connectors. The `GetDiagramUpdates` method converts these changes into a JSON format suitable for sending to the server, ensuring updates can be easily applied by other clients. Finally, the `BroadcastToOtherClients` method on the server broadcasts these updates to all users in the same collaborative session. -To keep all collaborators in sync, changes made on the client-side must be sent to the server, which then broadcasts them to other connected users. This can be achieved using the `HistoryChanged` event of the Blazor Diagram component and the `GetDiagramUpdates` method. +For grouped interactions (e.g., multiple changes in a single operation), enable `EnableGroupActions` in `DiagramHistoryManager`. This ensures `StartGroupAction` and `EndGroupAction` notifications are included in the `HistoryChanged` event, allowing you to broadcast changes only after the group action completes. ```razor @@ -139,27 +119,17 @@ To keep all collaborators in sync, changes made on the client-side must be sent } } ``` -**Explanation** -* **HistoryChanged Event:** Triggered whenever a change occurs in the diagram (e.g., adding, deleting, or modifying shapes/connectors). -* **GetDiagramUpdates:** Serializes the diagram changes into a JSON format suitable for transmission to the server. This ensures that updates can be easily processed and applied by other clients. -* **BroadcastToOtherClients:** A server-side SignalR method that sends updates to all clients in the same SignalR group (room). - -**Grouped Interactions** -To optimize broadcasting during grouped actions (e.g., multiple changes in a single operation): - -Enable `EnableGroupActions` in DiagramHistoryManager -```razor - -``` -This ensures `StartGroupAction` and `EndGroupAction` notifications are included in `HistoryChangedEvent`, allowing you to broadcast changes only after the group action completes. ## Server configuration ### Step 1: Configure SignalR Hub to Create Rooms for Collaborative Editing Sessions -To manage SignalR groups for each diagram, create a folder named Hubs and add a file named DiagramHub.cs inside it. This hub will handle group management and broadcasting updates to connected clients. +Create a folder named Hubs and add a file DiagramHub.cs. This hub manages SignalR groups (rooms) per diagram and broadcasts updates to connected clients. -Use the `JoinDiagram` method to join a SignalR group (room) based on the unique connection ID. +**OnConnectedAsync:** It will trigger when a new client connects. Sends the generated connection ID to the client so it can be used as a session identifier. +**JoinDiagram(roomName):** Adds the current connection to a SignalR group (room) identified by roomName. Also records the mapping so the connection can be removed later. +**BroadcastToOtherClients(payloads, roomName):** Sends edits/updates to other clients in the same room (excludes the sender). +**OnDisconnectedAsync:** Triggered when a client disconnects. The hub removes the connection from any rooms it had joined. ```csharp using Microsoft.AspNetCore.SignalR; @@ -181,7 +151,6 @@ namespace DiagramServerApplication.Hubs Clients.Caller.SendAsync("OnConnectedAsync", Context.ConnectionId); return base.OnConnectedAsync(); } - public async Task JoinDiagram(string roomName) { try @@ -196,7 +165,6 @@ namespace DiagramServerApplication.Hubs _logger.LogError(ex); } } - public async Task BroadcastToOtherClients(List payloads, string roomName) { try @@ -228,6 +196,10 @@ namespace DiagramServerApplication.Hubs ### Step 2: Register services, Redis backplane, CORS, and map the hub (Program.cs) Add these registrations to your server Program.cs so clients can connect and scale via Redis. Adjust policies/connection strings to your environment. +* Register Redis for shared state and backplane support. +* Configure SignalR with Redis for distributed messaging. +* Add your application services (like RedisService). +* Map the SignalR hub so clients can connect. ```csharp var builder = WebApplication.CreateBuilder(args); @@ -235,20 +207,17 @@ var builder = WebApplication.CreateBuilder(args); // Redis (shared connection) builder.Services.AddSingleton(sp => { - var cs = builder.Configuration.GetConnectionString("RedisConnectionString") - ?? "localhost:6379,abortConnect=false"; + var cs = builder.Configuration.GetConnectionString("RedisConnectionString"); return ConnectionMultiplexer.Connect(cs); }); // SignalR + Redis backplane builder.Services .AddSignalR() - .AddStackExchangeRedis(builder.Configuration.GetConnectionString("RedisConnectionString") - ?? "localhost:6379,abortConnect=false"); + .AddStackExchangeRedis(builder.Configuration.GetConnectionString("RedisConnectionString")); // App services -builder.Services.AddScoped(); -builder.Services.AddScoped(); // your implementation +builder.Services.AddScoped(); // your implementation var app = builder.Build(); @@ -324,7 +293,6 @@ This approach keeps collaborators in sync without locking, while ensuring determ }) .WithAutomaticReconnect() .Build(); - connection.On("OnConnectedAsync", OnConnectedAsync); // Show conflict notification connection.On("ShowConflict", ShowConflict); connection.On("UpdateVersion", UpdateVersion); @@ -370,7 +338,6 @@ This approach keeps collaborators in sync without locking, while ensuring determ // TODO: extract IDs from args (nodes/connectors edited) return new List(); } - } ``` **Server (SignalR Hub) – Validate with Redis and broadcast** @@ -386,11 +353,8 @@ public class DiagramUpdateMessage public class DiagramHub : Hub { - private readonly IDiagramService _diagramService; private readonly IRedisService _redisService; private readonly ILogger _logger; - private readonly IHubContext _diagramHubContext; - private static readonly ConcurrentDictionary _diagramUsers = new(); public DiagramHub(IRedisService redis, ILogger logger) { @@ -445,41 +409,6 @@ public class DiagramHub : Hub _logger.LogError(ex); } } - private async Task> GetUpdatesSinceVersionAsync(long sinceVersion, int maxScan = 200) - { - var historyKey = "diagram_updates_history"; - var length = await _redisService.ListLengthAsync(historyKey); - if (length == 0) return Array.Empty(); - - long start = Math.Max(0, length - maxScan); - long end = length - 1; - - var range = await _redisService.ListRangeAsync(historyKey, start, end); - - var results = new List(range.Length); - foreach (var item in range) - { - if (item.IsNullOrEmpty) continue; - var update = JsonSerializer.Deserialize(item.ToString()); - if (update is not null && update.Version > sinceVersion && update.SourceConnectionId != Context.ConnectionId) - results.Add(update); - } - results.Sort((a, b) => a.Version.CompareTo(b.Version)); - return results; - } - private async Task StoreUpdateInRedis(DiagramUpdateMessage updateMessage) - { - try - { - // Store updates in redis - var historyKey = "diagram_updates_history"; - await _redisService.ListPushAsync(historyKey, updateMessage); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error storing update in Redis for diagram"); - } - } } ``` **Redis service interface & implementation** @@ -488,10 +417,7 @@ using StackExchange.Redis; public interface IRedisService { - Task ListPushAsync(string key, T value); Task<(bool accepted, long version)> CompareAndIncrementAsync(string key, long expectedVersion); - Task ListLengthAsync(string key); - Task ListRangeAsync(string key, long start = 0, long stop = -1); } ``` ```csharp @@ -551,41 +477,6 @@ end _logger.LogError(ex, "Error in CompareAndIncrementAsync for key {Key}", key); } } - public async Task ListPushAsync(string key, T value) - { - try - { - var serializedValue = JsonSerializer.Serialize(value); - return await _database.ListLeftPushAsync(key, serializedValue); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error pushing to list {Key}", key); - } - } - public async Task ListLengthAsync(string key) - { - try - { - return await _database.ListLengthAsync(key); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting list length {Key}", key); - return 0; - } - } - public async Task ListRangeAsync(string key, long start = 0, long stop = -1) - { - try - { - return await _database.ListRangeAsync(key, start, stop); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting list range {Key}", key); - } - } } ``` @@ -640,6 +531,8 @@ To ensure reliable and efficient collaborative editing, consider the following b * The current implementation does not support multiple server instances. SignalR connections and Redis-based versioning are designed for a single-node setup. Scaling out requires configuring a SignalR backplane (e.g., Redis backplane) and ensuring consistent state across nodes. * **Zoom and Pan Not Collaborative** * Zoom and pan actions are local to each client and are not synchronized across users. This means collaborators may view different portions of the diagram independently. +* **Unsupported Diagram Settings** + * Changes to properties such as PageSettings, ContextMenu, and ScrollSettings are not propagated to other users and will only apply locally. The full version of the code can be found in the GitHub location below. From c60e95f37d91b10183458d9c724c50509c6b93b0 Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 14:58:37 +0530 Subject: [PATCH 06/17] 996403: changes added --- .../using-redis-cache-asp-net.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index 5c33295279..a271ee56dd 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -76,6 +76,7 @@ The **RoomName** represents the unique group name for the diagram session, and a }) .WithAutomaticReconnect() .Build(); + // Triggers when connection established to server connection.On("OnConnectedAsync", OnConnectedAsync); await connection.StartAsync(); } @@ -93,9 +94,9 @@ The **RoomName** represents the unique group name for the diagram session, and a ### Step 3: Broadcast Current Editing Changes to Remote Users -To keep all collaborators in sync, changes made on the client-side must be sent to the server, which then broadcasts them to other connected users. This is done by handling the HistoryChanged event of the Blazor Diagram component and using the `GetDiagramUpdates` method to serialize changes into JSON format for transmission. The server-side method BroadcastToOtherClients then sends these updates to all clients in the same SignalR group (room). +To keep all collaborators in sync, changes made on the client-side must be sent to the server, which then broadcasts them to other connected users. This is done by handling the `HistoryChanged` event of the Blazor Diagram component and using the `GetDiagramUpdates` method to serialize changes into JSON format for transmission. The server-side method `BroadcastToOtherClients` then sends these updates to all clients in the same SignalR group (room). -The `HistoryChanged` event is triggered whenever a change occurs in the diagram, such as adding, deleting, or modifying shapes or connectors. The `GetDiagramUpdates` method converts these changes into a JSON format suitable for sending to the server, ensuring updates can be easily applied by other clients. Finally, the `BroadcastToOtherClients` method on the server broadcasts these updates to all users in the same collaborative session. +The `HistoryChanged` event is triggered whenever a change occurs in the diagram, such as adding, deleting, or modifying shapes or connectors. The `GetDiagramUpdates` method converts these changes into a JSON format suitable for sending to the server, ensuring updates can be easily applied by other clients. Finally, the `BroadcastToOtherClients` method on the server broadcasts these updates to other users in the same collaborative session. For grouped interactions (e.g., multiple changes in a single operation), enable `EnableGroupActions` in `DiagramHistoryManager`. This ensures `StartGroupAction` and `EndGroupAction` notifications are included in the `HistoryChanged` event, allowing you to broadcast changes only after the group action completes. @@ -126,10 +127,11 @@ For grouped interactions (e.g., multiple changes in a single operation), enable Create a folder named Hubs and add a file DiagramHub.cs. This hub manages SignalR groups (rooms) per diagram and broadcasts updates to connected clients. -**OnConnectedAsync:** It will trigger when a new client connects. Sends the generated connection ID to the client so it can be used as a session identifier. -**JoinDiagram(roomName):** Adds the current connection to a SignalR group (room) identified by roomName. Also records the mapping so the connection can be removed later. -**BroadcastToOtherClients(payloads, roomName):** Sends edits/updates to other clients in the same room (excludes the sender). -**OnDisconnectedAsync:** Triggered when a client disconnects. The hub removes the connection from any rooms it had joined. +The following key methods are implemented on the server side: +* **OnConnectedAsync:** It will trigger when a new client connects. Sends the generated connection ID to the client so it can be used as a session identifier. +* **JoinDiagram(roomName):** Adds the current connection to a SignalR group (room) identified by roomName. Also records the mapping so the connection can be removed later. +* **BroadcastToOtherClients(payloads, roomName):** Sends updates to other clients in the same room (excludes the sender). +* **OnDisconnectedAsync:** Triggered when a client disconnects. The hub removes the connection from any rooms it had joined. ```csharp using Microsoft.AspNetCore.SignalR; @@ -412,6 +414,9 @@ public class DiagramHub : Hub } ``` **Redis service interface & implementation** +The IRedisService interface defines `CompareAndIncrementAsync(string key, long expectedVersion)`. +This method checks if the current version stored in Redis matches the version we expect. If it matches, it increases the version by 1. +**Purpose:** This is used in collaborative applications to avoid conflicts when multiple users edit the same diagram. It ensures only one update happens at a time. ```csharp using StackExchange.Redis; From 5fcfef974f75753e778ba1950d77b55499e53e4f Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 15:07:50 +0530 Subject: [PATCH 07/17] 996403: changes added --- .../using-redis-cache-asp-net.md | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index a271ee56dd..335aa90188 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -96,7 +96,7 @@ The **RoomName** represents the unique group name for the diagram session, and a To keep all collaborators in sync, changes made on the client-side must be sent to the server, which then broadcasts them to other connected users. This is done by handling the `HistoryChanged` event of the Blazor Diagram component and using the `GetDiagramUpdates` method to serialize changes into JSON format for transmission. The server-side method `BroadcastToOtherClients` then sends these updates to all clients in the same SignalR group (room). -The `HistoryChanged` event is triggered whenever a change occurs in the diagram, such as adding, deleting, or modifying shapes or connectors. The `GetDiagramUpdates` method converts these changes into a JSON format suitable for sending to the server, ensuring updates can be easily applied by other clients. Finally, the `BroadcastToOtherClients` method on the server broadcasts these updates to other users in the same collaborative session. +The `HistoryChanged` event is triggered whenever a change occurs in the diagram, such as adding, deleting, or modifying shapes or connectors. The `GetDiagramUpdates` method converts these changes into a JSON format suitable for sending to the server, ensuring updates can be easily applied by other clients. Finally, the `BroadcastToOtherClients` method on the server broadcasts these updates to other users in the same collaborative session. Each remote user receives the changes through the `ReceiveData` listener and applies them to their diagram using `SetDiagramUpdatesAsync(diagramChanges)`. For grouped interactions (e.g., multiple changes in a single operation), enable `EnableGroupActions` in `DiagramHistoryManager`. This ensures `StartGroupAction` and `EndGroupAction` notifications are included in the `HistoryChanged` event, allowing you to broadcast changes only after the group action completes. @@ -106,6 +106,28 @@ For grouped interactions (e.g., multiple changes in a single operation), enable @code { + private async Task InitializeSignalR() + { + if (connection == null) + { + connection = new HubConnectionBuilder() + .WithUrl(NavigationManager.ToAbsoluteUri("/diagramHub"), options => + { + options.SkipNegotiation = true; + options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets; + }) + .WithAutomaticReconnect() + .Build(); + // Triggers when connection established to server + connection.On("OnConnectedAsync", OnConnectedAsync); + // Apply remote changes to current diagram. + connection.On>("ReceiveData", async (diagramChanges) => + { + await DiagramInstance.SetDiagramUpdatesAsync(diagramChanges); + }); + await connection.StartAsync(); + } + } public async void OnHistoryChange(HistoryChangedEventArgs args) { if (args != null) @@ -158,6 +180,8 @@ namespace DiagramServerApplication.Hubs try { string userId = Context.ConnectionId; + // Store room name in current context. + Context.Items["roomName"] = roomName; // Add to SignalR group await Groups.AddToGroupAsync(userId, roomName); @@ -182,6 +206,8 @@ namespace DiagramServerApplication.Hubs { try { + // Get roomName from context + string roomName = Context.Items["roomName"].ToString(); // Remove from SignalR group await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName); await base.OnDisconnectedAsync(exception); @@ -334,7 +360,6 @@ This approach keeps collaborators in sync without locking, while ensuring determ { // Show a dialog telling the user their change was rejected due to overlap } - private List GetEditedElements(HistoryChangedEventArgs args) { // TODO: extract IDs from args (nodes/connectors edited) From 40cd56592e712b25c1d75991796163cb50034b0f Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 15:11:24 +0530 Subject: [PATCH 08/17] 9906403: comments --- .../diagram/collaborative-editing/using-redis-cache-asp-net.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index 335aa90188..45aa8085b6 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -76,7 +76,7 @@ The **RoomName** represents the unique group name for the diagram session, and a }) .WithAutomaticReconnect() .Build(); - // Triggers when connection established to server + // Triggered when the connection to the server is successfully established connection.On("OnConnectedAsync", OnConnectedAsync); await connection.StartAsync(); } From 26c27affeacca9eefc05d21015ebc937d01e9ac4 Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 15:19:57 +0530 Subject: [PATCH 09/17] 996403: changes added --- .../collaborative-editing/using-redis-cache-asp-net.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index 45aa8085b6..dc11a83292 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -195,6 +195,7 @@ namespace DiagramServerApplication.Hubs { try { + // Broadcast diagram changes to other connected clients in same room. await Clients.OthersInGroup(roomName).SendAsync("ReceiveData", payloads); } catch (Exception ex) @@ -323,6 +324,7 @@ This approach keeps collaborators in sync without locking, while ensuring determ .Build(); // Show conflict notification connection.On("ShowConflict", ShowConflict); + // Update the client version to the latest server version. connection.On("UpdateVersion", UpdateVersion); // Receive remote changes with the new server version connection.On>("ReceiveData", async (diagramChanges, serverVersion) => @@ -439,9 +441,9 @@ public class DiagramHub : Hub } ``` **Redis service interface & implementation** -The IRedisService interface defines `CompareAndIncrementAsync(string key, long expectedVersion)`. +* The IRedisService interface defines `CompareAndIncrementAsync(string key, long expectedVersion)`. This method checks if the current version stored in Redis matches the version we expect. If it matches, it increases the version by 1. -**Purpose:** This is used in collaborative applications to avoid conflicts when multiple users edit the same diagram. It ensures only one update happens at a time. +**Purpose:** This is used in collaborative applications to avoid conflicts when multiple users edit the same element. It ensures only one update happens at a time. ```csharp using StackExchange.Redis; From ebd20d92964256acbbf574b9ebbd4b73a4a813ee Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 15:41:15 +0530 Subject: [PATCH 10/17] 996403: changes added --- .../using-redis-cache-asp-net.md | 121 +++++++++--------- 1 file changed, 60 insertions(+), 61 deletions(-) diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index dc11a83292..35edc942b8 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -48,7 +48,7 @@ Redis imposes limits on concurrent connections. Select an appropriate Redis conf ### Step 1: Configure SignalR Connection To enable real-time collaboration, you need to establish a SignalR connection that can send and receive diagram updates. This connection will allow the client to join a SignalR group (room) for collaborative editing, ensuring changes are shared only among users working on the same diagram. -The **RoomName** represents the unique group name for the diagram session, and all users editing the same diagram should join this group to share updates within that session. The **OnConnectedAsync** method is triggered after the client successfully connects to the server and receives a unique connection ID, confirming the connection. After that, the **JoinDiagram** method is called to add the client to the specified SignalR group, enabling the client to send and receive real-time updates with other users in the same room. +The `RoomName` represents the unique group name for the diagram session, and all users editing the same diagram should join this group to share updates within that session. The `OnConnectedAsync` method is triggered after the client successfully connects to the server and receives a unique connection ID, confirming the connection. After that, the `JoinDiagram` method is called to add the client to the specified SignalR group, enabling the client to send and receive real-time updates with other users in the same room. ```csharp @using Microsoft.AspNetCore.SignalR.Client @@ -144,9 +144,63 @@ For grouped interactions (e.g., multiple changes in a single operation), enable ``` ## Server configuration +### Step 1: Register services, Redis backplane, CORS, and map the hub (Program.cs) -### Step 1: Configure SignalR Hub to Create Rooms for Collaborative Editing Sessions +Add these registrations to your server Program.cs so clients can connect and scale via Redis. Adjust policies/connection strings to your environment. +* Register Redis for shared state and backplane support. +* Configure SignalR with Redis for distributed messaging. +* Add your application services (like RedisService). +* Map the SignalR hub so clients can connect. + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Redis (shared connection) +builder.Services.AddSingleton(sp => +{ + var cs = builder.Configuration.GetConnectionString("RedisConnectionString"); + return ConnectionMultiplexer.Connect(cs); +}); + +// SignalR + Redis backplane +builder.Services + .AddSignalR() + .AddStackExchangeRedis(builder.Configuration.GetConnectionString("RedisConnectionString")); + +// App services +builder.Services.AddScoped(); // your implementation + +var app = builder.Build(); + +app.MapHub("/diagramHub"); +app.Run(); +``` + +Notes: +- Ensure WebSockets are enabled on the host/proxy, or remove SkipNegotiation on the client to allow fallback transports. +- Use a singleton IConnectionMultiplexer to respect Redis connection limits. + +### Step 2: Configure Redis Cache Connection String at the Application Level +To enable collaborative editing with real-time synchronization, configure Redis as the temporary data store. Redis ensures fast and reliable communication between multiple server instances when scaling the application. + +Add your Redis connection string in the `appsettings.json` file: +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "RedisConnectionString": "<>" + } +} +``` + +### Step 3: Configure SignalR Hub to Create Rooms for Collaborative Editing Sessions Create a folder named Hubs and add a file DiagramHub.cs. This hub manages SignalR groups (rooms) per diagram and broadcasts updates to connected clients. The following key methods are implemented on the server side: @@ -221,63 +275,6 @@ namespace DiagramServerApplication.Hubs } } ``` - -### Step 2: Register services, Redis backplane, CORS, and map the hub (Program.cs) - -Add these registrations to your server Program.cs so clients can connect and scale via Redis. Adjust policies/connection strings to your environment. -* Register Redis for shared state and backplane support. -* Configure SignalR with Redis for distributed messaging. -* Add your application services (like RedisService). -* Map the SignalR hub so clients can connect. - -```csharp -var builder = WebApplication.CreateBuilder(args); - -// Redis (shared connection) -builder.Services.AddSingleton(sp => -{ - var cs = builder.Configuration.GetConnectionString("RedisConnectionString"); - return ConnectionMultiplexer.Connect(cs); -}); - -// SignalR + Redis backplane -builder.Services - .AddSignalR() - .AddStackExchangeRedis(builder.Configuration.GetConnectionString("RedisConnectionString")); - -// App services -builder.Services.AddScoped(); // your implementation - -var app = builder.Build(); - -app.MapHub("/diagramHub"); - -app.Run(); -``` - -Notes: -- Ensure WebSockets are enabled on the host/proxy, or remove SkipNegotiation on the client to allow fallback transports. -- Use a singleton IConnectionMultiplexer to respect Redis connection limits. - -### Step 3: Configure Redis Cache Connection String at the Application Level -To enable collaborative editing with real-time synchronization, configure Redis as the temporary data store. Redis ensures fast and reliable communication between multiple server instances when scaling the application. - -Add your Redis connection string in the `appsettings.json` file: -```json -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*", - "ConnectionStrings": { - "RedisConnectionString": "<>" - } -} -``` - ## Conflict policy (optimistic concurrency) To handle conflicts during collaborative editing, we use an optimistic concurrency strategy with versioning: @@ -443,7 +440,7 @@ public class DiagramHub : Hub **Redis service interface & implementation** * The IRedisService interface defines `CompareAndIncrementAsync(string key, long expectedVersion)`. This method checks if the current version stored in Redis matches the version we expect. If it matches, it increases the version by 1. -**Purpose:** This is used in collaborative applications to avoid conflicts when multiple users edit the same element. It ensures only one update happens at a time. +* **Purpose:** This is used in collaborative applications to avoid conflicts when multiple users edit the same element. It ensures only one update happens at a time. ```csharp using StackExchange.Redis; @@ -564,8 +561,10 @@ To ensure reliable and efficient collaborative editing, consider the following b * **Zoom and Pan Not Collaborative** * Zoom and pan actions are local to each client and are not synchronized across users. This means collaborators may view different portions of the diagram independently. * **Unsupported Diagram Settings** - * Changes to properties such as PageSettings, ContextMenu, and ScrollSettings are not propagated to other users and will only apply locally. + * Changes to properties such as PageSettings, ContextMenu, DiagramHistoryManager, UmlSequenceDiagram, Layout, and ScrollSettings are not propagated to other users and will only apply locally. +>**Note:** +> * Collaboration is currently supported only for actions that trigger the HistoryChanged event. The full version of the code can be found in the GitHub location below. GitHub Example: [Collaborative editing examples](https://github.com/syncfusion/blazor-showcase-diagram-collaborative-editing). From 668ecf1e9dae3ff35bbaee93517290bd6ad5589f Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 17:25:40 +0530 Subject: [PATCH 11/17] 996403: changes added --- .../diagram/collaborative-editing/using-redis-cache-asp-net.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index 35edc942b8..8b9d5db3d1 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -561,10 +561,11 @@ To ensure reliable and efficient collaborative editing, consider the following b * **Zoom and Pan Not Collaborative** * Zoom and pan actions are local to each client and are not synchronized across users. This means collaborators may view different portions of the diagram independently. * **Unsupported Diagram Settings** - * Changes to properties such as PageSettings, ContextMenu, DiagramHistoryManager, UmlSequenceDiagram, Layout, and ScrollSettings are not propagated to other users and will only apply locally. + * Changes to properties such as PageSettings, ContextMenu, DiagramHistoryManager, SnapSettings, Rulers, UmlSequenceDiagram, Layout, and ScrollSettings are not propagated to other users and will only apply locally. >**Note:** > * Collaboration is currently supported only for actions that trigger the HistoryChanged event. + The full version of the code can be found in the GitHub location below. GitHub Example: [Collaborative editing examples](https://github.com/syncfusion/blazor-showcase-diagram-collaborative-editing). From 0f6df5135336825b0bb48bcc1e5ee0f4a2fc2106 Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 17:40:42 +0530 Subject: [PATCH 12/17] 996403: moved section --- .../using-redis-cache-asp-net.md | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index 8b9d5db3d1..bcbe2fe5db 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -19,16 +19,6 @@ Following things are needed to enable collaborative editing in Diagram Component * SignalR * Redis -## NuGet packages required - -- Client (Blazor): - - Microsoft.AspNetCore.SignalR.Client - - Syncfusion.Blazor.Diagram -- Server: - - Microsoft.AspNetCore.SignalR - - Microsoft.AspNetCore.SignalR.StackExchangeRedis - - StackExchange.Redis - ## SignalR In collaborative editing, real-time communication is essential for users to see each other’s changes instantly. We use a real-time transport protocol to efficiently send and receive data as edits occur. For this, we utilize SignalR, which supports real-time data exchange between the client and server. SignalR ensures that updates are transmitted immediately, allowing seamless collaboration by handling the complexities of connection management and offering reliable communication channels. @@ -43,8 +33,17 @@ All diagram editing operations performed during collaboration are cached in Redi Redis imposes limits on concurrent connections. Select an appropriate Redis configuration based on your expected user load to maintain optimal performance and avoid connection bottlenecks. -## How to enable collaborative editing in client side +## NuGet packages required + +- Client (Blazor): + - Microsoft.AspNetCore.SignalR.Client + - Syncfusion.Blazor.Diagram +- Server: + - Microsoft.AspNetCore.SignalR + - Microsoft.AspNetCore.SignalR.StackExchangeRedis + - StackExchange.Redis +## How to enable collaborative editing in client side ### Step 1: Configure SignalR Connection To enable real-time collaboration, you need to establish a SignalR connection that can send and receive diagram updates. This connection will allow the client to join a SignalR group (room) for collaborative editing, ensuring changes are shared only among users working on the same diagram. From 0312e18ddbc4a1840a2263cafc8c801582df76d8 Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 18:13:40 +0530 Subject: [PATCH 13/17] 996403: naming changes --- .../diagram/collaborative-editing/overview.md | 4 +-- .../using-redis-cache-asp-net.md | 30 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/blazor/diagram/collaborative-editing/overview.md b/blazor/diagram/collaborative-editing/overview.md index e0de3298bd..7bf79e8aa8 100644 --- a/blazor/diagram/collaborative-editing/overview.md +++ b/blazor/diagram/collaborative-editing/overview.md @@ -13,12 +13,12 @@ Collaborative editing enables multiple users to work on the same diagram at the ## Prerequisites -- *Real-time Transport Protocol*: Enables instant communication between clients and the server, ensuring that updates during collaborative editing are transmitted and reflected immediately. +- *Real-time Transport Protocol*: Enables instant communication between blazor server and the collaboration hub, ensuring that updates during collaborative editing are transmitted and reflected immediately. - *Distributed Cache or Database*: Serves as temporary storage for the queue of diagram editing operations, helping maintain synchronization and consistency across multiple users. ### Real time transport protocol -- *Managing Connections*: Maintains active connections between clients and the server to enable uninterrupted real-time collaboration. This ensures smooth and consistent communication throughout the editing session. +- *Managing Connections*: Maintains active connections between blazor server and the collaboration hub to enable uninterrupted real-time collaboration. This ensures smooth and consistent communication throughout the editing session. - *Broadcasting Changes*: Instantly propagates any edits made by one user to all other collaborators. This guarantees that everyone is always working on the most up-to-date version of the diagram, fostering accuracy and teamwork. ### Distributed cache or database diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index bcbe2fe5db..eaf1dc6ac9 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -21,7 +21,7 @@ Following things are needed to enable collaborative editing in Diagram Component ## SignalR -In collaborative editing, real-time communication is essential for users to see each other’s changes instantly. We use a real-time transport protocol to efficiently send and receive data as edits occur. For this, we utilize SignalR, which supports real-time data exchange between the client and server. SignalR ensures that updates are transmitted immediately, allowing seamless collaboration by handling the complexities of connection management and offering reliable communication channels. +In collaborative editing, real-time communication is essential for users to see each other’s changes instantly. We use a real-time transport protocol to efficiently send and receive data as edits occur. For this, we utilize SignalR, which supports real-time data exchange between the blazor server and collaboration hub. SignalR ensures that updates are transmitted immediately, allowing seamless collaboration by handling the complexities of connection management and offering reliable communication channels. To make SignalR work in a distributed environment (with more than one server instance), it needs to be configured with either AspNetCore SignalR Service or a Redis backplane. @@ -35,15 +35,15 @@ Redis imposes limits on concurrent connections. Select an appropriate Redis conf ## NuGet packages required -- Client (Blazor): +- Blazor Server: - Microsoft.AspNetCore.SignalR.Client - Syncfusion.Blazor.Diagram -- Server: +- Collaboration Hub: - Microsoft.AspNetCore.SignalR - Microsoft.AspNetCore.SignalR.StackExchangeRedis - StackExchange.Redis -## How to enable collaborative editing in client side +## How to enable collaborative editing in Blazor server side ### Step 1: Configure SignalR Connection To enable real-time collaboration, you need to establish a SignalR connection that can send and receive diagram updates. This connection will allow the client to join a SignalR group (room) for collaborative editing, ensuring changes are shared only among users working on the same diagram. @@ -93,9 +93,9 @@ The `RoomName` represents the unique group name for the diagram session, and all ### Step 3: Broadcast Current Editing Changes to Remote Users -To keep all collaborators in sync, changes made on the client-side must be sent to the server, which then broadcasts them to other connected users. This is done by handling the `HistoryChanged` event of the Blazor Diagram component and using the `GetDiagramUpdates` method to serialize changes into JSON format for transmission. The server-side method `BroadcastToOtherClients` then sends these updates to all clients in the same SignalR group (room). +To keep all collaborators in sync, changes made on the blazor server side must be sent to the collaboration hub, which then broadcasts them to other connected users. This is done by handling the `HistoryChanged` event of the Blazor Diagram component and using the `GetDiagramUpdates` method to serialize changes into JSON format for transmission. The collaboration hub method `BroadcastToOtherClients` then sends these updates to all clients in the same SignalR group (room). -The `HistoryChanged` event is triggered whenever a change occurs in the diagram, such as adding, deleting, or modifying shapes or connectors. The `GetDiagramUpdates` method converts these changes into a JSON format suitable for sending to the server, ensuring updates can be easily applied by other clients. Finally, the `BroadcastToOtherClients` method on the server broadcasts these updates to other users in the same collaborative session. Each remote user receives the changes through the `ReceiveData` listener and applies them to their diagram using `SetDiagramUpdatesAsync(diagramChanges)`. +The `HistoryChanged` event is triggered whenever a change occurs in the diagram, such as adding, deleting, or modifying shapes or connectors. The `GetDiagramUpdates` method converts these changes into a JSON format suitable for sending to the collaboration hub, ensuring updates can be easily applied by other clients. Finally, the `BroadcastToOtherClients` method on the collaboration hub broadcasts these updates to other users in the same collaborative session. Each remote user receives the changes through the `ReceiveData` listener and applies them to their diagram using `SetDiagramUpdatesAsync(diagramChanges)`. For grouped interactions (e.g., multiple changes in a single operation), enable `EnableGroupActions` in `DiagramHistoryManager`. This ensures `StartGroupAction` and `EndGroupAction` notifications are included in the `HistoryChanged` event, allowing you to broadcast changes only after the group action completes. @@ -142,10 +142,10 @@ For grouped interactions (e.g., multiple changes in a single operation), enable } ``` -## Server configuration +## Collaboration Hub configuration ### Step 1: Register services, Redis backplane, CORS, and map the hub (Program.cs) -Add these registrations to your server Program.cs so clients can connect and scale via Redis. Adjust policies/connection strings to your environment. +Add these registrations to your collaboration hub Program.cs so clients can connect and scale via Redis. Adjust policies/connection strings to your environment. * Register Redis for shared state and backplane support. * Configure SignalR with Redis for distributed messaging. * Add your application services (like RedisService). @@ -202,7 +202,7 @@ Add your Redis connection string in the `appsettings.json` file: ### Step 3: Configure SignalR Hub to Create Rooms for Collaborative Editing Sessions Create a folder named Hubs and add a file DiagramHub.cs. This hub manages SignalR groups (rooms) per diagram and broadcasts updates to connected clients. -The following key methods are implemented on the server side: +The following key methods are implemented on the collaboration hub: * **OnConnectedAsync:** It will trigger when a new client connects. Sends the generated connection ID to the client so it can be used as a session identifier. * **JoinDiagram(roomName):** Adds the current connection to a SignalR group (room) identified by roomName. Also records the mapping so the connection can be removed later. * **BroadcastToOtherClients(payloads, roomName):** Sends updates to other clients in the same room (excludes the sender). @@ -279,12 +279,12 @@ namespace DiagramServerApplication.Hubs To handle conflicts during collaborative editing, we use an optimistic concurrency strategy with versioning: * **Versioning**: Each update carries the client’s clientVersion and the list of editedElementIds. -* **Client Update:** The client sends serialized diagram changes, clientVersion, and editedElementIds to the server. +* **Client Update:** The client sends serialized diagram changes, clientVersion, and editedElementIds to the collaboration hub. -* **Server Validation:** The server compares the incoming clientVersion with the latest version stored in Redis. +* **Collaboration Hub Validation:** The collaboration hub compares the incoming clientVersion with the latest version stored in Redis. * **If stale and overlapping elements exist:** reject the update, instruct the client to revert, and show a conflict notice. * **If stale but no overlap:** accept the update and increment the version atomically. -* **Client Synchronization:** After acceptance, the client must update its local clientVersion to the server version. +* **Client Synchronization:** After acceptance, the client must update its local clientVersion to the server(collaboration hub) version. This approach keeps collaborators in sync without locking, while ensuring deterministic conflict handling. @@ -365,7 +365,7 @@ This approach keeps collaborators in sync without locking, while ensuring determ } } ``` -**Server (SignalR Hub) – Validate with Redis and broadcast** +**Collaboration Hub – Validate with Redis and broadcast** ```csharp using Microsoft.AspNetCore.SignalR; @@ -549,9 +549,9 @@ To ensure reliable and efficient collaborative editing, consider the following b * Configure keep-alives to maintain long-lived connections and prevent timeouts during idle periods. * **2. Transport** * **Preferred:** WebSockets for low-latency, full-duplex communication. - * **Fallback:** If WebSockets are unavailable, remove SkipNegotiation on the client to allow SignalR to fall back to Server-Sent Events (SSE) or Long Polling. + * **Fallback:** If WebSockets are unavailable, remove SkipNegotiation on the Blazor server to allow SignalR to fall back to Server-Sent Events (SSE) or Long Polling. * **3. Serialization** - * For large payloads, enable MessagePack on both server and client for efficient binary serialization. + * For large payloads, enable MessagePack on both collaboration hub and blazor server projects for efficient binary serialization. * Consider sending diffs (incremental changes) instead of full diagram state to reduce bandwidth usage. ## Limitations From 91ef7fa460ffacc2c560f10c7ecfe5ee6a48e6cb Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 18:20:27 +0530 Subject: [PATCH 14/17] 996403: naming changes --- .../collaborative-editing/using-redis-cache-asp-net.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index eaf1dc6ac9..46e7b9d256 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -279,16 +279,16 @@ namespace DiagramServerApplication.Hubs To handle conflicts during collaborative editing, we use an optimistic concurrency strategy with versioning: * **Versioning**: Each update carries the client’s clientVersion and the list of editedElementIds. -* **Client Update:** The client sends serialized diagram changes, clientVersion, and editedElementIds to the collaboration hub. +* **Blazor Server Update:** The blazor server sends serialized diagram changes, clientVersion, and editedElementIds to the collaboration hub. * **Collaboration Hub Validation:** The collaboration hub compares the incoming clientVersion with the latest version stored in Redis. * **If stale and overlapping elements exist:** reject the update, instruct the client to revert, and show a conflict notice. * **If stale but no overlap:** accept the update and increment the version atomically. -* **Client Synchronization:** After acceptance, the client must update its local clientVersion to the server(collaboration hub) version. +* **Blazor server Synchronization:** After acceptance, the blazor server must update its local clientVersion to the server(collaboration hub) version. This approach keeps collaborators in sync without locking, while ensuring deterministic conflict handling. -**Client (Blazor) – Send updates & apply remote changes** +**Blazor server – Send updates & apply remote changes** ```razor @using Microsoft.AspNetCore.SignalR.Client From bb1cb51d440140ecd34dff9ea3dd915fad6e9e93 Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 18:36:57 +0530 Subject: [PATCH 15/17] 996403: changes added --- .../diagram/collaborative-editing/overview.md | 43 +++++++------- .../using-redis-cache-asp-net.md | 58 ++++--------------- 2 files changed, 31 insertions(+), 70 deletions(-) diff --git a/blazor/diagram/collaborative-editing/overview.md b/blazor/diagram/collaborative-editing/overview.md index 7bf79e8aa8..160bf26307 100644 --- a/blazor/diagram/collaborative-editing/overview.md +++ b/blazor/diagram/collaborative-editing/overview.md @@ -8,37 +8,34 @@ documentation: ug --- # Collaborative Editing in Blazor Diagram - -Collaborative editing enables multiple users to work on the same diagram at the same time. Changes are reflected in real-time, allowing all participants to instantly see updates as they happen. This feature promotes seamless teamwork by eliminating the need to wait for others to finish their edits. As a result, teams can boost productivity, streamline workflows, and ensure everyone stays aligned throughout the design process. +Collaborative editing allows multiple users to work on the same diagram simultaneously. All changes are synchronized in real-time, ensuring that collaborators can instantly see updates as they occur. This feature enhances productivity by removing the need to wait for others to finish their edits and promotes seamless teamwork. +By leveraging Redis as the real-time data store, the application ensures fast and reliable communication between clients, making collaborative diagram editing smooth and efficient. ## Prerequisites +Following things are needed to enable collaborative editing in Diagram Component -- *Real-time Transport Protocol*: Enables instant communication between blazor server and the collaboration hub, ensuring that updates during collaborative editing are transmitted and reflected immediately. -- *Distributed Cache or Database*: Serves as temporary storage for the queue of diagram editing operations, helping maintain synchronization and consistency across multiple users. - -### Real time transport protocol +* SignalR +* Redis -- *Managing Connections*: Maintains active connections between blazor server and the collaboration hub to enable uninterrupted real-time collaboration. This ensures smooth and consistent communication throughout the editing session. -- *Broadcasting Changes*: Instantly propagates any edits made by one user to all other collaborators. This guarantees that everyone is always working on the most up-to-date version of the diagram, fostering accuracy and teamwork. +## NuGet packages required -### Distributed cache or database +- Blazor Server: + - Microsoft.AspNetCore.SignalR.Client + - Syncfusion.Blazor.Diagram +- Collaboration Hub: + - Microsoft.AspNetCore.SignalR + - Microsoft.AspNetCore.SignalR.StackExchangeRedis + - StackExchange.Redis -Collaborative editing requires a reliable backing system to temporarily store and manage editing operations from all active users. This ensures real-time synchronization and conflict resolution across multiple clients. There are two primary options: +## SignalR +In collaborative editing, real-time communication is essential for users to see each other’s changes instantly. We use a real-time transport protocol to efficiently send and receive data as edits occur. For this, we utilize SignalR, which supports real-time data exchange between the blazor server and collaboration hub. SignalR ensures that updates are transmitted immediately, allowing seamless collaboration by handling the complexities of connection management and offering reliable communication channels. -- *Distributed Cache*: - * Designed for high throughput and low latency. - * Handles significantly more HTTP requests per second compared to a database. - * Example: A server with 2 vCPUs and 8 GB RAM can process up to 125 requests per second using a distributed cache. +To make SignalR work in a distributed environment (with more than one server instance), it needs to be configured with either AspNetCore SignalR Service or a Redis backplane. -- *Database*: - * Suitable for smaller-scale collaboration scenarios. - * With the same server configuration, a database can handle approximately 50 requests per second. +## Redis +Redis is used as a temporary data store to manage real-time diagram collaborative editing operations. It helps queue editing actions and resolve conflicts through versioning mechanisms. -> *Recommendation*: - * If your application expects 50 or fewer requests per second, a database provides a reliable solution for managing the operation queue. - * If your application expects more than 50 requests per second, a distributed cache is highly recommended for optimal performance. +All diagram editing operations performed during collaboration are cached in Redis. To prevent excessive memory usage, old versioning data is periodically removed from the Redis cache. -> Tips to calculate the average requests per second of your application: -Assume the editor in your live application is actively used by 1000 users and each user's edit can trigger 2 to 5 requests per second. The total requests per second of your applications will be around 2000 to 5000. In this case, you can finalize a configuration to support around 5000 average requests per second. +Redis imposes limits on concurrent connections. Select an appropriate Redis configuration based on your expected user load to maintain optimal performance and avoid connection bottlenecks. -> Note: The metrics provided are for illustration purposes only. Actual throughput may vary based on additional server-side operations. It is strongly recommended to monitor your application’s traffic and performance and select a configuration that best meets your real-world requirements. diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index 46e7b9d256..acc66f6696 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -7,42 +7,6 @@ control: Diagram documentation: ug --- -# Collaborative Editing with Redis in Blazor Diagram - -Collaborative editing allows multiple users to work on the same diagram simultaneously. All changes are synchronized in real-time, ensuring that collaborators can instantly see updates as they occur. This feature enhances productivity by removing the need to wait for others to finish their edits and promotes seamless teamwork. -By leveraging Redis as the real-time data store, the application ensures fast and reliable communication between clients, making collaborative diagram editing smooth and efficient. - -## Prerequisites - -Following things are needed to enable collaborative editing in Diagram Component - -* SignalR -* Redis - -## SignalR - -In collaborative editing, real-time communication is essential for users to see each other’s changes instantly. We use a real-time transport protocol to efficiently send and receive data as edits occur. For this, we utilize SignalR, which supports real-time data exchange between the blazor server and collaboration hub. SignalR ensures that updates are transmitted immediately, allowing seamless collaboration by handling the complexities of connection management and offering reliable communication channels. - -To make SignalR work in a distributed environment (with more than one server instance), it needs to be configured with either AspNetCore SignalR Service or a Redis backplane. - -## Redis - -Redis is used as a temporary data store to manage real-time diagram collaborative editing operations. It helps queue editing actions and resolve conflicts through versioning mechanisms. - -All diagram editing operations performed during collaboration are cached in Redis. To prevent excessive memory usage, old versioning data is periodically removed from the Redis cache. - -Redis imposes limits on concurrent connections. Select an appropriate Redis configuration based on your expected user load to maintain optimal performance and avoid connection bottlenecks. - -## NuGet packages required - -- Blazor Server: - - Microsoft.AspNetCore.SignalR.Client - - Syncfusion.Blazor.Diagram -- Collaboration Hub: - - Microsoft.AspNetCore.SignalR - - Microsoft.AspNetCore.SignalR.StackExchangeRedis - - StackExchange.Redis - ## How to enable collaborative editing in Blazor server side ### Step 1: Configure SignalR Connection To enable real-time collaboration, you need to establish a SignalR connection that can send and receive diagram updates. This connection will allow the client to join a SignalR group (room) for collaborative editing, ensuring changes are shared only among users working on the same diagram. @@ -390,23 +354,23 @@ public class DiagramHub : Hub { try { - var versionKey = "diagram:version"; + string versionKey = "diagram:version"; // Try to accept based on expected version (CAS via Lua) - var (accepted, serverVersion) = await _redisService.CompareAndIncrementAsync(versionKey, clientVersion); + (bool accepted, long serverVersion) = await _redisService.CompareAndIncrementAsync(versionKey, clientVersion); if (!accepted) { // Check for overlaps since client's version - var recentUpdates = await GetUpdatesSinceVersionAsync(clientVersion, maxScan: 200); - var recentlyTouched = new HashSet(StringComparer.Ordinal); - foreach (var upd in recentUpdates) + List recentUpdates = await GetUpdatesSinceVersionAsync(clientVersion, maxScan: 200); + HashSet recentlyTouched = new HashSet(StringComparer.Ordinal); + foreach (DiagramUpdateMessage upd in recentUpdates) { if (upd.ModifiedElementIds == null) continue; - foreach (var id in upd.ModifiedElementIds) + foreach (string id in upd.ModifiedElementIds) recentlyTouched.Add(id); } - var overlaps = elementIds?.Where(id => recentlyTouched.Contains(id)).Distinct().ToList(); + List overlaps = elementIds?.Where(id => recentlyTouched.Contains(id)).Distinct().ToList(); if (overlaps?.Count > 0) { // Reject & notify caller of conflict @@ -415,11 +379,11 @@ public class DiagramHub : Hub } // Accept non-overlapping stale update: increment once more - var (_, newServerVersion) = await _redisService.CompareAndIncrementAsync(versionKey, serverVersion); + (bool _, long newServerVersion) = await _redisService.CompareAndIncrementAsync(versionKey, serverVersion); serverVersion = newServerVersion; } // Store update in Redis history - var update = new DiagramUpdateMessage + DiagramUpdateMessage update = new DiagramUpdateMessage { SourceConnectionId = connId, Version = serverVersion, @@ -485,7 +449,7 @@ end "; try { - var result = (StackExchange.Redis.RedisResult[])await _database.ScriptEvaluateAsync( + RedisResult[] result = (StackExchange.Redis.RedisResult[])await _database.ScriptEvaluateAsync( lua, keys: new StackExchange.Redis.RedisKey[] { key }, values: new StackExchange.Redis.RedisValue[] { expectedVersion.ToString() }); @@ -525,7 +489,7 @@ public async Task SetAsync(string key, T value, TimeSpan? expiry = null { try { - var serializedValue = JsonSerializer.Serialize(value); + string serializedValue = JsonSerializer.Serialize(value); return await _database.StringSetAsync(key, serializedValue, expiry); } catch (Exception ex) From 36b051b2f97a2083abe5dad0c16b73ad9884d913 Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 19:17:36 +0530 Subject: [PATCH 16/17] 996403: naming --- .../diagram/collaborative-editing/using-redis-cache-asp-net.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index acc66f6696..385a69e300 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -11,7 +11,7 @@ documentation: ug ### Step 1: Configure SignalR Connection To enable real-time collaboration, you need to establish a SignalR connection that can send and receive diagram updates. This connection will allow the client to join a SignalR group (room) for collaborative editing, ensuring changes are shared only among users working on the same diagram. -The `RoomName` represents the unique group name for the diagram session, and all users editing the same diagram should join this group to share updates within that session. The `OnConnectedAsync` method is triggered after the client successfully connects to the server and receives a unique connection ID, confirming the connection. After that, the `JoinDiagram` method is called to add the client to the specified SignalR group, enabling the client to send and receive real-time updates with other users in the same room. +The `RoomName` represents the unique group name for the diagram session, and all users editing the same diagram should join this group to share updates within that session. The `OnConnectedAsync` method is triggered after the client successfully connects to the collaboration hub and receives a unique connection ID, confirming the connection. After that, the `JoinDiagram` method is called to add the client to the specified SignalR group, enabling the client to send and receive real-time updates with other users in the same room. ```csharp @using Microsoft.AspNetCore.SignalR.Client From 0a23dba03193e6152fc899af1e8a805b1ff6377c Mon Sep 17 00:00:00 2001 From: issacimmanuvel-git Date: Thu, 18 Dec 2025 19:23:41 +0530 Subject: [PATCH 17/17] 996403: comments --- .../using-redis-cache-asp-net.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md index 385a69e300..7b3f6c32c1 100644 --- a/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md +++ b/blazor/diagram/collaborative-editing/using-redis-cache-asp-net.md @@ -39,7 +39,7 @@ The `RoomName` represents the unique group name for the diagram session, and all }) .WithAutomaticReconnect() .Build(); - // Triggered when the connection to the server is successfully established + // Triggered when the connection to the collaboration hub is successfully established connection.On("OnConnectedAsync", OnConnectedAsync); await connection.StartAsync(); } @@ -81,7 +81,7 @@ For grouped interactions (e.g., multiple changes in a single operation), enable }) .WithAutomaticReconnect() .Build(); - // Triggers when connection established to server + // Triggered when the connection to the collaboration hub is successfully established connection.On("OnConnectedAsync", OnConnectedAsync); // Apply remote changes to current diagram. connection.On>("ReceiveData", async (diagramChanges) => @@ -106,7 +106,7 @@ For grouped interactions (e.g., multiple changes in a single operation), enable } ``` -## Collaboration Hub configuration +## Collaboration Hub Configuration ### Step 1: Register services, Redis backplane, CORS, and map the hub (Program.cs) Add these registrations to your collaboration hub Program.cs so clients can connect and scale via Redis. Adjust policies/connection strings to your environment. @@ -238,7 +238,7 @@ namespace DiagramServerApplication.Hubs } } ``` -## Conflict policy (optimistic concurrency) +## Conflict Policy (Optimistic Concurrency) To handle conflicts during collaborative editing, we use an optimistic concurrency strategy with versioning: @@ -400,7 +400,7 @@ public class DiagramHub : Hub } } ``` -**Redis service interface & implementation** +**Redis Service Interface & Implementation** * The IRedisService interface defines `CompareAndIncrementAsync(string key, long expectedVersion)`. This method checks if the current version stored in Redis matches the version we expect. If it matches, it increases the version by 1. * **Purpose:** This is used in collaborative applications to avoid conflicts when multiple users edit the same element. It ensures only one update happens at a time. @@ -472,7 +472,7 @@ end } ``` -## Cleanup strategy for Redis +## Cleanup Strategy for Redis To prevent unbounded memory growth and maintain optimal performance, implement one or both of the following strategies: * **Keep Only the Last K Versions** @@ -505,7 +505,7 @@ long version = 5; await _redisService.SetAsync(versionKey, version, TimeSpan.FromHours(1)); ``` -## Hosting, transport, and serialization +## Hosting, Transport, and Serialization To ensure reliable and efficient collaborative editing, consider the following best practices: * **1. Hosting**