Collaborative Editing in Blazor Diagram Using SignalR and Redis | Syncfusion Blogs
Loader
Collaborative Editing in Blazor Diagram Using SignalR and Redis

Summarize this blog post with:

TL;DR: Implement collaborative editing in Blazor Diagram by wiring an ASP.NET Core SignalR hub with a Redis backplane. Send delta payloads from the HistoryChanged event, apply updates via SetDiagramUpdatesAsync, and track userVersion/serverVersion for optimistic concurrency. Detect overlapping edits and reject stale updates; scale out with Redis groups for multi-instance deployments.

In today’s fast-moving digital landscape, effective teamwork is key. For visual projects like diagramming, live collaboration isn’t just a bonus; it’s essential. Think about teams simultaneously sketching flowcharts, designing UI mockups, or planning complex systems. This kind of real-time co-creation boosts efficiency and sparks innovation.

With the Essential Studio® 2025 Volume 4 release, collaborative editing is now available in the Blazor Diagram component, making online diagram collaboration a true game-changer.

In this guide, you will learn how to build a powerful ASP.NET Core application that enables seamless real-time collaborative editing in Syncfusion Blazor Diagram. We’ll explore how SignalR provides the instant communication needed for live updates, and how Redis helps maintain efficient performance, even as the number of users increases.

Setting up real-time collaboration in ASP.NET Core

To enable live, collaborative diagram editing, the core setup involves configuring an ASP.NET Core SignalR hub, powered by Redis.

Let’s begin by setting up our project.

Step 1: Project initialization and NuGet packages

Start by creating an ASP.NET Core Web App using the Razor Pages template in Visual Studio via the Microsoft Templates Guide.

To add the collaborative editing features and integrate Redis, you’ll need to install the following NuGet packages.

  • Microsoft.AspNetCore.SignalR: Enables real-time communication between server and clients, making collaborative editing seamless by instantly syncing changes across users.
  • StackExchange.Redis: This library provides direct access to Redis functionalities.

Step 2: Configure SignalR and Redis

Open the appsettings.json file and add your Redis connection string under the ConnectionStrings section. Replace <<Your Redis connection string>> with the actual connection details to your Redis server.

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    },
    "AllowedHosts": "*",
    "ConnectionStrings": {
        "RedisConnectionString": "<<Your Redis connection string>>"
    }
}

Now, you should modify the Program.cs file to set up SignalR and Redis.

1. Register Redis connection (Singleton)

Register a single, shared connection to Redis (IConnectionMultiplexer) that the entire application can reuse, as shown in the code example below.

using StackExchange.Redis;

// Redis connection
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
{
    string connectionString = builder.Configuration.GetConnectionString("RedisConnectionString");
    return ConnectionMultiplexer.Connect(connectionString);
});

2. Configure SignalR services

The AddSignalR() enables the SignalR service.

// SignalR configuration
builder.Services.AddSignalR();

3. Map SignalR hub endpoint

Finally, you need to define a URL endpoint where the Blazor app can connect to your SignalR hub, as shown in the code example below.

// Map the hub connection
app.MapHub<DiagramHub>("/diagramHub");

Step 3: Implement the SignalR hub

Next, create DiagramHub.cs file in a Hubs folder and implement the following key methods within it:

  • OnConnectedAsync: This method is automatically called every time a new client connects to your hub.
  • JoinDiagram: When a user wants to collaborate on a specific diagram, they “join” a group within the hub. This group is uniquely identified by the diagramId. Messages sent to this group will only be visible to users who have joined it.
  • BroadcastToOtherUsers: Sends updates to other users in the same room.
  • OnDisconnectedAsync: This method is called when a client disconnects. It is important for cleanup, such as removing the user from any groups they belong to, to prevent sending updates to disconnected clients.

Here’s how you can do it in code:

public class DiagramHub : Hub
{
    private readonly ILogger<DiagramHub> _logger;

    public DiagramHub(ILogger<DiagramHub> logger)
    {
        _logger = logger;
    }

    // 1) Triggers when a user connects to the hub
    public override Task OnConnectedAsync()
    {
        // Send a unique connection ID back to the user
        Clients.Caller.SendAsync("OnConnectedAsync", Context.ConnectionId);
        return base.OnConnectedAsync();
    }

