ModelingEvolution.Chat 1.0.0-preview.7

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

ModelingEvolution.Chat

Event-sourced chat — workspaces → channels → participants — with MudBlazor 9 Blazor components: ConversationList, ConversationView, ChatWidget, ChatInput, ChatMessageView (Markdown messages). Server-side command handlers and read models over MicroPlumberd 1.2.x / KurrentDB.

The published language (identifiers, commands, events) is ModelingEvolution.Chat.Types, a dependency of this package.

What a host needs — nothing else

This is the whole composition. A fresh Blazor Server host with only these lines renders a working multi-channel conversation (that claim is tested: testing/FreshChatHost in the source repository is exactly this and nothing more).

<PackageReference Include="ModelingEvolution.Chat" Version="X.Y.Z" />

Program.cs

using KurrentDB.Client;
using MicroPlumberd.Services;
using ModelingEvolution.Chat;
using MudBlazor.Services;

builder.Services.AddRazorComponents().AddInteractiveServerComponents();
builder.Services.AddMudServices();            // MudBlazor
builder.Services.AddMudMarkdownServices();    // MudBlazor.Markdown — message bodies render as Markdown
builder.Services.AddPlumberd(KurrentDBClientSettings.Create(builder.Configuration["KurrentDB"]!));
builder.Services.AddChatServer();             // command handlers + read models (AddChatClient() = read models only)
builder.Services.AddHealthChecks().AddPlumberdHealthChecks();   // readiness — see "Before you send" below

…and map it: app.MapHealthChecks("/health", …) with a response writer that names each check (the default prints only the status word) — see testing/FreshChatHost/Program.cs.

App.razor (or your layout's <head> / <body>)

<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link href="_content/MudBlazor.Markdown/MudBlazor.Markdown.min.css" rel="stylesheet" />
…
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script src="_content/MudBlazor.Markdown/MudBlazor.Markdown.min.js"></script>
<script src="_framework/blazor.web.js"></script>

Layout — MudBlazor's providers, once:

<MudThemeProvider />
<MudPopoverProvider />
<MudDialogProvider />
<MudSnackbarProvider />

_Imports.razor

@using MudBlazor
@using ModelingEvolution.Chat
@using ModelingEvolution.Chat.Components
@using ModelingEvolution.Chat.ReadModels

A page (interactive — the components bind input events and take EventCallbacks; static SSR will not run them):

@page "/chat"
@rendermode InteractiveServer

<ConversationList WorkspaceId="@Workspace" @bind-SelectedChannelId="_channel" />
<ConversationView ChannelId="@_channel" SenderId="@Me" />

Seeding a conversation

Everything goes through ICommandBus (MicroPlumberd) with the commands from ModelingEvolution.Chat.Types:

await bus.SendAsync(workspaceId,   new DefineWorkspace     { Name = "Support" });
await bus.SendAsync(channelId,     new DefineChannel       { WorkspaceId = workspaceId, Name = "general" });
await bus.SendAsync(participantId, new RegisterParticipant { Email = "alice@example.com", Name = "Alice" });
await bus.SendAsync(channelId,     new SendMessage         { ChannelId = channelId, SenderId = participantId, Content = "**hello**" });

Before you send. MicroPlumberd's command handlers subscribe from the end of the app command stream once the host is running; a command sent before they are ready is never answered (a 2-minute timeout, not an error). Wait for /health to report Healthy (or for AddPlumberdHealthChecks's check) before the first SendAsync — on startup, and in any test that boots the host and seeds it.

Stream naming: Workspace-{WorkspaceId}, Channel-{ChannelId}, Participant-{ParticipantId}; the read models subscribe by event type and need KurrentDB's standard projections running ($by_event_type).

What is in the contract (frozen at 1.0.0)

  • The components' [Parameter]s and EventCallbacks — see each .razor — not their pixels.
  • Workspaces / channels / participants; the multi-channel surface; send + live update; Markdown; participant name resolution.
  • Not in the contract: typing indicators (do not exist), notification hooks (deleted), read receipts (half-built — not asserted).

Latest is a standing constraint

The components are built on MudBlazor 9's supported primitives (MudPaper, MudStack, MudText, MudMarkdown) — never on a MudBlazor "chat" component. In the source repository Directory.Build.props makes a Razor element that resolves to no component (RZ10012) a build error, so a future MudBlazor deletion cannot ship as inert markup.

Rendering user-authored Markdown — the one policy (preview.3 / preview.4)

