MODULE 05 · LESSON 5.3
Implement the complete C# chat client
Implement the MAUI connection service and independent receive loop.
Lesson 23 of 86 · FabrCore 2.0
Overview
SendMessageAsync acknowledges acceptance; it is not the final reply. SendMessageAndReceiveAsync awaits a reply for that request. ReadDeliveriesAsync handles delivered messages, including out-of-turn results. Keeping receipt separate from UI dispatch allows the same service to serve Android and another .NET client.
Accepted means queued, not answered
SendMessageAsync reports that the Host accepted the request. Processing may still be running, and a final reply arrives later. A request-scoped receive helper can await its matching answer, while ReadDeliveriesAsync supplies delivered traffic such as replies and out-of-turn messages. Decide which consumer owns each message so the UI does not append it twice.
Acknowledge after application state is safe
The reusable service calls your application callback with a delivery, then acknowledges its sequence only after that callback succeeds. The callback should apply/deduplicate the message in the account's local store. If it fails, acknowledging anyway would let the server discard a delivery the user never retained. UI rendering can be refreshed from stored state.
Implement sending and receiving
Download the Operations Desk source. The README lists project setup, package prerequisites and local ports.
- Register the connection service through MauiProgram.cs. Supply an authenticated HttpClient, stable device/client ID and checkpoint store to FabrCoreWebSocketClient.
- ConnectAsync, start the delivery loop under a lifetime CancellationTokenSource, and query tracked/shared agents for the picker. Send AgentMessage with ToHandle, Channel and RequestId metadata.
- Apply received deliveries to local state before AcknowledgeAsync. Dispose the connection and cancel the loop when its owning session ends, not every time the page redraws.
using FabrCore.Client.WebSocket;
using FabrCore.Core;
public sealed class ChatConnection : IAsyncDisposable
{
private readonly FabrCoreWebSocketClient client;
public ChatConnection(HttpClient authenticatedHttpClient, Uri hostUri,
string clientId, IFabrCoreWebSocketCheckpointStore checkpoints)
{
client = new FabrCoreWebSocketClient(authenticatedHttpClient,
new FabrCoreWebSocketClientOptions { HostUri = hostUri, ClientId = clientId },
checkpoints);
}
public event EventHandler<FabrCoreWebSocketGapEventArgs> ResyncRequired
{
add => client.ResyncRequired += value;
remove => client.ResyncRequired -= value;
}
public Task ConnectAsync(CancellationToken ct) => client.ConnectAsync(ct);
public async Task ReceiveAsync(
Func<FabrCoreWebSocketDelivery, CancellationToken, Task> apply,
CancellationToken ct)
{
await foreach (var delivery in client.ReadDeliveriesAsync(ct))
{
// Persist/deduplicate local state before acknowledging.
await apply(delivery, ct);
await client.AcknowledgeAsync(delivery.Sequence, ct);
}
}
public Task<FabrCore.Core.WebSockets.FabrCoreWebSocketAccepted> SendAsync(
string target, string text, CancellationToken ct) => client.SendMessageAsync(
new AgentMessage { ToHandle = target, Message = text,
Channel = "operations-desk", Args = new() { ["RequestId"] = "SR-1042" } }, ct);
public ValueTask DisposeAsync() => client.DisposeAsync();
}
Follow one send through the receiver
- Send a message to an already provisioned echo instance. Show a pending item when the send is accepted and keep it pending until the actual reply is observed.
- Log the returned acceptance and the later delivered message separately. Correlate the reply with the original send and render its text once.
- Force the application callback to fail in a development test before acknowledging. On recovery, expect the unacknowledged delivery to be eligible for replay rather than silently considered processed.
This demonstrates acceptance, delivery, application processing and acknowledgement as separate stages. A connected socket or successful send receipt is not a completed conversation turn.
If the result is different
An HTTP 200 provisioning response can contain per-agent failures. Check those before debugging an empty picker. Do not run the receive loop synchronously on the UI thread.
Go deeper
Explore the related documentation.