    // 2) Add the current connection to a SignalR group (room)
    public async Task JoinDiagram(string roomName)
    {
        string userId = Context.ConnectionId;

        // Store the room name in the connection context for later retrieval (e.g., on disconnect)
        Context.Items["roomName"] = roomName;

        // Add this connection to the specified group (room)
        await Groups.AddToGroupAsync(userId, roomName);
    }

    // 3) Broadcasts updates to other users in the same room (excludes the sender)
    public async Task BroadcastToOtherUsers(List<string> payloads, string roomName)
    {
        // Send the updates to all other connections in the room
        await Clients.OthersInGroup(roomName).SendAsync("ReceiveData", payloads);
    }

    // 4) Removes the connection from its room when the user disconnects
    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        // Retrieve previously stored room name
        string? roomName = Context.Items.TryGetValue("roomName", out var value) ? value as string : null;

        if (!string.IsNullOrEmpty(roomName))
        {
            await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);
        }
        await base.OnDisconnectedAsync(exception);
    }
}

Step 4: Conflict resolution

When multiple users edit the same diagram, conflicts can happen. To avoid this, we use optimistic concurrency:

Here’s how optimistic concurrency works.

  • Diagram versioning: Every diagram updates we store in Redis will have a unique version number associated with it. When a user interacts with the diagram, it sets version 1.
  • Client tracks version: When a new user loads a diagram, their Blazor application also gets and remembers the diagram’s current version.
  • Sending updates: When a user makes changes and sends an update to the server, they include the version number they are editing. They also send the actual changes (the “payload”) and IDs of affected elements.
  • Server validation: The SignalR hub (via a RedisService) receives the update. It checks the version number sent by the client against the actual latest version of the diagram stored in Redis.
  • If the versions match: Great! The client was working with the most up-to-date diagram. The server applies the changes, increments the version number in Redis, and then broadcasts the new diagram state (along with the latest version) to all other collaborators.
  • If the versions don’t match: This means another user made a change and updated the diagram before this client’s update arrived. The client’s update is rejected. The server then informs the client that their update failed and they need to refresh their view to obtain the latest diagram state, thereby preventing them from overwriting newer changes.

This approach keeps our application responsive because we don’t hold locks on the data. Conflicts are resolved when they occur by asking the user to retrieve the latest version if their changes are outdated.

To manage your diagram state and versions within Redis, you need to create a dedicated RedisService class. This service handles storing, retrieving, and updating diagrams in Redis, ensuring your versioning logic is centralized. The SignalR hub will then inject and use this service to validate and save updates.

Note: You can refer to the RedisService file in the Services folder of the GitHub sample, and add the RedisService to the Program.cs file.

Add the following code to the DiagramHub class:

using Microsoft.AspNetCore.SignalR;

public class DiagramUpdateMessage
{
    public string SourceConnectionId { get; set; } = "";
    public long Version { get; set; }
    public List<string>? ModifiedElementIds { get; set; }
}

