ModelingEvolution.Chat 1.0.0-preview.5

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" />
<!-- brings ModelingEvolution.Chat.Types, MicroPlumberd 1.2.x, MudBlazor 9.x, MudBlazor.Markdown 9.x,
     ModelingEvolution.Observable.Blazor -->

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).

No packages depend on ModelingEvolution.Chat.

Version Downloads Last updated
1.0.0-preview.11 2 08/19/2026
1.0.0-preview.10 0 08/19/2026
1.0.0-preview.9 0 08/19/2026
1.0.0-preview.8 3 08/17/2026
1.0.0-preview.7 1 08/17/2026
1.0.0-preview.6 4 08/17/2026
1.0.0-preview.5 1 08/17/2026
1.0.0-preview.4 2 08/17/2026
1.0.0-preview.3 4 08/17/2026
1.0.0-preview.2 2 08/17/2026
1.0.0-preview.1 2 08/17/2026