# Setting Bcrdt up - Blazor Server (InteractiveServer) One project, one DI container per circuit: `IBcrdtCollaborationService` is scoped, so every connected user gets their own replica and their own client id - never re-register it as a singleton, which would make every visitor one replica whose edits the engine cannot tell apart. The replicas of all circuits live in this one process, so they meet over `BcrdtInMemoryNetwork.Shared` with no relay at all; behind a load balancer, map `BcrdtHub` with a SignalR backplane and connect every circuit to it with `BcrdtSignalRTransportFactory` at an absolute address instead. Pages are prerendered first, and a document opened then belongs to a request that is already over: open it once `RendererInfo.IsInteractive`. A circuit outlives its browser tab by the circuit retention period, so a `CircuitHandler` suspends presence the moment the connection drops. ## Checklist 1. `dotnet add package Bit.Bcrdt` (the engine and the Blazor components) to the project with the collaborative components - the .Client project of a Blazor Web App - and a transport: `Bit.Bcrdt.SignalR`, or the WebSocket transport built into `Bit.Bcrdt`, or none at all for a purely local document. The relay is `Bit.Bcrdt.Server`, on the server. 2. Call `builder.Services.AddBitBcrdtServices(...)` in EVERY DI container that renders collaborative components - a Blazor Web App has two - through one shared extension method so both get the same configuration. It registers `IBcrdtCollaborationService` scoped: one replica per circuit in Blazor Server, one per app in WebAssembly. Never re-register it as a singleton. 3. Give it a transport with an ABSOLUTE address: the .NET SignalR client rejects `"/hub/bcrdt"` when the connection is built. Resolve it against the host - `builder.HostEnvironment.BaseAddress` in WebAssembly, `NavigationManager.BaseUri` where a component or a scoped service can ask. 4. On the relay: `builder.Services.AddBitBcrdtHub()` - not a bare `AddSignalR`, whose 32 KB receive limit disconnects every client once a document outgrows it - and `app.MapHub("/hub/bcrdt")`; or `app.UseWebSockets()` BEFORE `app.MapBitBcrdtWebSocket("/ws/bcrdt/{documentId}")`. Either relay only relays: no document lives on it. 5. Add `@using Bit.Bcrdt` and `@using Bit.Bcrdt.Blazor` to `_Imports.razor`. 6. Open a document with ``, which opens it, cascades it and releases it, or with `IBcrdtCollaborationService.OpenAsync` paired with `CloseAsync` - and only once the component is interactive wherever pages are prerendered: `RendererInfo.IsInteractive` on .NET 9 and later, `OnAfterRenderAsync` on .NET 8. 7. Derive your own collaborative components from `BcrdtComponentBase` and `Track` what they render, so a change from anyone re-renders them, coalesced onto a frame. There is no script tag to add: `BcrdtTextArea` imports its small caret module from `_content/Bit.Bcrdt/bit-bcrdt.js` itself. ## The files ### `Program.cs` ```csharp using Bit.Bcrdt; using Microsoft.AspNetCore.Components.Server.Circuits; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); // One replica per circuit (the service is scoped), all in this process, meeting over the in-memory network. // The file store serializes the appends of every replica of a document, so they can share it. builder.Services.AddBitBcrdtServices(collaboration => collaboration .UseTransport(documentId => BcrdtInMemoryNetwork.Shared.CreateTransport(documentId)) .UseStore(new BcrdtFileSystemDocumentStore(Path.Combine(builder.Environment.ContentRootPath, "documents")))); // Resolved from each circuit's own scope, so it sees that circuit's documents. builder.Services.AddScoped(); var app = builder.Build(); app.UseAntiforgery(); app.MapStaticAssets(); app.MapRazorComponents() .AddInteractiveServerRenderMode(); app.Run(); ``` ### `PresenceCircuitHandler.cs` ```csharp using Bit.Bcrdt; using Microsoft.AspNetCore.Components.Server.Circuits; // A circuit outlives its tab by the retention period, and would keep the user listed until then. This // withdraws them the moment the connection drops, and publishes them again if it comes back. public sealed class PresenceCircuitHandler(IBcrdtCollaborationService collaboration) : CircuitHandler { public override Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken) { foreach (BcrdtCollaborativeDocument document in collaboration.OpenDocuments) document.Presence.Suspend(); return Task.CompletedTask; } public override Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken) { foreach (BcrdtCollaborativeDocument document in collaboration.OpenDocuments) document.Presence.Resume(); return Task.CompletedTask; } } ``` ### `Components/Pages/Notes.razor` ```razor @page "/notes/{Id}" @rendermode InteractiveServer @if (!RendererInfo.IsInteractive) {