public class DiagramHub : Hub
{
    // Triggers the method when the user sends the data to other users via SignalR
    public async Task BroadcastToOtherUsers(List<string> payloads, long userVersion, List<string>? elementIds, string roomName)
    {
        try
        {
            string versionKey = "diagram:version";
            // Try to accept based on expected version (CAS via Lua script)
            (bool accepted, long serverVersion) = await _redisService.CompareAndIncrementAsync(versionKey, userVersion);

            if (!accepted)
            {
                // Check for overlaps based on the previous user version
                List<DiagramUpdateMessage> recentUpdates = await GetUpdatesSinceVersionAsync(userVersion, maxScan: 200);
                HashSet<string> recentlyTouched = new HashSet<string>(StringComparer.Ordinal);
                foreach (DiagramUpdateMessage message in recentUpdates)
                {
                    if (message.ModifiedElementIds == null) continue;
                    foreach (string id in message.ModifiedElementIds)
                        recentlyTouched.Add(id);
                }

                List<string> overlaps = elementIds?.Where(id => recentlyTouched.Contains(id)).Distinct().ToList();
                if (overlaps?.Count > 0)
                {
                    // Reject and notify the user of the conflict message
                    await Clients.Caller.SendAsync("ShowConflict");
                    return;
                }

                // Accept non-overlapping stale update: increment once more
                (bool _, long newServerVersion) = await _redisService.CompareAndIncrementAsync(versionKey, serverVersion);
                serverVersion = newServerVersion;
            }
            // Store update in Redis history
            DiagramUpdateMessage update = new DiagramUpdateMessage
            {
                SourceConnectionId = connId,
                Version = serverVersion,
                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)
        {
            _logger.LogError(ex);
        }
    }

    // Compare version from user's interactions and filter the updates.
    private async Task<IReadOnlyList<DiagramUpdateMessage>> GetUpdatesSinceVersionAsync(long sinceVersion, int maxScan = 200)
    {
        var historyKey = "diagram_updates_history";
        var length = await _redisService.ListLengthAsync(historyKey);
        if (length == 0) return Array.Empty<DiagramUpdateMessage>();

        long start = Math.Max(0, length - maxScan);
        long end = length - 1;

        var range = await _redisService.ListRangeAsync(historyKey, start, end);

        var results = new List<DiagramUpdateMessage>(range.Length);
        foreach (var item in range)
        {
            if (item.IsNullOrEmpty) continue;
            var update = JsonSerializer.Deserialize<DiagramUpdateMessage>(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;
    }
}

Setting up the Blazor application for collaborative editing

In this section, we will see how to configure a Blazor application to connect to our SignalR hub and enable collaborative diagram editing using the Syncfusion Blazor Diagram component.

Let’s begin by setting up our Blazor project and adding the necessary libraries.

Step 1: Create a Blazor application

To create a Blazor Web App, follow the steps outlined in our Blazor documentation.

Next, install these NuGet packages into your Blazor project:

  • Microsoft.AspNetCore.SignalR.Client: This is the essential SignalR client library that allows your Blazor application to connect and communicate with the SignalR hub in your ASP.NET Core app.
  • Syncfusion.Blazor.Diagram: This package provides the powerful Syncfusion Blazor Diagram component, which will be our canvas for drawing and editing diagrams collaboratively.
  • Syncfusion.Blazor.Themes: This package provides the necessary CSS theming to make your Syncfusion Blazor Diagram component look visually appealing and integrated.

Step 2: Configure SignalR service

Once your Blazor application is created and the required packages are installed, the next step is to set up the connection to the SignalR hub. This involves performing the initial handshake and joining a specific collaboration “room” (or group). Each room serves as a dedicated communication channel, ensuring that users receive only updates related to the diagram they are currently editing.

Here’s what we will implement now,

  • Initialize the HubConnection during the component’s initial rendering (OnAfterRenderAsync) and start it using StartAsync.
  • Connect to the /diagramHub endpoint with option WebSockets SkipNegotiation = true, which enables automatic reconnect to handle transient network issues.
  • Subscribe to the OnConnectedAsync callback to receive the unique connection ID, confirming a successful handshake with the server.
  • Join a SignalR group by calling JoinDiagram(roomName) after connecting. This ensures updates are shared only with users in the same diagram session.

Here’s the code example for the above step:

@using Microsoft.AspNetCore.SignalR.Client
@using Microsoft.AspNetCore.Components
@inject NavigationManager NavigationManager

@code {
    private HubConnection? connection;
    private string roomName = "Syncfusion";

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender) 
        { 
            if (connection == null)
            {
                // Run the hub service and add your service URL, like "/diagramHub", in the connection
                connection = new HubConnectionBuilder()
                            .WithUrl("<<Your ServiceURL>>", options =>
                            {
                                options.SkipNegotiation = true;
                                options.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets;
                            })
                            .WithAutomaticReconnect()
                            .Build();
                // Triggered when the connection to the SignalR Hub is successfully established
                connection.On<string>("OnConnectedAsync", OnConnectedAsync);
                await connection.StartAsync();
            }
        }
    }
    private async Task OnConnectedAsync(string connectionId)
    {
        if(!string.IsNullOrEmpty(connectionId))
        {
            // Join the room after the connection is established
            await connection.SendAsync("JoinDiagram", roomName);
        }
    }
}

Here are a few tips,

