WitRPC's interface-driven model works in the browser: a Blazor WebAssembly client calls a .NET backend through the same shared contract as any desktop client, events included. The browser environment adds three constraints, and this page covers how each is handled: networking must be web-friendly (WebSockets, not raw sockets), standard .NET cryptography APIs are unavailable, and AOT compilation forbids runtime code generation.
The Blazor channel factory
The OutWit.Communication.Client.Blazor package wraps everything a Blazor WebAssembly client needs into one registration. It manages a WebSocket connection to the WitRPC server and brings browser-side answers to the environment's restrictions out of the box:
- encryption implemented on the browser's own Web Crypto API (enabled by default, compatible with the server's standard
WithEncryption()); - automatic reconnection with exponential backoff, and retry policies for transient failures;
- integration with Blazor authentication when present: the channel reconnects on sign-in and disconnects on sign-out (and works without any auth packages installed);
- lazy connection: the WebSocket opens on first use.
dotnet add package OutWit.Communication.Client.BlazorRegistration
One call in Program.cs registers the token provider and the channel factory:
using OutWit.Communication.Client.Blazor;
// All defaults: encryption + reconnect + retry enabled
builder.Services.AddWitRpcChannel();
// Custom endpoint and timeout
builder.Services.AddWitRpcChannel(options =>
{
options.ApiPath = "api"; // WebSocket endpoint path (default: "api")
options.TimeoutSeconds = 15; // connection & request timeout (default: 10)
});By default the WebSocket URL derives from the application's own origin. To reach a server on another host, set BaseUrl:
builder.Services.AddWitRpcChannel(options =>
{
options.BaseUrl = "https://api.example.com"; // -> wss://api.example.com/api
});The channel speaks WebSocket with MemoryPack serialization; the server pairs with a matching configuration such as WithWebSocket("http://host:5000/api", maxClients), WithMemoryPack(), and WithEncryption().
Using service proxies
Inject IChannelFactory and request typed proxies; the factory connects on first use:
@inject IChannelFactory ChannelFactory
@code {
private IMyService? m_service;
protected override async Task OnInitializedAsync()
{
m_service = await ChannelFactory.GetServiceAsync<IMyService>();
}
private async Task DoWork()
{
var result = await m_service!.SomethingAsync();
}
}await ChannelFactory.ReconnectAsync() tears the connection down and rebuilds it with fresh encryption keys, for cases like a server restart or a configuration change.
Tuning and escape hatches
The options object exposes the resilience policies and a builder hook for anything beyond the typed settings:
builder.Services.AddWitRpcChannel(options =>
{
options.Reconnect!.MaxAttempts = 5; // 0 = unlimited
options.Retry!.MaxRetries = 5;
options.Reconnect = null; // or disable a policy entirely
options.UseEncryption = false; // e.g. when wss:// TLS is considered sufficient
options.ConfigureClient = client =>
{
client.WithJson(); // override any builder setting
};
});ConfigureClient runs after the typed options are applied, so it can override or extend the underlying WitClientBuilderOptions freely.
Manual setup
The channel factory is a convenience layer; the regular WitClientBuilder works in the browser too, with two substitutions dictated by the environment. Networking goes over WebSocket, and encryption uses the BouncyCastle implementation, since the standard one depends on OS cryptography the browser sandbox does not provide:
var client = WitClientBuilder.Build(options =>
{
options.WithWebSocket("wss://server.example.com/rpc");
options.WithJson();
options.WithBouncyCastleEncryption(); // must also be configured on the server
});
await client.ConnectAsync(TimeSpan.FromSeconds(10), CancellationToken.None);
var service = client.GetService<IMyService>();BouncyCastle encryption requires WithBouncyCastleEncryption() on both ends and is not interoperable with the standard mode; the details live in Security & Authentication →. Running without message-layer encryption (WithoutEncryption() on both sides) is acceptable when the connection is already wss://.
Choose manual setup when you need a transport or lifecycle the channel factory does not model; otherwise the factory covers the typical Blazor application with less code.
Static proxies for AOT
By default, WitRPC builds client proxies at runtime with dynamic code generation. AOT-compiled environments forbid that, Blazor WebAssembly with RunAOTCompilation enabled included. The answer is a compile-time proxy generated by a Roslyn source generator.
Setup takes three steps. Add OutWit.Common.Proxy to the contract project and mark the interface:
using OutWit.Common.Proxy.Attributes;
[ProxyTarget("ExampleServiceProxy")]
public interface IExampleService
{
event Action<double> ProgressChanged;
Task<string> ProcessDataAsync(string data);
}Add OutWit.Common.Proxy.Generator to the client project; building it generates the ExampleServiceProxy class. Then pass the generated proxy to GetService:
var service = client.GetService<IExampleService>(x => new ExampleServiceProxy(x));Everything else stays the same. Static proxies also start faster (no runtime generation) at the cost of a build-time step; one limitation to know about is that generic methods in the contract are not supported by the generator. Use static proxies whenever AOT is in play, and dynamic proxies everywhere else for flexibility.
UI updates from events
Server events arrive on background threads. In Blazor, a handler that changes component state must re-render through the component's dispatcher:
service.ProgressChanged += progress =>
{
m_progress = progress;
InvokeAsync(StateHasChanged);
};The same rule appears in every UI framework (WPF and WinUI use their dispatchers); the framework delivering the event does not know about your UI thread.