Ensemble.Client
0.5.0
dotnet add package Ensemble.Client --version 0.5.0
NuGet\Install-Package Ensemble.Client -Version 0.5.0
<PackageReference Include="Ensemble.Client" Version="0.5.0" />
<PackageVersion Include="Ensemble.Client" Version="0.5.0" />
<PackageReference Include="Ensemble.Client" />
paket add Ensemble.Client --version 0.5.0
#r "nuget: Ensemble.Client, 0.5.0"
#:package Ensemble.Client@0.5.0
#addin nuget:?package=Ensemble.Client&version=0.5.0
#tool nuget:?package=Ensemble.Client&version=0.5.0
Ensemble.Client
.NET 8 client for the Ensemble
peer-to-peer messaging daemon. Sibling to the Python SDK at
clients/python. Wraps the daemon's gRPC API with idiomatic
async C#: typed records, IAsyncDisposable lifetimes, CancellationToken
propagation, typed exceptions for structured daemon errors.
Install
dotnet add package Ensemble.Client
Targets net8.0. NuGet pulls Google.Protobuf, Grpc.Net.Client, and
Microsoft.Extensions.Logging.Abstractions transitively.
Quickstart — identity
using Ensemble.Client;
await using var client = new EnsembleClient("127.0.0.1:9099");
var identity = await client.GetIdentityAsync();
Console.WriteLine($"address: {identity.Address}");
EnsembleClient picks plaintext HTTP/2 (http://) for loopback /
unix-socket endpoints and TLS (https://) otherwise. Override by passing
a fully-qualified URI (http://..., https://..., or unix:///path).
Quickstart — register a service
using Ensemble.Client;
await using var client = new EnsembleClient("127.0.0.1:9099");
var manifest = ServiceManifest.NewBuilder("my-bot")
.Description("a friendly bot")
.Acl(ServiceAcl.Contacts)
.Transport(ServiceTransport.Chat)
.Build();
await using var svc = await client.RegisterServiceAsync(
manifest,
onEvent: async ev =>
{
if (ev is ServiceEvent.ChatMessage cm)
Console.WriteLine($"[{cm.FromAddr}] {cm.Text}");
else if (ev is ServiceEvent.ConnectionRequest cr)
await svc.AcceptConnectionAsync(cr.RequestId);
},
onError: err =>
{
// Daemon-side structured errors. err.AsException() returns a typed
// exception for known codes (payload_too_large, rate_limited) and
// null for unstructured stream errors.
Console.Error.WriteLine($"service error: {err.Message} (code={err.Code})");
return ValueTask.CompletedTask;
});
Console.WriteLine($"my service address: {svc.ServiceAddress}");
Console.WriteLine($"reachable at: {svc.Onion}");
await Task.Delay(TimeSpan.FromMinutes(5));
Quickstart — RPC transport + introductions
For services that host strangers (matchmaking, marketplaces, group chat
rooms), register under SERVICE_TRANSPORT_RPC to exchange raw protobuf
bytes instead of chat envelopes, and use IntroducePeersAsync to wire
two peers together with a daemon-attested provenance stamp:
var manifest = ServiceManifest.NewBuilder("pong-matchmaker")
.Acl(ServiceAcl.Public)
.Transport(ServiceTransport.Rpc)
.MaxPayloadBytes(256 * 1024)
.RateLimit(requestsPerMinute: 600, burst: 60)
.Build();
await using var svc = await client.RegisterServiceAsync(manifest, onEvent, onError);
// Pair two peers and introduce them.
await svc.IntroducePeersAsync(
toAddr: playerA,
otherAddr: playerB,
sessionId: Guid.NewGuid().ToString(),
expiresAtMs: DateTimeOffset.UtcNow.AddMinutes(2).ToUnixTimeMilliseconds(),
roleHint: "host",
payload: sessionTicket);
Both peers receive a ServiceEvent.PeerIntroduction carrying
FromServiceAddr (daemon-attested — receivers trust the local daemon's
word for provenance, no client-side verification needed),
the other peer's E-address, the session id, and the optional payload.
Typed exceptions
Daemon-side errors with a stable code map to typed exceptions you can
catch (or pattern-match on) without string-matching message:
err => {
if (err.AsException() is PayloadTooLargeException pe)
Console.Error.WriteLine($"too big — service cap is {pe.LimitBytes} bytes");
else if (err.AsException() is RateLimitedException rl)
Console.Error.WriteLine($"throttled — retry after {rl.RetryAfter}");
else
Console.Error.WriteLine($"service error: {err.Message}");
return ValueTask.CompletedTask;
}
Unknown future codes surface as a plain ServiceProtocolException with
the code preserved so callers can branch on it without code changes here.
Stream lifecycle
RegisteredService : IAsyncDisposable. Dispose to deregister:
DisposeAsynchalf-closes the bidi request stream → the daemon sees a clean EOF → the service is dropped from the registry.- A reader task runs for the lifetime of the registration. Mid-flight
transport errors (daemon restart, network blip) synthesise an
unstructured
ServiceError(Code="") toonErrorso the host process doesn't crash on a connectivity loss. - The dispose path waits up to 5 seconds for the reader to drain, then aborts. The bidi stream is always closed exactly once.
EnsembleClient : IAsyncDisposable owns one GrpcChannel. One client
can host many registered services and many independent Subscribe
streams; dispose the client to release the channel.
Repo-side notes (for contributors)
Source: clients/dotnet/ in the
Ensemble repo. The proto at
api/proto/ensemble.proto is included directly via <Protobuf Include=...>
in the csproj — no vendoring; codegen runs every build.
Layout
clients/dotnet/
├── Ensemble.Client.sln
├── global.json pins .NET 8 SDK
├── Ensemble.Client/
│ ├── EnsembleClient.cs basic RPCs (identity, contacts, peer dial)
│ ├── EnsembleClient.Services.cs partial — RegisterServiceAsync
│ ├── RegisteredService.cs bidi-stream handle (IAsyncDisposable)
│ ├── ServiceManifest.cs fluent ManifestBuilder + immutable manifest
│ ├── ServiceTypes.cs enums, ServiceError, ServiceEvent variants
│ ├── Records.cs Identity, Status, Contact, ConnectResult, DaemonEvent
│ └── Errors.cs exception hierarchy
└── tests/Ensemble.Client.Tests/
├── DaemonFixture.cs shared headless daemon per test class
├── IsolatedDaemon.cs one-off daemon for kill scenarios
├── ManifestBuilderTests.cs pure-CLR validation tests
├── BasicRpcTests.cs (SmokeTests.cs) — identity/contacts/subscribe
├── ServiceRegistrationTests.cs chat-transport bidi + lifecycle
├── Phase4PrimitiveTests.cs RPC bytes / introductions / typed errors
└── DaemonLifecycleTests.cs daemon-killed onError pathway
Prerequisites
- .NET 8 SDK —
global.jsonpins this. Newer SDKs side-by-side are fine. - Go 1.22 — needed to build the
ensembledaemon binary integration tests spawn. - Tor binary — system
/usr/bin/tor(Fedora:dnf install tor) or set$ENSEMBLE_TOR_PATHto an alternative. The daemon's auto-download path is currently 404ing on dist.torproject.org; tests bypass it by passing--tor-pathexplicitly.
Build & test
make dotnet-build # restore + build the sln
make dotnet-test # pure-CLR unit tests (no daemon)
make dotnet-test-integration # builds bin/ensemble (build-dev) then runs
# the integration suite against a real daemon
Integration tests spawn ensemble --headless --data-dir <tmp> --api-addr 127.0.0.1:<ephemeral> --tor-path <found> per test session and wait for
Tor bootstrap (~10s on a healthy network) before exercising RegisterService.
Generated code
Grpc.Tools writes protobuf and gRPC client stubs into
Ensemble.Client/obj/Debug/net8.0/ under the proto's default namespace
(Ensemble.Api). Consumers don't normally need to touch this — the
public SDK surface re-exports the relevant shapes as typed records.
Versioning
Ensemble.Client versions independently from the daemon. Semver. Releases
are cut from tags namespaced client-dotnet/v* so daemon and sibling-SDK
tags don't collide.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Google.Protobuf (>= 3.27.3)
- Grpc.Net.Client (>= 2.65.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.