  • Use a unique roomName per diagram (e.g., a diagram ID) to isolate sessions.
  • If WebSockets may be unavailable, remove SkipNegotiation so SignalR can fall back to SSE or Long Polling.
  • Consider handling reconnecting/disconnected states in the UI and securing the connection with authentication if required.

Step 3: Send and apply real-time Diagram changes

The Syncfusion Blazor Diagram component is key to capturing user edits.

  • It automatically triggers the HistoryChanged event whenever a user modifies the diagram, whether they add, move, resize, or delete elements.
  • Instead of sending the whole diagram, we focus on efficiency. Inside our OnHistoryChanged method, we’ll extract only the specific changes that occurred from the HistoryChangedArgs. This small packet of changes is then sent to our SignalR Hub’s GetDiagramUpdates method, ensuring that it targets only users in the same diagram room.
  • When other users’ clients receive these changes (via the ReceiveData method from the hub), their Blazor Diagram components can efficiently apply them using SetDiagramUpdatesAsync, updating their view instantly.
  • For an even smoother experience, we enable the EnableGroupActions property in the diagram’s DiagramHistoryManager class. This setting cleverly bundles a series of quick, related actions into a single logical change, reducing the number of individual updates sent and improving performance.

Below is the code you need:

<SfDiagramComponent @ref="@DiagramInstance" ID="@DiagramId" HistoryChanged="@OnHistoryChange" >
    <DiagramHistoryManager EnableGroupActions="true"></DiagramHistoryManager>
</SfDiagramComponent>

@code {
    public SfDiagramComponent DiagramInstance;
    public DiagramId = "diagram";
    private string roomName = "Syncfusion";

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender) 
        { 
            if (connection == null)
            {
                // Receive remote changes from diagram hub
                connection.On<List<string>>("ReceiveData", async (diagramChanges) =>
                {
                    await ApplyRemoteDiagramChanges(diagramChanges);
                });
            }
        }
    }

    private async Task ApplyRemoteDiagramChanges(List<string> diagramChanges)
    {
        // Sets diagram updates to the current diagram
        await DiagramInstance.SetDiagramUpdatesAsync(diagramChanges);
    }
    
    private async void OnHistoryChange(HistoryChangedEventArgs args)
    {
        if (args != null)
        {
            List<string> diagramChanges = DiagramInstance.GetDiagramUpdates(args);
            // When EnableGroupActions is enabled, retrieve diagram changes only after the group action completes.
            if (diagramChanges.Count > 0)
            {
                // Send changes to the SignalR Hub for broadcasting
                await connection.SendAsync("BroadcastToOtherUsers", diagramChanges, roomName);
            }
        }
    }
}

Step 4: Conflict policy (optimistic concurrency) in Blazor Application

In collaborative editing, two users might change the same element at nearly the same time. We avoid locking by using optimistic concurrency: each client maintains a local userVersion, and the server returns the authoritative serverVersion with accepted changes. Clients update their userVersion after applying remote changes. If the server detects a mismatch when a client sends changes, it can reject the update and notify the client.

Below are the key implementations,

  • Always apply remote changes using SetDiagramUpdatesAsync(diagramChanges).
  • Update your local userVersion to match the serverVersion supplied by the server.
  • When sending local changes, include userVersion and a list of IDs affected (editedElementIds) so the server can perform conflict checks.

Add the following code to the Blazor sample application:

long userVersion;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender) 
    { 
        if (connection == null)
        {
            connection.On<List<string>, long>("ReceiveData", async (diagramChanges, serverVersion) =>
            {
                await ApplyRemoteDiagramChanges(diagramChanges, serverVersion);
            });
            // Conflict notification
            connection.On("ShowConflict", ShowConflict);

            // Explicit version sync (optional if version is included in ReceiveData)
            connection.On<long>("UpdateVersion", UpdateVersion);
        }
    }
}

private async Task ApplyRemoteDiagramChanges(List<string> diagramChanges, long serverVersion)
{
    // Sets diagram updates to the current diagram
    await DiagramInstance.SetDiagramUpdatesAsync(diagramChanges);
    userVersion = serverVersion;
}

