Getting Started with ASP.NET Core Collaboration Server
9 Sep 202624 minutes to read
This walk-through demonstrates how to create a collaborative DOCX Editor application using the ASP.NET Core Collaboration Server with SignalR transport (default). It uses the common collaboration services on the server and the shared @syncfusion/ej2-collaborator client package in the browser.
This guide uses DOCX Editor as the reference component. The same collaboration infrastructure can be used with PDF Viewer and Spreadsheet, with only the control-specific adapter implementation changing for each component.
Client Side
Step 1 — Install the client packages
In your front-end project:
npm install @syncfusion/ej2-collaboratorStep 2 - Reference Adapter (DocumentEditorAdapter.ts)
The adapter acts as a bridge between the Collaboration Client and the EJ2 Document Editor. It implements the ICollaborationProvider interface and is responsible for applying remote collaboration actions to the editor.
This guide uses the Document Editor adapter as an example. The same integration pattern can be used for PDF Viewer and Spreadsheet by implementing a control-specific adapter that conforms to the ICollaborationProvider interface.
import {
DocumentEditor,
DocumentEditorContainer,
Operation
} from "@syncfusion/ej2-documenteditor";
import { ICollaborationProvider, ICollaborationActionData } from "@syncfusion/ej2-collaborator";
export class DocumentEditorAdapter implements ICollaborationProvider {
constructor(
private container: DocumentEditorContainer,
private serviceUrl: string,
) { }
// Fetch the document from the product's REST API and return the room name.
public async loadFromServer(fileName: string): Promise<string> {
const roomName: string = this.getRoomName(fileName);
const response: Response = await fetch(
this.serviceUrl + 'api/CollaborativeEditing/ImportFile',
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ fileName, roomName })
}
);
if (!response.ok) {
throw new Error('Failed to load document');
}
const responseText: string = await response.text();
await this.open(responseText, roomName);
return roomName;
}
// Seed the editor and bridge local edits to the editor's sender.
public async open(responseText: string, roomName: string): Promise<void> {
const data: any = JSON.parse(responseText);
this.container?.documentEditor.collaborativeEditingHandlerModule?.updateRoomInfo(roomName, data.version, this.serviceUrl + 'api/CollaborativeEditing/');
this.container.documentEditor.open(data.sfdt);
this.container.contentChange = (args: any) => {
console.log('[SENT]', new Date().toISOString());
this.container.documentEditor.collaborativeEditingHandlerModule?.sendActionToServer(args.operations as Operation[]);
}
}
// The only ICollaborationProvider method — applied for every remote action.
public applyRemoteAction(action: string, data: ICollaborationActionData): void {
this.container.documentEditor.collaborativeEditingHandlerModule?.applyRemoteAction(action, data.payload);
}
private getRoomName(fileName: string): string {
const queryString: string = window.location.search;
const urlParams: URLSearchParams = new URLSearchParams(queryString);
let roomId: string | null = urlParams.get('id');
if (!roomId) {
roomId = Math.random().toString(32).slice(2);
window.history.replaceState({}, '', '?id=' + roomId);
}
return roomId;
}
}Step 3 — DOCX Editor initialization and collaborator client wiring
// Import the required Document Editor and collaboration client APIs.
import { DocumentEditorContainer, DocumentEditor, Toolbar, CollaborativeEditingHandler }from '@syncfusion/ej2-documenteditor';
import { CollaborationClient, UserInfo } from '@syncfusion/ej2\-collaborator';
import { DocumentEditorAdapter } from '../collaboration/DocumentEditorAdapter';
import { TitleBar } from './title-bar';
// Register the Document Editor services used by the sample.
DocumentEditor.Inject(CollaborativeEditingHandler);
DocumentEditorContainer.Inject(Toolbar);
// Set the backend service URL used by the adapter and editor.
const serviceUrl: string = 'http://localhost:62870/';
// Create the Document Editor container and enable collaborative editing.
const documenteditor: DocumentEditorContainer = new DocumentEditorContainer({
enableToolbar: true,
height: '590px',
currentUser: currentUser,
serviceUrl: serviceUrl + 'api/documenteditor' // product REST API (open/save SFDT)
});
documenteditor.appendTo('#DocumentEditor');
documenteditor.documentEditor.enableCollaborativeEditing = true;
// Create the adapter and collaboration client.
const adapter: DocumentEditorAdapter = new DocumentEditorAdapter(documenteditor, serviceUrl);
const client: CollaborationClient = new CollaborationClient(adapter, {
serviceUrl: "http://localhost:62870",
connectionType: "signalr",
currentUser: currentUser,
onUserJoined: (user: UserInfo) => {
console.log("User Joined", user);
// titleBar.addUser(user);
},
onUserLeft: (user: UserInfo) => {
console.log("User Left", user);
//titleBar.removeUser(user);
}
});
// Load the document, then join the collaboration room.
(async () => {
const roomName: string = await adapter.loadFromServer("Giant Panda.docx");
await client.joinRoomAsync(roomName);
})();Step 4 — Run the client
Build and serve the front-end application so the page is reachable at, for example, http://localhost:4000
Integrate Collaboration Server
Step 5 — Install the NuGet packages
In your ASP.NET Core project, add the Collaboration Server and the DOCX Editor server-side packages
Syncfusion.Collaborator.Server.AspNet.Core
Syncfusion.EJ2.WordEditor.AspNet.Core.
Step 6 — Configure Redis
Add the connection string in ’appsettings.json’
{
"ConnectionStrings": {
"Redis": "<Provide your redis connection string>"
}
}Step 7 — Register the Collaboration Server
Register the Collaboration Server and configure the Redis connection string during application startup.
using Syncfusion.Collaboration.Core.Extensions;
var builder = WebApplication.CreateBuilder(args);
..
..
builder.Services.AddCollaborationServer(options =>
{
options.ConnectionString =
builder.Configuration.GetConnectionString("Redis")
?? "localhost:6379";
// ConnectionType = ConnectionType.SignalR is the default. Switch to WebSocket to use /ws.
});
builder.Services.AddSingleton<ICollaborationAdapter, DocumentEditorAdapter>();
builder.Services.AddControllers();
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.MapControllers();
app.MapCollaborationServer(); // maps /collaborationhub
app.Run();By default, the ASP.NET Core Collaboration Server uses SignalR. To use WebSocket transport, configure ConnectionType as WebSocket and enable WebSocket support in the application pipeline and and call app.UseWebSockets() before MapCollaborationServer()
Step 8 — Add the DOCX Editor adapter
DocumentEditorCollaborationAdapter the control-specific translator on the server side. PDF Viewer and Spreadsheet applications provide their own adapter implementation, but the overall structure remains the same.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using EJ2DocumentEditorServer.Controllers;
using Microsoft.AspNetCore.Hosting;
using Newtonsoft.Json;
using Syncfusion.Collaboration.Core.Interfaces;
using Syncfusion.Collaboration.Core.Models;
using Syncfusion.Collaboration.Core.Services;
using Syncfusion.EJ2.DocumentEditor;
namespace EJ2DocumentEditorServer.Adapters;
// Translates Document Editor actions to and from the common collaboration model.
public class DocumentEditorCollaborationAdapter : ICollaborationAdapter
{
// Used to load the source document and persist the merged result.
private readonly IActionService actionService;
// Queues save operations so document persistence can happen in the background.
private readonly IBackgroundTaskQueue saveTaskQueue;
// Stores the wwwroot path for saving generated documents.
static string fileLocation;
// Provides access to the web root path.
private readonly IWebHostEnvironment _hostingEnvironment;
// Initializes the adapter with the hosting environment and background save queue.
public DocumentEditorCollaborationAdapter(IWebHostEnvironment hostingEnvironment, IBackgroundTaskQueue saveTaskQueue)
{
_hostingEnvironment = hostingEnvironment;
fileLocation = _hostingEnvironment.WebRootPath;
this.saveTaskQueue = saveTaskQueue;
}
// Converts a control-specific action into the shared collaboration action format.
public CollaborationAction MapControlToGenericAction(object controlAction)
{
var action = (Syncfusion.EJ2.DocumentEditor.ActionInfo)controlAction;
return new CollaborationAction
{
RoomName = action.RoomName,
ConnectionId = action.ConnectionId,
CurrentUser = action.CurrentUser,
Version = action.Version,
ClientVersion = action.ClientVersion,
IsTransformed = action.IsTransformed,
Data = JsonConvert.SerializeObject(action.Operations)
};
}
// Converts a shared collaboration action back into a Document Editor action.
public object MapGenericToControlAction(CollaborationAction action)
{
return new Syncfusion.EJ2.DocumentEditor.ActionInfo
{
RoomName = action.RoomName,
ConnectionId = action.ConnectionId,
CurrentUser = action.CurrentUser,
Version = action.Version,
ClientVersion = action.ClientVersion,
IsTransformed = action.IsTransformed,
Operations = JsonConvert.DeserializeObject<List<DocumentOperation>>(action.Data)
};
}
// Transforms the incoming actions before they are applied to the document.
public void TransformOperations(List<CollaborationAction> actions)
{
var documentActions = actions.Select(x => (Syncfusion.EJ2.DocumentEditor.ActionInfo)MapGenericToControlAction(x)).ToList();
documentActions.Where(x => !x.IsTransformed).ToList().ForEach(x => CollaborativeEditingHandler.TransformOperation(x, documentActions));
}
// Queues a save request so the updated document can be processed in the background.
public async Task SaveOperationsAsync(List<CollaborationAction> actions, string roomName, bool partialSave)
{
var documentActions = actions.Select(x => (Syncfusion.EJ2.DocumentEditor.ActionInfo)MapGenericToControlAction(x)).ToList();
var message = new SaveRequest
{
Actions = actions,
PartialSave = partialSave,
RoomName = roomName
};
await saveTaskQueue.QueueBackgroundWorkItemAsync(message);
}
// Applies the pending collaboration actions and saves the updated document.
public async Task ProcessSaveRequestAsync(SaveRequest request, CancellationToken ct)
{
Console.WriteLine("save called");
// You can get the document master document
Syncfusion.EJ2.DocumentEditor.WordDocument document = CollaborativeEditingController.GetSourceDocument();
CollaborativeEditingHandler handler = new CollaborativeEditingHandler(document);
//Get actions from Redis
var actions = request.Actions.Select(x => (Syncfusion.EJ2.DocumentEditor.ActionInfo)MapGenericToControlAction(x)).ToList();
if (actions.Count > 0)
{
foreach (var action in actions)
{
if (!action.IsTransformed)
{
CollaborativeEditingHandler.TransformOperation(action, actions);
}
}
//Apply the actions to document
foreach (var action in actions)
{
handler.UpdateAction(action);
}
MemoryStream stream = new MemoryStream();
//save the updated document in the loaction as per your need.
Syncfusion.DocIO.DLS.WordDocument doc = WordDocument.Save(Newtonsoft.Json.JsonConvert.SerializeObject(handler.Document));
doc.Save(stream, Syncfusion.DocIO.FormatType.Docx);
SaveDocument(stream, "Getting Started.docx");
stream.Close();
}
document.Dispose();
await actionService.ClearRecordsAsync(request.RoomName, request.PartialSave);
}
//Document is store in file stream, We can modify the code to store the document to any location based on your requirment.
private void SaveDocument(Stream document, string fileName)
{
string filePath;
if (Path.IsPathRooted(fileName))
{
filePath = fileName;
}
else
{
filePath = Path.Combine(fileLocation, fileName);
}
// Ensure target directory exists
var dir = Path.GetDirectoryName(filePath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
using (FileStream file = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
document.Position = 0; // Ensure the stream is at the start
document.CopyTo(file);
}
}
}Step 9 — Add the collaborative editing controller (web service methods)
CollaborativeEditingController is the HTTP bridge between the client control and the Common Collaborator. Every EJ2 content editor component that supports collaboration (DOCX Editor, PDF Viewer, Spreadsheet) exposes the same three web service methods on its collaboration controller. Each method is required:
| Web service method | Why it is needed |
|---|---|
| ImportFile | Loads the source document and applies any pending collaboration actions before sending the latest document state to a newly connected client. Returns the document content and current server version. |
| UpdateAction | Receives editing actions from connected clients, processes operational transformation, persists the action, and broadcasts the updated action to other participants. |
| GetActionsFromServer | Retrieves collaboration actions created after the client’s last synchronized version so the client can catch up with the latest document state. |
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Syncfusion.Collaboration.Core.Interfaces;
using Syncfusion.Collaboration.Core.Models;
using Syncfusion.Collaboration.Core.Services;
using Syncfusion.Collaboration.Core.Transports;
using Syncfusion.EJ2.DocumentEditor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace EJ2DocumentEditorServer.Controllers;
// Handles the web service endpoints used by the client-side collaboration adapter.
[Route("api/[controller]")]
[ApiController]
public class CollaborativeEditingController : ControllerBase
{
// Used to resolve the source document path.
private static string fileLocation;
// Provides access to the web root path.
private readonly IWebHostEnvironment _hostingEnvironment;
// Stores and retrieves collaboration actions.
private readonly IActionService actionService;
// Converts between Document Editor actions and common collaboration actions.
private readonly ICollaborationAdapter adapter;
// Broadcasts updated actions to other connected clients.
private readonly IActiveTransport _transport;
// Constructor for the CollaborativeEditingController
public CollaborativeEditingController(IWebHostEnvironment hostingEnvironment,
IConfiguration config, IActionService actionService, ICollaborationAdapter adapter, IActiveTransport transport)
{
_hostingEnvironment = hostingEnvironment;
fileLocation = _hostingEnvironment.WebRootPath;
this.adapter = adapter;
this.actionService = actionService;
_transport = transport;
}
// Loads the source document and applies any pending collaboration actions.
[HttpPost]
[Route("ImportFile")]
[EnableCors("AllowAllOrigins")]
public async Task<string> ImportFile([FromBody] FileInfo param)
{
try
{
// Create a new instance of DocumentContent to hold the document data
DocumentContent content = new DocumentContent();
Syncfusion.EJ2.DocumentEditor.WordDocument document = GetSourceDocument();
// Get the list of pending operations for the document
List<CollaborationAction> collaborationActions = await actionService.GetPendingOperationsAsync(param.roomName, 0, -1);
List<Syncfusion.EJ2.DocumentEditor.ActionInfo> actions =
collaborationActions.Select(x => (Syncfusion.EJ2.DocumentEditor.ActionInfo)adapter.MapGenericToControlAction(x)).ToList();
if (actions != null && actions.Count > 0)
{
// If there are any pending actions, update the document with these actions
document.UpdateActions(actions);
}
// Serialize the updated document to SFDT format
string sfdt = Newtonsoft.Json.JsonConvert.SerializeObject(document);
content.version = 0;
content.sfdt = sfdt;
// Dispose of the document to free resources
document.Dispose();
// Return the serialized content as a JSON string
return Newtonsoft.Json.JsonConvert.SerializeObject(content);
}
catch
{
return null;
}
}
// Receives a local editing action, transforms it, stores it, and broadcasts it.
[HttpPost]
[Route("UpdateAction")]
[EnableCors("AllowAllOrigins")]
public async Task<Syncfusion.EJ2.DocumentEditor.ActionInfo> UpdateAction(Syncfusion.EJ2.DocumentEditor.ActionInfo param)
{
// Convert DocumentEditor ActionInfo to CollaborationAction
CollaborationAction collaborationAction = (CollaborationAction)adapter.MapControlToGenericAction(param);
// Process through common package
CollaborationAction modifiedAction = await actionService.AddOperationAsync(collaborationAction, adapter);
// Convert back to DocumentEditor ActionInfo
var documentAction = (Syncfusion.EJ2.DocumentEditor.ActionInfo)adapter.MapGenericToControlAction(modifiedAction);
await _transport.SendToGroupAsync(param.RoomName, "action", documentAction);
return documentAction;
}
// Returns actions that the client has not yet synchronized.
[HttpPost]
[Route("GetActionsFromServer")]
[EnableCors("AllowAllOrigins")]
public async Task<string> GetActionsFromServer(Syncfusion.EJ2.DocumentEditor.ActionInfo param)
{
try
{
// Initialize necessary variables from the parameters and helper class
//int saveThreshold = CollaborativeEditingHelper.SaveThreshold;
string roomName = param.RoomName;
int lastSyncedVersion = param.Version;
int clientVersion = param.Version;
// Retrieve the database connection
// IDatabase database = _redisConnection.GetDatabase();
// Fetch actions that are effective and pending based on the last synced version
List<CollaborationAction> collaborationActions = await actionService.GetEffectivePendingVersionAsync(roomName, lastSyncedVersion);
List<Syncfusion.EJ2.DocumentEditor.ActionInfo> actions = collaborationActions.Select(x => (Syncfusion.EJ2.DocumentEditor.ActionInfo)adapter.MapGenericToControlAction(x)).ToList();
// Increment the version for each action sequentially
actions.ForEach(action => action.Version = ++clientVersion);
// Filter actions to only include those that are newer than the client's last known version
actions = actions.Where(action => action.Version > lastSyncedVersion).ToList();
// Transform actions that have not been transformed yet
actions.Where(action => !action.IsTransformed).ToList()
.ForEach(action => CollaborativeEditingHandler.TransformOperation(action, actions));
// Serialize the filtered and transformed actions to JSON and return
return Newtonsoft.Json.JsonConvert.SerializeObject(actions);
}
catch
{
// In case of an exception, return an empty JSON object
return "{}";
}
}
// Loads the document from the application root.
internal static Syncfusion.EJ2.DocumentEditor.WordDocument GetSourceDocument()
{
string path = fileLocation + "\\Giant Panda.docx";
int index = path.LastIndexOf('.');
string type = index > -1 && index < path.Length - 1 ?
path.Substring(index) : ".docx";
Stream stream = System.IO.File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read);
Syncfusion.EJ2.DocumentEditor.WordDocument document = Syncfusion.EJ2.DocumentEditor.WordDocument.Load(stream, FormatType.Docx);
stream.Dispose();
return document;
}
public class DocumentContent
{
// The current server version for the document.
public int version { get; set; }
// The serialized SFDT payload.
public string sfdt { get; set; }
}
public class FileInfo
{
// The requested file name.
public string fileName
{
get;
set;
}
// The collaboration room name.
public string roomName
{
get;
set;
}
}
}Step 10 - Run the Application
After completing the client and server setup:
- Start the Redis server.
- Run the ASP.NET Core application using
dotnet run - Run the client application
- Open the application in multiple browser windows or tabs.
- Open the same document and make changes in one window and it will be synced to other users.
Result
- Changes are synchronized automatically across all connected users.
- User join and leave events are reflected in real time.
- Editing operations are stored in Redis and processed by the Collaboration Server.
- Document changes are automatically saved when the configured
SaveThresholdis reached or when all users disconnect.