Connecting…

} else { } @code { [Parameter] public string Id { get; set; } = default!; private readonly BcrdtCollaboratorInfo _me = new("u-1", "Ada", BcrdtPresenceService.SuggestColor("u-1")); } ``` ### `Components/_Imports.razor` ```razor @using Bit.Bcrdt @using Bit.Bcrdt.Blazor ``` Behind a load balancer the circuits of one document are in different processes. Keep the same wiring, but map the hub (`AddBitBcrdtHub().AddStackExchangeRedis(...)` or another backplane, `MapHub`) and replace the in-memory transport with `new BcrdtSignalRTransportFactory("https://collab.example.com/hub/bcrdt")`. ## From the guide ### Hosting models The DI registration is identical in every hosting model, and `IBcrdtCollaborationService` is registered **scoped** for a specific reason: - **WebAssembly** - one scope per application, so scoped behaves as a singleton. Correct for a single-user client. - **Server** - one scope per circuit, so every connected user gets their own replica and their own client identity. Registering it as a singleton would make every visitor share one replica, and the engine could not tell their edits apart. - **Blazor Web App** - register it in both projects; whichever side renders interactively owns the replica. - **Hybrid (MAUI, WPF, WinForms)** - as WebAssembly, with the local process as the replica. The engine requires no JavaScript interop, so it runs unchanged in all of them, in a background service, in a console application, or on a server that hosts documents without rendering them. --- ### The five-minute version ```razor @using Bit.Bcrdt @using Bit.Bcrdt.Blazor @code { private readonly BcrdtCollaboratorInfo _me = new("u-1", "Ada", BcrdtPresenceService.SuggestColor("u-1")); } ``` That is a working collaborative editor with live presence, automatic reconnection and offline support. The textarea also publishes its caret through presence, anchored to content rather than to an index, so an application can draw collaborators' cursors; nothing in the library draws them for you. There is no stylesheet to reference and no script tag to add: the one small module `BcrdtTextArea` needs (`_content/Bit.Bcrdt/bit-bcrdt.js`) is imported on demand. - `BcrdtCollaborationScope` opens the document, renders its content with the document as context (and cascades it), shows `LoadingContent` while it opens, follows `DocumentId` when it changes, and releases the document when it goes away. - `BcrdtComponentBase` - the base of all three components, and the base to derive your own collaborative components from - marshals change notifications onto the renderer and coalesces them onto a frame budget (`RenderInterval`, 16 ms by default), so a fast typist and five collaborators do not produce five hundred renders a second. Subscriptions made through its `Track` overloads are disposed with the component. ```razor @inherits BcrdtComponentBase

@_title.Count characters

@code { [CascadingParameter] public BcrdtCollaborativeDocument Document { get; set; } = default!; private BcrdtText _title = default!; protected override void OnParametersSet() { _title = Document.Text("title"); Track(_title); // re-render, coalesced, whenever the text changes - from anyone } } ``` In Blazor Server, where a page is prerendered and then becomes interactive, open documents only once the component is interactive (`RendererInfo.IsInteractive`) - a document opened while prerendering belongs to a request that is already over. --- ### Presence Presence is **ephemeral, non-persistent, non-CRDT state** - who is here, where their cursor is, whether they are typing - deliberately separate from the document, with different guarantees. `BcrdtPresenceService` (`document.Presence`) is a typed façade over the awareness store: ```csharp document.Presence.SetUser(new BcrdtCollaboratorInfo("ada", "Ada", BcrdtPresenceService.SuggestColor("ada"))); document.Presence.SetCaret("body", document.Text("body"), 42); document.Presence.SetActivity("typing"); foreach (BcrdtCollaborator other in document.Presence.Others) { (int Start, int End)? selection = other.Selection?.Resolve(document.Crdt); // anchored to content, not to an index } ``` Each entry has a time-to-live (30 seconds by default, `BcrdtCollaborationOptions.PresenceTimeout`) refreshed by a heartbeat, so a crashed tab disappears from everyone's list without any server bookkeeping. `SuggestColor` hashes a user id into a fixed palette, so every participant agrees on everyone's colour with no coordination. In Blazor Server a circuit outlives its browser tab by the circuit retention period, and its documents keep publishing presence until then. To withdraw a user as soon as the connection drops, register a `CircuitHandler` that calls `BcrdtPresenceService.Suspend` on every open document in `OnConnectionDownAsync` and `Resume` in `OnConnectionUpAsync`. --- What a merge decides is the choice that matters most after the wiring - call `GetBcrdtGuideSection(heading: "Choosing a data type")`, and run concurrent edits through `InspectBcrdtMerge` before building on one. `GetBcrdtSourceFile(path: "Demo/Client/Pages/Live.razor")` is the page an application would write, and `GetBcrdtSourceFile()` lists the demo's working pages.