// Capture local changes and send with version and edited IDs
public async void OnHistoryChange(HistoryChangedEventArgs args)
{
    List<string> diagramChanges = DiagramInstance.GetDiagramUpdates(args);
    if (diagramChanges.Count == 0)
    {
        List<string> editedElements = GetEditedElements(args);
        await connection!.SendAsync("BroadcastToOtherUsers", diagramChanges, userVersion, editedElements, roomName);
    }
}

private List<string> GetEditedElements(HistoryChangedEventArgs args)
{
    // Extract and return IDs of affected nodes/connectors from args
    return new List<string>();
}

private void UpdateVersion(long serverVersion) => userVersion = serverVersion;

private void ShowConflict()
{
    // Display a notification to inform the user their update was rejected due to overlap
}

The GIF below showcases collaborative editing by two different users simultaneously on the Blazor application.

Collaborative editing in Blazor Diagram
Collaborative editing in Blazor Diagram

Before you dive in, keep these considerations in mind to ensure a smooth experience:

  • Default deployment: By default, a single server instance works without additional setup. For multi-instance (sca./le-out) deployments, configure a SignalR backplane (e.g., Redis) and use a shared Redis store so all nodes share group messages and version state consistently.
  • View-only interactions: Zoom and pan are local to each user and are not synchronized, allowing collaborators to view different areas of the diagram.
  • Unsupported synchronized settings: Changes to PageSettings, ContextMenu, DiagramHistoryManager, SnapSettings, Rulers, UmlSequenceDiagram, Layout, and ScrollSettings are not propagated to other users and apply only locally.

GitHub reference

You can explore a complete working sample of collaborative editing in Blazor Diagram on the GitHub repository.

FAQs

1. How do I configure SignalR for Blazor Diagram collaboration?
For configuring SignalR in Blazor Diagram, add the NuGet packages SignalR.Client, Syncfusion.Blazor.Diagram, and Syncfusion.Blazor.Themes. Next, set up a HubConnection pointing to /diagramHub, enable WebSockets, skip negotiation, and turn on automatic reconnect. After the connection is established, call JoinDiagram(roomName) so the user can join the collaboration session.

2. How are diagram changes propagated between clients?
Diagram changes are shared between clients whenever the HistoryChanged event occurs, such as when nodes are moved or connectors are edited. These updates are serialized using the GetDiagramUpdates method and sent to the server hub, which then broadcasts them to all clients in the same session group. When you receive the data from the hub, apply the changes using the SetDiagramUpdatesAsync(diagramChanges) method.

3. How does the client handle reconnection and session restoration?
To handle reconnection and restore sessions, the client uses .WithAutomaticReconnect() to automatically reconnect after a disconnection. Once reconnected, the client must call JoinDiagram again to continue collaborating without losing progress.

Syncfusion Blazor components can be transformed into stunning and efficient web apps.

Conclusion

Thank you for reading! This article guided you through building a real-time collaborative editing in Blazor Diagram, powered by SignalR and Redis. We covered setting up the SignalR service, implementing efficient real-time updates for diagram changes, and applying robust conflict resolution.

This approach delivers a seamless, interactive experience for multiple users collaborating simultaneously. Such real-time collaboration not only boosts productivity but also guarantees data consistency and fosters better team communication.

With the Essential Studio 2025 Volume 4 release, collaborative editing is now a built-in feature for Blazor Diagram, making it easier than ever to integrate these techniques into your projects. Integrate these techniques into your Blazor projects to harness the full potential of real-time applications.

Check out our Release Notes and What’s New pages to see the other updates in this release, and leave your feedback in the comments section below. We would love to hear from you.

If you’re a Syncfusion user, you can download the setup from the license and downloads page. Otherwise, you can download a free 30-day trial.

You can also contact us through our support forum, support portal, or feedback portal for queries. We are always happy to assist you!

Be the first to get updates

Suganthi KaruppannanSuganthi Karuppannan profile icon

Meet the Author

Suganthi Karuppannan

Suganthi is a Software Engineer at Syncfusion. She has been a .NET developer since 2017. She specializes in TypeScript, Blazor, and other .NET frameworks.

Leave a comment