Every message goes through ChatMarkdownMudMarkdown with MarkdownPipeline="@ChatMarkdown.Pipeline" (raw HTML disabled: tags are literal text) and Props="@ChatMarkdown.Props" (every link/image URL through ChatMarkdown.SafeUrl). MudMarkdown on its own passes raw HTML through and puts any URL scheme into href (P0, 2026-08-17: stored XSS across participants); a host that renders chat content itself must use the same two knobs.

  • Links (ChatMarkdown.IsAllowedUrl): http/https/mailto or a same-origin relative path. Anything else — javascript:, data:, vbscript:, file:, and any spelling of protocol-relative (//evil, /\evil, \\evil, /%2F%2Fevil, /%5Cevil\, %2F, %5C are read as / and control/whitespace dropped before the test) — renders with an EMPTY href.
  • Images (ChatMarkdown.IsAllowedImageUrl) — hazard 5: a link is a choice, an image is a FETCH on render. A remote image from a stranger is a tracking pixel (viewer IP/UA) on a page that may carry a buyer capability, so image sources are allowed ONLY when same-origin relative (/media/a.png); https://… and everything with a scheme or an authority renders <img src=""> (a browser fetches nothing for it).
  • External links carry rel="noopener noreferrer" + target=_blank (asserted per link in the rendered DOM).

The payload sweep (ChatMarkdownXssTests, structural DOM assertions) is the guard; a host's own renderer of chat content belongs behind the same class, and the source repository pins that no raw-markup sink takes user text any other way.

preview.5 — additive, for a host whose buyer has no address and whose command server is elsewhere (design §4.6c)

  • RegisterParticipant.Email is optional: a participant may be registered WITHOUT an address (a portal buyer keyed by its enquiry, whose address lives sealed elsewhere). Address-less participants are never indexed by address (two of them do not collide); an address that IS given must be valid.
  • ConversationView/ChatMessageView OwnLabel (optional): what the viewer's OWN bubbles are labelled — "You" in the viewer's language — instead of the stored name/address; others keep their stored name; nothing stored ⇒ "Unknown".
  • ConversationView.FireAndForget (optional, default false): dispatch SendMessage without awaiting the handler, so an out-of-process command server that is down never stalls the sender's UI until the bus timeout. The command is on the app command stream and is handled when the server is up (see item 10 above: handlers subscribe from the END — a command sent while the server is DOWN is not replayed to it; a host that needs delivery-while-down keeps the default and shows the timeout, or queues on its own side).

preview.6 — channel tags (design §4.6d) and the estate's one tag type

DefineChannel.Tags / ChannelDefined.Tags carry ExternalReference tags from the ModelingEvolution.Tags package — the SAME type a booking's tags are — bounded at 16, duplicates collapsed, '@' refused by the type; ChannelReadModel.ByTag(tag) / ByTagKind(kind) answer "the chat for this offer" exactly as BookingLookupModel.ByTag answers "the meeting for this offer". Channels defined before tags existed read as having none. Add using ModelingEvolution.Tags; (Chat.Types depends on the package).

preview.6 — a sent message is local until the store acks it (design §4.6c, lead's ruling)

ConversationView AWAITS the send with a bounded SendTimeout (default 5 s) and shows the message the viewer typed as a LOCAL bubble — data-chat-delivery="sending" — until the store folds it (the command's id IS the message id: the handler writes it onto MessageSent, the read model folds once per id, and the local bubble is dropped when its stored twin appears). A fault or the timeout ⇒ data-chat-delivery="failed" with NotDeliveredLabel + a RetryLabel button (data-chat-retry) that re-sends the SAME message (SendMessage.MessageId, stable) as a NEW command (SendMessage.Id, fresh) — never a second message. Why a new command (preview.7, measured on TEST): a command appended while the server is DOWN is NOT replayed to it — MicroPlumberd's handlers subscribe from the END of the app command stream (item 10) — and a retry that re-used the command id was refused by the bus as in-flight / de-duplicated by the store, so it did nothing: "retry" that never leaves "failed". A retry must therefore be a command nobody has seen; the stable MessageId is what keeps a retry after a landed-but-unacked attempt from showing twice (the read model folds once per MessageSent.Id). Labels (SendingLabel, NotDeliveredLabel, RetryLabel) are the host's, per language; OnSendFailed (EventCallback<Exception>) fires as well, so a host can keep the draft or toast — in addition to the visible state, never instead of it. FireAndForget still exists as a flag but the buyer page does not use it: a possibly-lost message is worse than a visible "not delivered".

preview.7 — stranded commands are reconciled (§6.124), interim

A SendMessage appended while the chat server is unavailable is durably stored and never handled — after a restart (handlers subscribe from the END of the app command stream, item 10) and after a dropped subscription (a paused container). AddChatServer therefore registers StrandedSendMessageReconciler: at start and every Interval (60 s) it reads $et-SendMessage and, for every command older than Grace (30 s) whose message id has no MessageSent on its channel, runs the real ChannelCommandHandler — one path, idempotent by the stable message id. /health entry "Chat stranded commands": stranded: N (Degraded while the last sweep found any; Healthy at 0; Unhealthy before the first sweep or after a failed one). Requires the standard projections ($by_event_type). Interim — the framework-level fix (command handlers resuming from a checkpoint) is micro-plumberd's; this reconciler is deleted when it lands.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-preview.11 0 8/19/2026
1.0.0-preview.10 0 8/19/2026
1.0.0-preview.9 0 8/19/2026
1.0.0-preview.8 58 8/17/2026
1.0.0-preview.7 47 8/17/2026
1.0.0-preview.6 43 8/17/2026
1.0.0-preview.5 43 8/17/2026
1.0.0-preview.4 49 8/17/2026
1.0.0-preview.3 52 8/17/2026
1.0.0-preview.2 56 8/17/2026
1.0.0-preview.1 52 8/17/2026