Typed Clients
Typed clients
Section titled “Typed clients”Every bridge ships a .Client package: its request and response contracts, and an interface such as
ICalendarBridge whose implementation is generated at build time. The native bridge serializes the same
contracts, so the page and the device agree on every shape by construction.
builder.Services .AddWebAppHostClient() // transport + host, settings, files, links .AddCalendarBridgeClient() .AddWifiBridgeClient();
// a page@inject ICalendarBridge Calendar
var created = await Calendar.CreateEventAsync(new NewCalendarEvent{ Title = "Standup", Start = DateTimeOffset.Now.AddHours(1), End = DateTimeOffset.Now.AddHours(1.5)});- Errors throw
BridgeExceptionwithStatusCode, the bridge’sCodeandIsNotSupportedfor501. - Events are methods too:
await using var sub = await Wifi.OnChangedAsync(e => …). - Files and binaries: a method returning
Task<Stream>orTask<byte[]>reads the body raw; a[BridgeBody("text/plain")]parameter sends one. - TypeScript:
clients/typescriptholds the same clients, generated from the same assemblies bytools/Shiny.AppDeviceBridge.TypeScript—new CalendarBridge().createEvent({ title, start, end }). Required members are required, optional parameters go in an options object with anAbortSignal, and events return an unsubscribe function. A test fails when the committed TypeScript falls behind the C# declarations. - Your own endpoints can still be called untyped through
WebAppBridge.GetAsync<T>(path, typeInfo)andSendAsync<TBody, TResult>(…), or given a[BridgeClient]interface of their own.
Writing a bridge
Section titled “Writing a bridge”public sealed class ClipboardBridge(IClipboard clipboard) : IWebAppBridge{ public string Name => "clipboard"; public bool IsSupported => true;
public void Map(WebAppBridgeRoutes routes) => routes .MapGet("", async ctx => await WebAppBridgeResults.Json(ctx, new Clip(await clipboard.GetTextAsync()), MyJson.Default.Clip));}
public static MauiAppBuilder AddClipboardBridge(this MauiAppBuilder builder){ builder.Services.AddWebAppBridge<ClipboardBridge>(); return builder;}Use source-generated JSON contexts (JsonTypeInfo). The packages are trim and AOT clean, and bridges
should stay that way.
To give it a typed client, declare the API once in a plain net10.0 project that references
Shiny.AppDeviceBridge.Client. The generator implements the interface, and the same contracts serialize on
both sides:
[BridgeClient("clipboard", typeof(ClipboardJson))]public interface IClipboardBridge{ [BridgeGet] Task<Clip> GetAsync(CancellationToken cancellationToken = default); [BridgePut] Task SetAsync(Clip clip, CancellationToken cancellationToken = default); [BridgeEvent("clipboard.changed")] Task<IAsyncDisposable> OnChangedAsync(Func<Clip, Task> handler);}
public sealed record Clip(string? Text);
[JsonSerializable(typeof(Clip))]public partial class ClipboardJson : JsonSerializerContext;
// the pagebuilder.Services.AddWebAppHostClient().AddClipboardBridgeClient();Route tokens ([BridgeGet("items/{id}")]) bind parameters by name, a complex parameter on a POST or PUT is the
JSON body, and everything else is the query string. A declaration the generator can’t turn into a request —
a complex type on a GET, a route token with no parameter — is build error ADB001.


