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.
bash
dotnet add package OutWit.Communication.Client.Blazor

The channel factory hands out proxies through GetServiceAsync<T>() and sits on the runtime-proxy path, so the package brings OutWit.Communication.Client.DynamicProxy with it. That suits a JIT-compiled Blazor WebAssembly application. For an AOT-published one, use the manual setup below with source-generated proxies.

Registration

One call in Program.cs registers the token provider and the channel factory:

csharp
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:

csharp
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:

csharp
@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:

csharp
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:

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

Source-generated proxies for AOT

A client proxy can be emitted at runtime or generated at compile time. Runtime emission is unavailable under AOT, which includes Blazor WebAssembly with RunAOTCompilation enabled, so AOT builds use the source-generated path. That path needs no runtime proxy package at all: since version 2.4.0, Castle.Core lives only in the opt-in OutWit.Communication.Client.DynamicProxy package, and a client on source-generated proxies publishes with no Castle assembly in its dependency graph.

Setup takes three steps. Add OutWit.Common.Proxy to the contract project and mark the interface:

csharp
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:

csharp
var service = client.GetService<IExampleService>(x => new ExampleServiceProxy(x));

Everything else stays the same. Source-generated proxies also start faster, since nothing is emitted at runtime, at the cost of a build-time step; the one limitation to know about is that the generator does not support generic methods in a contract. Use them whenever AOT or trimming is in play; the runtime path stays available elsewhere for its convenience. Client Proxies → covers both in full.

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:

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