From 69033acd74db84670f2f06473cabb35156cabdec Mon Sep 17 00:00:00 2001 From: EugeneTes Date: Tue, 25 Aug 2026 14:43:55 +0000 Subject: [PATCH] Add C# NATS-over-WebSocket example (core + JetStream replay) --- .gitignore | 4 ++ examples/csharp/NatsExample.csproj | 12 +++++ examples/csharp/Program.cs | 74 ++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 examples/csharp/NatsExample.csproj create mode 100644 examples/csharp/Program.cs diff --git a/.gitignore b/.gitignore index 5e28bb7..9d6e4b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ deploy.json + +# .NET build output (for examples/csharp) +bin/ +obj/ diff --git a/examples/csharp/NatsExample.csproj b/examples/csharp/NatsExample.csproj new file mode 100644 index 0000000..16cc3db --- /dev/null +++ b/examples/csharp/NatsExample.csproj @@ -0,0 +1,12 @@ + + + Exe + net8.0 + enable + enable + NatsExample + + + + + diff --git a/examples/csharp/Program.cs b/examples/csharp/Program.cs new file mode 100644 index 0000000..7412f13 --- /dev/null +++ b/examples/csharp/Program.cs @@ -0,0 +1,74 @@ +// Small NATS-over-WebSocket example for the deployed server. +// +// dotnet run --project examples/csharp +// +// Env vars: +// NATS_URL default: wss://nats.tes.gd +// NATS_USER default: admin +// NATS_PASSWORD required + +using System.Text; +using NATS.Client.Core; +using NATS.Client.JetStream; +using NATS.Client.JetStream.Models; + +var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "wss://nats.tes.gd"; +var user = Environment.GetEnvironmentVariable("NATS_USER") ?? "admin"; +var pass = Environment.GetEnvironmentVariable("NATS_PASSWORD") + ?? throw new InvalidOperationException("NATS_PASSWORD is required"); + +// 1. Connect. wss:// is understood natively; credentials go via AuthOpts. +var opts = new NatsOpts +{ + Url = url, + AuthOpts = new NatsAuthOpts { Username = user, Password = pass }, +}; +await using var nats = new NatsConnection(opts); +await nats.ConnectAsync(); +Console.WriteLine($"connected server={nats.ServerInfo?.Name} version={nats.ServerInfo?.Version} jetstream={nats.ServerInfo?.JetStreamAvailable}"); + +// 2. Core pub/sub — fire-and-forget events, no persistence. +var subject = $"demo.{Guid.NewGuid():N}"; +using var subCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); +var subTask = Task.Run(async () => +{ + await foreach (var msg in nats.SubscribeAsync(subject, cancellationToken: subCts.Token)) + { + Console.WriteLine($"core recv subject={msg.Subject} data=\"{msg.Data}\""); + subCts.Cancel(); + } +}); +await Task.Delay(200); // give the subscription a tick to register +await nats.PublishAsync(subject, "hello-core"); +try { await subTask; } catch (OperationCanceledException) { } + +// 3. JetStream — persistent events. Create a stream, publish a few, then read them back. +var js = new NatsJSContext(nats); +const string streamName = "DEMO_EVENTS"; +await js.CreateOrUpdateStreamAsync(new StreamConfig(streamName, new[] { "events.>" })); + +for (var i = 1; i <= 5; i++) +{ + var ack = await js.PublishAsync($"events.item.{i}", Encoding.UTF8.GetBytes($"payload-{i}")); + Console.WriteLine($"js publish subject=events.item.{i} seq={ack.Seq}"); +} + +// 4. Replay from the beginning with a fresh ephemeral consumer. +// DeliverPolicy.All ignores prior consumers — you get every message in the stream. +var consumer = await js.CreateOrUpdateConsumerAsync(streamName, new ConsumerConfig +{ + Name = $"replay-{Guid.NewGuid():N}", + DeliverPolicy = ConsumerConfigDeliverPolicy.All, + AckPolicy = ConsumerConfigAckPolicy.Explicit, +}); + +var fetchOpts = new NatsJSFetchOpts { MaxMsgs = 10, Expires = TimeSpan.FromSeconds(2) }; +await foreach (var msg in consumer.FetchAsync(opts: fetchOpts)) +{ + Console.WriteLine($"js replay seq={msg.Metadata?.Sequence.Stream} data=\"{Encoding.UTF8.GetString(msg.Data!)}\""); + await msg.AckAsync(); +} + +// 5. Clean up the demo stream so re-runs stay idempotent. +await js.DeleteStreamAsync(streamName); +Console.WriteLine("done");