diff --git a/docker/dotnet2/Dockerfile b/docker/dotnet2/Dockerfile index c655c9d8..38330e41 100644 --- a/docker/dotnet2/Dockerfile +++ b/docker/dotnet2/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:6.0 +FROM mcr.microsoft.com/dotnet/sdk:8.0 WORKDIR /src diff --git a/docker/dotnet2/example.csproj b/docker/dotnet2/example.csproj deleted file mode 100644 index 1fd74e82..00000000 --- a/docker/dotnet2/example.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - Exe - net6.0 - enable - - - - - - - diff --git a/dotnet2.sln b/dotnet2.sln new file mode 100644 index 00000000..d3ffefec --- /dev/null +++ b/dotnet2.sln @@ -0,0 +1,105 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "messaging-pub-sub", "examples\messaging\pub-sub\dotnet2\messaging-pub-sub.csproj", "{B8E154A8-79C7-403B-949B-83E084ED4B9D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{738E884F-DB8E-4F45-8B7E-D018C1384A0C}" + ProjectSection(SolutionItems) = preProject + docker\dotnet2\Dockerfile = docker\dotnet2\Dockerfile + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "messaging-protobuf", "examples\messaging\protobuf\dotnet2\messaging-protobuf.csproj", "{C94CC882-87D1-4155-A03A-82CE03FB4B36}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "messaging-concurrent", "examples\messaging\concurrent\dotnet2\messaging-concurrent.csproj", "{18393E5C-2816-42AF-9EEF-C9B761D20A53}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "messaging-iterating-multiple-subscriptions", "examples\messaging\iterating-multiple-subscriptions\dotnet2\messaging-iterating-multiple-subscriptions.csproj", "{39A3C1EA-903C-49FB-B732-2840DE482204}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "messaging-json", "examples\messaging\json\dotnet2\messaging-json.csproj", "{FE8671EC-CC41-4BD9-8EFB-38C1D417AC58}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "messaging-request-reply", "examples\messaging\request-reply\dotnet2\messaging-request-reply.csproj", "{AEF65329-7E66-46A3-8789-078F89954543}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "jetstream-limits-stream", "examples\jetstream\limits-stream\dotnet2\jetstream-limits-stream.csproj", "{EDDBAF02-74D2-463A-9A9D-2F570D7EDAD0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "jetstream-interest-stream", "examples\jetstream\interest-stream\dotnet2\jetstream-interest-stream.csproj", "{D93AE1C6-8AC0-48A3-A023-EDB3A7E6055D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "jetstream-workqueue-stream", "examples\jetstream\workqueue-stream\dotnet2\jetstream-workqueue-stream.csproj", "{C3CEAB63-E841-4585-BFF7-3A4BF9F78BE9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "jetstream-pull-consumer", "examples\jetstream\pull-consumer\dotnet2\jetstream-pull-consumer.csproj", "{7671C02D-9D21-4124-97DE-80EB55879377}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "jetstream-pull-consumer-limits", "examples\jetstream\pull-consumer-limits\dotnet2\jetstream-pull-consumer-limits.csproj", "{F70CB8D4-71C1-466A-AEF9-91F4DD37988C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "kv-intro", "examples\kv\intro\dotnet2\kv-intro.csproj", "{CFD0ECE5-7675-45D6-AFEA-532983C6D1FA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "os-intro", "examples\os\intro\dotnet2\os-intro.csproj", "{08EEA81C-62AF-464E-96A2-8F514B37CBE0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "services-intro", "examples\services\intro\dotnet2\services-intro.csproj", "{88CDA1EE-8CFF-4123-86F7-6FAE12CF01F9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B8E154A8-79C7-403B-949B-83E084ED4B9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8E154A8-79C7-403B-949B-83E084ED4B9D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8E154A8-79C7-403B-949B-83E084ED4B9D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8E154A8-79C7-403B-949B-83E084ED4B9D}.Release|Any CPU.Build.0 = Release|Any CPU + {C94CC882-87D1-4155-A03A-82CE03FB4B36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C94CC882-87D1-4155-A03A-82CE03FB4B36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C94CC882-87D1-4155-A03A-82CE03FB4B36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C94CC882-87D1-4155-A03A-82CE03FB4B36}.Release|Any CPU.Build.0 = Release|Any CPU + {18393E5C-2816-42AF-9EEF-C9B761D20A53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {18393E5C-2816-42AF-9EEF-C9B761D20A53}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18393E5C-2816-42AF-9EEF-C9B761D20A53}.Release|Any CPU.ActiveCfg = Release|Any CPU + {18393E5C-2816-42AF-9EEF-C9B761D20A53}.Release|Any CPU.Build.0 = Release|Any CPU + {39A3C1EA-903C-49FB-B732-2840DE482204}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39A3C1EA-903C-49FB-B732-2840DE482204}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39A3C1EA-903C-49FB-B732-2840DE482204}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39A3C1EA-903C-49FB-B732-2840DE482204}.Release|Any CPU.Build.0 = Release|Any CPU + {FE8671EC-CC41-4BD9-8EFB-38C1D417AC58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FE8671EC-CC41-4BD9-8EFB-38C1D417AC58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FE8671EC-CC41-4BD9-8EFB-38C1D417AC58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FE8671EC-CC41-4BD9-8EFB-38C1D417AC58}.Release|Any CPU.Build.0 = Release|Any CPU + {AEF65329-7E66-46A3-8789-078F89954543}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEF65329-7E66-46A3-8789-078F89954543}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEF65329-7E66-46A3-8789-078F89954543}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEF65329-7E66-46A3-8789-078F89954543}.Release|Any CPU.Build.0 = Release|Any CPU + {EDDBAF02-74D2-463A-9A9D-2F570D7EDAD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDDBAF02-74D2-463A-9A9D-2F570D7EDAD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDDBAF02-74D2-463A-9A9D-2F570D7EDAD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDDBAF02-74D2-463A-9A9D-2F570D7EDAD0}.Release|Any CPU.Build.0 = Release|Any CPU + {D93AE1C6-8AC0-48A3-A023-EDB3A7E6055D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D93AE1C6-8AC0-48A3-A023-EDB3A7E6055D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D93AE1C6-8AC0-48A3-A023-EDB3A7E6055D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D93AE1C6-8AC0-48A3-A023-EDB3A7E6055D}.Release|Any CPU.Build.0 = Release|Any CPU + {C3CEAB63-E841-4585-BFF7-3A4BF9F78BE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3CEAB63-E841-4585-BFF7-3A4BF9F78BE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3CEAB63-E841-4585-BFF7-3A4BF9F78BE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3CEAB63-E841-4585-BFF7-3A4BF9F78BE9}.Release|Any CPU.Build.0 = Release|Any CPU + {7671C02D-9D21-4124-97DE-80EB55879377}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7671C02D-9D21-4124-97DE-80EB55879377}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7671C02D-9D21-4124-97DE-80EB55879377}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7671C02D-9D21-4124-97DE-80EB55879377}.Release|Any CPU.Build.0 = Release|Any CPU + {F70CB8D4-71C1-466A-AEF9-91F4DD37988C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F70CB8D4-71C1-466A-AEF9-91F4DD37988C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F70CB8D4-71C1-466A-AEF9-91F4DD37988C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F70CB8D4-71C1-466A-AEF9-91F4DD37988C}.Release|Any CPU.Build.0 = Release|Any CPU + {CFD0ECE5-7675-45D6-AFEA-532983C6D1FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CFD0ECE5-7675-45D6-AFEA-532983C6D1FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CFD0ECE5-7675-45D6-AFEA-532983C6D1FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CFD0ECE5-7675-45D6-AFEA-532983C6D1FA}.Release|Any CPU.Build.0 = Release|Any CPU + {08EEA81C-62AF-464E-96A2-8F514B37CBE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08EEA81C-62AF-464E-96A2-8F514B37CBE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08EEA81C-62AF-464E-96A2-8F514B37CBE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08EEA81C-62AF-464E-96A2-8F514B37CBE0}.Release|Any CPU.Build.0 = Release|Any CPU + {88CDA1EE-8CFF-4123-86F7-6FAE12CF01F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {88CDA1EE-8CFF-4123-86F7-6FAE12CF01F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {88CDA1EE-8CFF-4123-86F7-6FAE12CF01F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {88CDA1EE-8CFF-4123-86F7-6FAE12CF01F9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/examples/jetstream/interest-stream/dotnet2/Main.cs b/examples/jetstream/interest-stream/dotnet2/Main.cs new file mode 100644 index 00000000..8d029f48 --- /dev/null +++ b/examples/jetstream/interest-stream/dotnet2/Main.cs @@ -0,0 +1,176 @@ +// Install NuGet packages `NATS.Net` and `Microsoft.Extensions.Logging.Console`. +using Microsoft.Extensions.Logging; +using NATS.Client.Core; +using NATS.Client.JetStream; +using NATS.Client.JetStream.Models; + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("NATS-by-Example"); + +// `NATS_URL` environment variable can be used to pass the locations of the NATS servers. +var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222"; + +// Connect to NATS server. Since connection is disposable at the end of our scope we should flush +// our buffers and close connection cleanly. +var opts = new NatsOpts +{ + Url = url, + LoggerFactory = loggerFactory, + Name = "NATS-by-Example", +}; +await using var nats = new NatsConnection(opts); + +// Create `JetStream Context` which provides methods to create +// streams and consumers as well as convenience methods for publishing +// to streams and consuming messages from the streams. +var js = new NatsJSContext(nats); + +// ### Creating the stream +// Define the stream configuration, specifying `InterestPolicy` for retention, and +// create the stream. +var config = new StreamConfig(name: "EVENTS", subjects: new[] { "events.>" }) +{ + Retention = StreamConfigRetention.Interest, +}; + +var stream = await js.CreateStreamAsync(config); + +// To demonstrate the base case behavior of the stream without any consumers, we +// will publish a few messages to the stream. +await js.PublishAsync(subject: "events.page_loaded", data: null); +await js.PublishAsync(subject: "events.mouse_clicked", data: null); +var ack = await js.PublishAsync(subject: "events.input_focused", data: null); +logger.LogInformation("Published 3 messages"); + +// We confirm that all three messages were published and the last message sequence +// is 3. +logger.LogInformation("Last message seq: {Seq}", ack.Seq); + +// Checking out the stream info, notice how zero messages are present in +// the stream, but the `last_seq` is 3 which matches the last ACKed +// publish sequence above. Also notice that the `first_seq` is one greater +// which behaves as a sentinel value indicating the stream is empty. This +// sequence has not been assigned to a message yet, but can be interpreted +// as _no messages available_ in this context. +logger.LogInformation("# Stream info without any consumers"); +await PrintStreamStateAsync(stream); + +// ### Adding a consumer Now let's add a pull consumer and publish a few +// more messages. Also note that we are _only_ creating the consumer and +// have not yet started consuming the messages. This is only to point out +// that a it is not _required_ to be actively consuming messages to show +// _interest_, but it is the presence of a consumer which the stream cares +// about to determine retention of messages. [pull](/examples/jetstream/pull-consumer/dotnet2) +var consumer = await stream.CreateConsumerAsync(new ConsumerConfig("processor-1") +{ + AckPolicy = ConsumerConfigAckPolicy.Explicit, +}); + +await js.PublishAsync(subject: "events.page_loaded", data: null); +await js.PublishAsync(subject: "events.mouse_clicked", data: null); + +// If we inspect the stream info again, we will notice a few differences. +// It shows two messages (which we expect) and the first and last sequences +// corresponding to the two messages we just published. We also see that +// the `consumer_count` is now one. +logger.LogInformation("# Stream info with one consumer"); +await PrintStreamStateAsync(stream); + +await foreach (var msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 2 })) +{ + await msg.AckAsync(new AckOpts { DoubleAck = true }); +} + +// What do we expect in the stream? No messages and the `first_seq` has been set to +// the _next_ sequence number like in the base case. +// ☝️ As a quick aside on that second ack, We are using `AckSync` here for this +// example to ensure the stream state has been synced up for this subsequent +// retrieval. +logger.LogInformation("# Stream info with one consumer and acked messages"); +await PrintStreamStateAsync(stream); + +// ### Two or more consumers +// Since each consumer represents a separate _view_ over a stream, we would expect +// that if messages were processed by one consumer, but not the other, the messages +// would be retained. This is indeed the case. +var consumer2 = await stream.CreateConsumerAsync(new ConsumerConfig("processor-2") +{ + AckPolicy = ConsumerConfigAckPolicy.Explicit, +}); + +await js.PublishAsync(subject: "events.page_loaded", data: null); +await js.PublishAsync(subject: "events.mouse_clicked", data: null); + +// Here we fetch 2 messages for `processor-2`. There are two observations to +// make here. First the fetched messages are the latest two messages that +// were published just above and not any prior messages since these were +// already deleted from the stream. This should be apparent now, but this +// reinforces that a _late_ consumer cannot retroactively show interest. The +// second point is that the stream info shows that the latest two messages +// are still present in the stream. This is also expected since the first +// consumer had not yet processed them. +var msgMetas = new List(); +await foreach (var msg in consumer2.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 2 })) +{ + await msg.AckAsync(new AckOpts { DoubleAck = true }); + if (msg.Metadata is { } metadata) + { + msgMetas.Add(metadata); + } +} + +logger.LogInformation("msg seqs {Seq1} and {Seq2}", msgMetas[0].Sequence.Stream, msgMetas[1].Sequence.Stream); + +logger.LogInformation("# Stream info with two consumers, but only one set of acked messages"); +await PrintStreamStateAsync(stream); + +// Fetching and ack'ing from the first consumer subscription will result in the messages +// being deleted. +await foreach (var msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 2 })) +{ + await msg.AckAsync(new AckOpts { DoubleAck = true }); +} + +logger.LogInformation("# Stream info with two consumers having both acked"); +await PrintStreamStateAsync(stream); + +// A final callout is that _interest_ respects the `FilterSubject` on a consumer. +// For example, if a consumer defines a filter only for `events.mouse_clicked` events +// then it won't be considered _interested_ in events such as `events.input_focused`. +await stream.CreateConsumerAsync(new ConsumerConfig("processor-3") +{ + AckPolicy = ConsumerConfigAckPolicy.Explicit, + FilterSubject = "events.mouse_clicked", +}); + +await js.PublishAsync(subject: "events.input_focused", data: null); + +// Fetch and `Terminate` (also works) and ack from the first consumers that _do_ have interest. +await foreach (var msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 1 })) +{ + await msg.AckTerminateAsync(); +} + +await foreach (var msg in consumer2.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 1 })) +{ + await msg.AckAsync(new AckOpts { DoubleAck = true }); +} + +logger.LogInformation("# Stream info with three consumers with interest from two"); +await PrintStreamStateAsync(stream); + +// That's it! +logger.LogInformation("Bye!"); + +async Task PrintStreamStateAsync(INatsJSStream jsStream) +{ + await jsStream.RefreshAsync(); + var state = jsStream.Info.State; + logger.LogInformation( + "Stream has messages:{Messages} first:{FirstSeq} last:{LastSeq} consumer_count:{ConsumerCount} num_subjects:{NumSubjects}", + state.Messages, + state.FirstSeq, + state.LastSeq, + state.ConsumerCount, + state.NumSubjects); +} diff --git a/examples/jetstream/interest-stream/dotnet2/jetstream-interest-stream.csproj b/examples/jetstream/interest-stream/dotnet2/jetstream-interest-stream.csproj new file mode 100644 index 00000000..2eeef688 --- /dev/null +++ b/examples/jetstream/interest-stream/dotnet2/jetstream-interest-stream.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + + diff --git a/examples/jetstream/interest-stream/dotnet2/output.cast b/examples/jetstream/interest-stream/dotnet2/output.cast new file mode 100644 index 00000000..c1515752 --- /dev/null +++ b/examples/jetstream/interest-stream/dotnet2/output.cast @@ -0,0 +1,17 @@ +{"version": 2, "width": 104, "height": 51, "timestamp": 1700974702, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: jetstream/interest-stream/dotnet2"} +[2.668687, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Try to connect NATS nats://nats:4222\r\n"] +[2.705731, "o", "info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005]\r\n Received server info: ServerInfo { Id = NDMT742URUINNER4MQPYJSWSTLJCQ2DYTPPGNBTX4JPLUM3K4D3XVTZL, Name = NDMT742URUINNER4MQPYJSWSTLJCQ2DYTPPGNBTX4JPLUM3K4D3XVTZL, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.144.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False }\r\n"] +[2.725147, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Connect succeed NATS-by-Example, NATS nats://nats:4222\r\n"] +[2.81112, "o", "info: NATS-by-Example[0]\r\n Published 3 messages\r\ninfo: NATS-by-Example[0]\r\n Last message seq: 3\r\ninfo: NATS-by-Example[0]\r\n # Stream info without any consumers\r\n"] +[2.816307, "o", "info: NATS-by-Example[0]\r\n Stream has messages:0 first:4 last:3 consumer_count:0 num_subjects:0\r\n"] +[2.844571, "o", "info: NATS-by-Example[0]\r\n # Stream info with one consumer\r\n"] +[2.846404, "o", "info: NATS-by-Example[0]\r\n Stream has messages:2 first:4 last:5 consumer_count:1 num_subjects:2\r\n"] +[2.869248, "o", "info: NATS-by-Example[0]\r\n # Stream info with one consumer and acked messages\r\n"] +[2.869962, "o", "info: NATS-by-Example[0]\r\n Stream has messages:0 first:6 last:5 consumer_count:1 num_subjects:0\r\n"] +[2.876819, "o", "info: NATS-by-Example[0]\r\n msg seqs 6 and 7\r\ninfo: NATS-by-Example[0]\r\n # Stream info with two consumers, but only one set of acked messages\r\n"] +[2.87735, "o", "info: NATS-by-Example[0]\r\n Stream has messages:2 first:6 last:7 consumer_count:2 num_subjects:2\r\n"] +[2.878722, "o", "info: NATS-by-Example[0]\r\n # Stream info with two consumers having both acked\r\n"] +[2.879509, "o", "info: NATS-by-Example[0]\r\n Stream has messages:0 first:8 last:7 consumer_count:2 num_subjects:0\r\n"] +[2.88285, "o", "info: NATS-by-Example[0]\r\n # Stream info with three consumers with interest from two\r\n"] +[2.883439, "o", "info: NATS-by-Example[0]\r\n Stream has messages:0 first:9 last:8 consumer_count:3 num_subjects:0\r\ninfo: NATS-by-Example[0]\r\n Bye!\r\n"] +[2.884323, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Disposing connection NATS-by-Example\r\n"] diff --git a/examples/jetstream/interest-stream/dotnet2/output.txt b/examples/jetstream/interest-stream/dotnet2/output.txt new file mode 100644 index 00000000..a4d1992f --- /dev/null +++ b/examples/jetstream/interest-stream/dotnet2/output.txt @@ -0,0 +1,40 @@ +info: NATS.Client.Core.NatsConnection[1001] + Try to connect NATS nats://nats:4222 +info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005] + Received server info: ServerInfo { Id = NDMT742URUINNER4MQPYJSWSTLJCQ2DYTPPGNBTX4JPLUM3K4D3XVTZL, Name = NDMT742URUINNER4MQPYJSWSTLJCQ2DYTPPGNBTX4JPLUM3K4D3XVTZL, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.144.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False } +info: NATS.Client.Core.NatsConnection[1001] + Connect succeed NATS-by-Example, NATS nats://nats:4222 +info: NATS-by-Example[0] + Published 3 messages +info: NATS-by-Example[0] + Last message seq: 3 +info: NATS-by-Example[0] + # Stream info without any consumers +info: NATS-by-Example[0] + Stream has messages:0 first:4 last:3 consumer_count:0 num_subjects:0 +info: NATS-by-Example[0] + # Stream info with one consumer +info: NATS-by-Example[0] + Stream has messages:2 first:4 last:5 consumer_count:1 num_subjects:2 +info: NATS-by-Example[0] + # Stream info with one consumer and acked messages +info: NATS-by-Example[0] + Stream has messages:0 first:6 last:5 consumer_count:1 num_subjects:0 +info: NATS-by-Example[0] + msg seqs 6 and 7 +info: NATS-by-Example[0] + # Stream info with two consumers, but only one set of acked messages +info: NATS-by-Example[0] + Stream has messages:2 first:6 last:7 consumer_count:2 num_subjects:2 +info: NATS-by-Example[0] + # Stream info with two consumers having both acked +info: NATS-by-Example[0] + Stream has messages:0 first:8 last:7 consumer_count:2 num_subjects:0 +info: NATS-by-Example[0] + # Stream info with three consumers with interest from two +info: NATS-by-Example[0] + Stream has messages:0 first:9 last:8 consumer_count:3 num_subjects:0 +info: NATS-by-Example[0] + Bye! +info: NATS.Client.Core.NatsConnection[1001] + Disposing connection NATS-by-Example diff --git a/examples/jetstream/limits-stream/dotnet2/Main.cs b/examples/jetstream/limits-stream/dotnet2/Main.cs new file mode 100644 index 00000000..97f4c838 --- /dev/null +++ b/examples/jetstream/limits-stream/dotnet2/Main.cs @@ -0,0 +1,110 @@ +// Install NuGet packages `NATS.Net` and `Microsoft.Extensions.Logging.Console`. +using Microsoft.Extensions.Logging; +using NATS.Client.Core; +using NATS.Client.JetStream; +using NATS.Client.JetStream.Models; + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("NATS-by-Example"); + +// `NATS_URL` environment variable can be used to pass the locations of the NATS servers. +var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222"; + +// Connect to NATS server. Since connection is disposable at the end of our scope we should flush +// our buffers and close connection cleanly. +var opts = new NatsOpts +{ + Url = url, + LoggerFactory = loggerFactory, + Name = "NATS-by-Example", +}; +await using var nats = new NatsConnection(opts); + +// Create `JetStream Context` which provides methods to create +// streams and consumers as well as convenience methods for publishing +// to streams and consuming messages from the streams. +var js = new NatsJSContext(nats); + +// We will declare the initial stream configuration by specifying +// the name and subjects. Stream names are commonly uppercase to +// visually differentiate them from subjects, but this is not required. +// A stream can bind one or more subjects which almost always include +// wildcards. In addition, no two streams can have overlapping subjects +// otherwise the primary messages would be persisted twice. +var config = new StreamConfig(name: "EVENTS", subjects: new [] { "events.>" }); + +// JetStream provides both file and in-memory storage options. For +// durability of the stream data, file storage must be chosen to +// survive crashes and restarts. This is the default for the stream, +// but we can still set it explicitly. +config.Storage = StreamConfigStorage.File; + +// Finally, let's add/create the stream with the default (no) limits. +var stream = await js.CreateStreamAsync(config); + +// Let's publish a few messages which are received by the stream since +// they match the subject bound to the stream. The `js.Publish` method +// is a convenience for sending a `Request` and waiting for the +// acknowledgement. +for (var i = 0; i < 2; i++) +{ + await js.PublishAsync(subject: "events.page_loaded", data: null); + await js.PublishAsync(subject: "events.mouse_clicked", data: null); + await js.PublishAsync(subject: "events.mouse_clicked", data: null); + await js.PublishAsync(subject: "events.page_loaded", data: null); + await js.PublishAsync(subject: "events.mouse_clicked", data: null); + await js.PublishAsync(subject: "events.input_focused", data: null); + logger.LogInformation("Published 6 messages"); +} + +// Checking out the stream info, we can see how many messages we +// have. +await PrintStreamStateAsync(stream); + +var configUpdate = new StreamUpdateRequest { Name = config.Name, Subjects = config.Subjects, Storage = config.Storage }; + +// Stream configuration can be dynamically changed. For example, +// we can set the max messages limit to 10 and it will truncate the +// two initial events in the stream. +configUpdate.MaxMsgs = 10; +await js.UpdateStreamAsync(configUpdate); +logger.LogInformation("set max messages to 10"); + +// Checking out the info, we see there are now 10 messages and the +// first sequence and timestamp are based on the third message. +await PrintStreamStateAsync(stream); + +// Limits can be combined and whichever one is reached, it will +// be applied to truncate the stream. For example, let's set a +// maximum number of bytes for the stream. +configUpdate.MaxBytes = 300; +await js.UpdateStreamAsync(configUpdate); +logger.LogInformation("set max bytes to 300"); + +// Inspecting the stream info we now see more messages have been +// truncated to ensure the size is not exceeded. +await PrintStreamStateAsync(stream); + +// Finally, for the last primary limit, we can set the max age. +configUpdate.MaxAge = (long)TimeSpan.FromSeconds(1).TotalNanoseconds; +await js.UpdateStreamAsync(configUpdate); +logger.LogInformation("set max age to one second"); + +// Looking at the stream info, we still see all the messages.. +await PrintStreamStateAsync(stream); + +// until a second passes. +logger.LogInformation("sleeping one second..."); +await Task.Delay(TimeSpan.FromSeconds(1)); + +await PrintStreamStateAsync(stream); + +// That's it! +logger.LogInformation("Bye!"); + +async Task PrintStreamStateAsync(INatsJSStream jsStream) +{ + await jsStream.RefreshAsync(); + var state = jsStream.Info.State; + logger.LogInformation("Stream has {Messages} messages using {Bytes} bytes", state.Messages, state.Bytes); +} diff --git a/examples/jetstream/limits-stream/dotnet2/jetstream-limits-stream.csproj b/examples/jetstream/limits-stream/dotnet2/jetstream-limits-stream.csproj new file mode 100644 index 00000000..2eeef688 --- /dev/null +++ b/examples/jetstream/limits-stream/dotnet2/jetstream-limits-stream.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + + diff --git a/examples/jetstream/limits-stream/dotnet2/output.cast b/examples/jetstream/limits-stream/dotnet2/output.cast new file mode 100644 index 00000000..6623d37e --- /dev/null +++ b/examples/jetstream/limits-stream/dotnet2/output.cast @@ -0,0 +1,15 @@ +{"version": 2, "width": 104, "height": 51, "timestamp": 1700974688, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: jetstream/limits-stream/dotnet2"} +[5.327579, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Try to connect NATS nats://nats:4222\r\n"] +[5.363965, "o", "info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005]\r\n Received server info: ServerInfo { Id = NCK4CIW6RWLVL4BHIPQ2QHQHEE62XZGXDY2EHCLMJHHKUKFWJLLB7SDR, Name = NCK4CIW6RWLVL4BHIPQ2QHQHEE62XZGXDY2EHCLMJHHKUKFWJLLB7SDR, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.128.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False }\r\n"] +[5.382762, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Connect succeed NATS-by-Example, NATS nats://nats:4222\r\n"] +[5.470934, "o", "info: NATS-by-Example[0]\r\n Published 6 messages\r\n"] +[5.473316, "o", "info: NATS-by-Example[0]\r\n Published 6 messages\r\n"] +[5.478215, "o", "info: NATS-by-Example[0]\r\n Stream has 12 messages using 592 bytes\r\n"] +[5.486071, "o", "info: NATS-by-Example[0]\r\n set max messages to 10\r\n"] +[5.487131, "o", "info: NATS-by-Example[0]\r\n Stream has 10 messages using 494 bytes\r\n"] +[5.48825, "o", "info: NATS-by-Example[0]\r\n set max bytes to 300\r\n"] +[5.489092, "o", "info: NATS-by-Example[0]\r\n Stream has 6 messages using 296 bytes\r\n"] +[5.489906, "o", "info: NATS-by-Example[0]\r\n set max age to one second\r\n"] +[5.490672, "o", "info: NATS-by-Example[0]\r\n Stream has 6 messages using 296 bytes\r\ninfo: NATS-by-Example[0]\r\n sleeping one second...\r\n"] +[6.492466, "o", "info: NATS-by-Example[0]\r\n Stream has 0 messages using 0 bytes\r\ninfo: NATS-by-Example[0]\r\n Bye!\r\n"] +[6.493341, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Disposing connection NATS-by-Example\r\n"] diff --git a/examples/jetstream/limits-stream/dotnet2/output.txt b/examples/jetstream/limits-stream/dotnet2/output.txt new file mode 100644 index 00000000..1c16e04a --- /dev/null +++ b/examples/jetstream/limits-stream/dotnet2/output.txt @@ -0,0 +1,32 @@ +info: NATS.Client.Core.NatsConnection[1001] + Try to connect NATS nats://nats:4222 +info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005] + Received server info: ServerInfo { Id = NCK4CIW6RWLVL4BHIPQ2QHQHEE62XZGXDY2EHCLMJHHKUKFWJLLB7SDR, Name = NCK4CIW6RWLVL4BHIPQ2QHQHEE62XZGXDY2EHCLMJHHKUKFWJLLB7SDR, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.128.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False } +info: NATS.Client.Core.NatsConnection[1001] + Connect succeed NATS-by-Example, NATS nats://nats:4222 +info: NATS-by-Example[0] + Published 6 messages +info: NATS-by-Example[0] + Published 6 messages +info: NATS-by-Example[0] + Stream has 12 messages using 592 bytes +info: NATS-by-Example[0] + set max messages to 10 +info: NATS-by-Example[0] + Stream has 10 messages using 494 bytes +info: NATS-by-Example[0] + set max bytes to 300 +info: NATS-by-Example[0] + Stream has 6 messages using 296 bytes +info: NATS-by-Example[0] + set max age to one second +info: NATS-by-Example[0] + Stream has 6 messages using 296 bytes +info: NATS-by-Example[0] + sleeping one second... +info: NATS-by-Example[0] + Stream has 0 messages using 0 bytes +info: NATS-by-Example[0] + Bye! +info: NATS.Client.Core.NatsConnection[1001] + Disposing connection NATS-by-Example diff --git a/examples/jetstream/pull-consumer-limits/dotnet2/Main.cs b/examples/jetstream/pull-consumer-limits/dotnet2/Main.cs new file mode 100644 index 00000000..0de4a167 --- /dev/null +++ b/examples/jetstream/pull-consumer-limits/dotnet2/Main.cs @@ -0,0 +1,247 @@ +// Install NuGet packages `NATS.Net` and `Microsoft.Extensions.Logging.Console`. + +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using NATS.Client.Core; +using NATS.Client.JetStream; +using NATS.Client.JetStream.Models; + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("NATS-by-Example"); + +// `NATS_URL` environment variable can be used to pass the locations of the NATS servers. +var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222"; + +// Connect to NATS server. Since connection is disposable at the end of our scope we should flush +// our buffers and close connection cleanly. +var opts = new NatsOpts +{ + Url = url, + LoggerFactory = loggerFactory, + Name = "NATS-by-Example", +}; +await using var nats = new NatsConnection(opts); + +// Access JetStream for managing streams and consumers as well as for +// publishing and consuming messages to and from the stream. +var js = new NatsJSContext(nats); + +var streamName = "EVENTS"; + +// Declare a simple [limits-based stream](/examples/jetstream/limits-stream/dotnet2/). +var stream = await js.CreateStreamAsync(new StreamConfig(streamName, new[] { "events.>" })); + +// Define a basic pull consumer without any limits and a short ack wait +// time for the purpose of this example. These default options will be +// reused when we update the consumer to show-case various limits. +// If you haven't seen the first [pull consumer][1] example yet, check +// that out first! +// [1]: /examples/jetstream/pull-consumer/dotnet2/ +var consumerName = "processor"; +var ackWait = TimeSpan.FromSeconds(10); +var ackPolicy = ConsumerConfigAckPolicy.Explicit; +var maxWaiting = 1; + +// One quick note. This example show cases how consumer configuration +// can be changed on-demand. This one exception is `MaxWaiting` which +// cannot be updated on a consumer as of now. This must be set up front +// when the consumer is created. +var consumer = await stream.CreateConsumerAsync(new ConsumerConfig(consumerName) +{ + AckPolicy = ackPolicy, + AckWait = (long)ackWait.TotalNanoseconds, + MaxWaiting = maxWaiting, +}); + +// ### Max in-flight messages +// The first limit to explore is the max in-flight messages. This +// will limit how many un-acked in-flight messages there are across +// all subscriptions bound to this consumer. +// We can update the consumer config on-the-fly with the +// `MaxAckPending` setting. +logger.LogInformation("--- max in-flight messages (n=1) ---"); + +await stream.CreateConsumerAsync(new ConsumerConfig(consumerName) +{ + AckPolicy = ackPolicy, + AckWait = (long)ackWait.TotalNanoseconds, + MaxWaiting = maxWaiting, + MaxAckPending = 1, +}); + +// Let's publish a couple events for this section. +await js.PublishAsync(subject: "events.1", data: "event-data-1"); +await js.PublishAsync(subject: "events.2", data: "event-data-2"); + +// We can request a larger batch size, but we will only get one +// back since only one can be un-acked at any given time. This +// essentially forces serial processing messages for a pull consumer. +var received = new List>(); +await foreach (NatsJSMsg msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 3, Expires = TimeSpan.FromSeconds(3) })) +{ + received.Add(msg); +} +logger.LogInformation("Requested 3, got {Count}", received.Count); + + +// This limit becomes more apparent with the second fetch which would +// timeout without any messages since we haven't acked the previous one yet. +var received2 = new List>(); +await foreach (NatsJSMsg msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 1, Expires = TimeSpan.FromSeconds(1) })) +{ + received2.Add(msg); +} +logger.LogInformation("Requested 1, got {Count}", received2.Count); + +// Let's ack it and then try another fetch. +await received[0].AckAsync(); + +// It works this time! +await foreach (NatsJSMsg msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 1 })) +{ + received2.Add(msg); + await msg.AckAsync(); +} +logger.LogInformation("Requested 1, got {Count}", received2.Count); + +// ### Max fetch batch size +// This one limits the max batch size any one fetch can receive. This +// can be used to keep the fetches to a reasonable size. +logger.LogInformation("--- max fetch batch size (n=2) ---"); + +consumer = await stream.CreateConsumerAsync(new ConsumerConfig(consumerName) +{ + AckPolicy = ackPolicy, + AckWait = (long)ackWait.TotalNanoseconds, + MaxWaiting = maxWaiting, + MaxBatch = 2, +}); + +// Publish a couple events for this section... +await js.PublishAsync(subject: "events.1", data: "hello"); +await js.PublishAsync(subject: "events.2", data: "world"); + + +// If a batch size is larger than the limit, it is considered an error. +// Because Fetch is non-blocking, we need to wait for the operation to +// complete before checking the error. +await foreach (var msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 10, Expires = TimeSpan.FromSeconds(1) })) +{ +} + +// Using the max batch size (or less) will, of course, work. +var fetchCount = 0; +await foreach (var msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 2 })) +{ + logger.LogInformation("Received {Data}", msg.Data); + await msg.AckAsync(); + fetchCount++; +} +logger.LogInformation("Requested 2, got {Count}", fetchCount); + +// ### Max waiting requests +// The next limit defines the maximum number of fetch requests +// that are all waiting in parallel to receive messages. This +// prevents building up too many requests that the server will +// have to distribute to for a given consumer. +logger.LogInformation("--- max waiting requests (n=1) ---"); + +// Since `MaxWaiting` was already set to 1 when the consumer +// was created, this is a no-op. +await stream.CreateConsumerAsync(new ConsumerConfig(consumerName) +{ + AckPolicy = ackPolicy, + AckWait = (long)ackWait.TotalNanoseconds, + MaxWaiting = maxWaiting, +}); + +// Publish lots of events to trigger 409 Exceeded MaxWaiting. +for (int i = 0; i < 1000; i++) +{ + await js.PublishAsync(subject: "events.x", data: "event-data"); +} + +var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); +await foreach (var msg in consumer.ConsumeAsync(opts: new NatsJSConsumeOpts { MaxMsgs = 100 }, cancellationToken: cts.Token)) +{ +} + +await foreach (var msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 1000 })) +{ + await msg.AckAsync(); +} + +// ### Max fetch timeout +// Normally each fetch call can specify it's own max wait timeout, i.e. +// how long the client wants to wait to receive at least one message. +// It may be desirable to limit defined on the consumer to prevent +// requests waiting too long for messages. +logger.LogInformation("--- max fetch timeout (d=1s) ---"); + +await stream.CreateConsumerAsync(new ConsumerConfig(consumerName) +{ + AckPolicy = ackPolicy, + AckWait = (long)ackWait.TotalNanoseconds, + MaxWaiting = maxWaiting, + MaxExpires = (long)TimeSpan.FromSeconds(1).TotalNanoseconds, +}); + +// Using a max wait equal or less than `MaxRequestExpires` not return an +// error and return expected number of messages (zero in that case, since +// there are no more). +var fetchStopwatch = Stopwatch.StartNew(); +fetchCount = 0; +await foreach (var msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 10, Expires = TimeSpan.FromSeconds(1) })) +{ + fetchCount++; +} +logger.LogInformation("Got {Count} messages in {Elapsed}", fetchCount, fetchStopwatch.Elapsed); + +// However, trying to use a longer timeout you'd get a warning `409 Exceeded MaxRequestExpires of 1s` +fetchStopwatch.Restart(); +fetchCount = 0; +await foreach (var msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 10, Expires = TimeSpan.FromSeconds(5) })) +{ + fetchCount++; +} +logger.LogInformation("Got {Count} messages in {Elapsed}", fetchCount, fetchStopwatch.Elapsed); + + +// ### Max total bytes per fetch +// +logger.LogInformation("--- max total bytes per fetch (n=4) ---"); +/* fmt.Println("\n--- max total bytes per fetch (n=4) ---") + + stream.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{ + Name: consumerName, + AckPolicy: ackPolicy, + AckWait: ackWait, + MaxWaiting: maxWaiting, + MaxRequestMaxBytes: 3, + }) + + js.Publish(ctx, "events.3", []byte("hola")) + js.Publish(ctx, "events.4", []byte("again")) + + msgs, _ = cons.FetchBytes(4) + for range msgs.Messages() { + } + fmt.Printf("%s\n", msgs.Error()) +*/ +await stream.CreateConsumerAsync(new ConsumerConfig(consumerName) +{ + AckPolicy = ackPolicy, + AckWait = (long)ackWait.TotalNanoseconds, + MaxWaiting = maxWaiting, + MaxBytes = 3, +}); + +await js.PublishAsync(subject: "events.1", data: "hi"); +await js.PublishAsync(subject: "events.2", data: "again"); + +await foreach (var msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxBytes = 4, Expires = TimeSpan.FromSeconds(1) })) +{ +} + +// That's it! +logger.LogInformation("Bye!"); diff --git a/examples/jetstream/pull-consumer-limits/dotnet2/jetstream-pull-consumer-limits.csproj b/examples/jetstream/pull-consumer-limits/dotnet2/jetstream-pull-consumer-limits.csproj new file mode 100644 index 00000000..2eeef688 --- /dev/null +++ b/examples/jetstream/pull-consumer-limits/dotnet2/jetstream-pull-consumer-limits.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + + diff --git a/examples/jetstream/pull-consumer-limits/dotnet2/output.cast b/examples/jetstream/pull-consumer-limits/dotnet2/output.cast new file mode 100644 index 00000000..6868d587 --- /dev/null +++ b/examples/jetstream/pull-consumer-limits/dotnet2/output.cast @@ -0,0 +1,19 @@ +{"version": 2, "width": 104, "height": 51, "timestamp": 1700974736, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: jetstream/pull-consumer-limits/dotnet2"} +[2.729573, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Try to connect NATS nats://nats:4222\r\n"] +[2.767317, "o", "info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005]\r\n Received server info: ServerInfo { Id = NBACXYPZZOMGMAHOMI6SSNE6UXXYY23YUQV5E7CRYWFM7EP777OFIIVV, Name = NBACXYPZZOMGMAHOMI6SSNE6UXXYY23YUQV5E7CRYWFM7EP777OFIIVV, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.192.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False }\r\n"] +[2.784956, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Connect succeed NATS-by-Example, NATS nats://nats:4222\r\n"] +[2.891949, "o", "info: NATS-by-Example[0]\r\n --- max in-flight messages (n=1) ---\r\n"] +[5.913788, "o", "info: NATS-by-Example[0]\r\n Requested 3, got 1\r\n"] +[6.915867, "o", "info: NATS-by-Example[0]\r\n Requested 1, got 0\r\n"] +[6.920842, "o", "info: NATS-by-Example[0]\r\n Requested 1, got 1\r\ninfo: NATS-by-Example[0]\r\n --- max fetch batch size (n=2) ---\r\n"] +[6.925302, "o", "warn: NATS.Client.JetStream.Internal.NatsJSFetch[2005]\r\n Unhandled protocol message: 409 Exceeded MaxRequestBatch of 2\r\n"] +[7.927806, "o", "info: NATS-by-Example[0]\r\n Received hello\r\ninfo: NATS-by-Example[0]\r\n Received world\r\n"] +[7.928836, "o", "info: NATS-by-Example[0]\r\n Requested 2, got 2\r\n"] +[7.928922, "o", "info: NATS-by-Example[0]\r\n --- max waiting requests (n=1) ---\r\n"] +[18.266815, "o", "info: NATS-by-Example[0]\r\n --- max fetch timeout (d=1s) ---\r\n"] +[19.270871, "o", "info: NATS-by-Example[0]\r\n Got 0 messages in 00:00:01.0012871\r\n"] +[19.271447, "o", "warn: NATS.Client.JetStream.Internal.NatsJSFetch[2005]\r\n Unhandled protocol message: 409 Exceeded MaxRequestExpires of 1s\r\n"] +[24.268231, "o", "info: NATS-by-Example[0]\r\n Got 0 messages in 00:00:04.9971654\r\ninfo: NATS-by-Example[0]\r\n --- max total bytes per fetch (n=4) ---\r\n"] +[24.271794, "o", "warn: NATS.Client.JetStream.Internal.NatsJSFetch[2005]\r\n Unhandled protocol message: 409 Exceeded MaxRequestMaxBytes of 3\r\n"] +[25.271882, "o", "info: NATS-by-Example[0]\r\n Bye!\r\n"] +[25.273041, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Disposing connection NATS-by-Example\r\n"] diff --git a/examples/jetstream/pull-consumer-limits/dotnet2/output.txt b/examples/jetstream/pull-consumer-limits/dotnet2/output.txt new file mode 100644 index 00000000..63211c2f --- /dev/null +++ b/examples/jetstream/pull-consumer-limits/dotnet2/output.txt @@ -0,0 +1,42 @@ +info: NATS.Client.Core.NatsConnection[1001] + Try to connect NATS nats://nats:4222 +info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005] + Received server info: ServerInfo { Id = NBACXYPZZOMGMAHOMI6SSNE6UXXYY23YUQV5E7CRYWFM7EP777OFIIVV, Name = NBACXYPZZOMGMAHOMI6SSNE6UXXYY23YUQV5E7CRYWFM7EP777OFIIVV, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.192.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False } +info: NATS.Client.Core.NatsConnection[1001] + Connect succeed NATS-by-Example, NATS nats://nats:4222 +info: NATS-by-Example[0] + --- max in-flight messages (n=1) --- +info: NATS-by-Example[0] + Requested 3, got 1 +info: NATS-by-Example[0] + Requested 1, got 0 +info: NATS-by-Example[0] + Requested 1, got 1 +info: NATS-by-Example[0] + --- max fetch batch size (n=2) --- +warn: NATS.Client.JetStream.Internal.NatsJSFetch[2005] + Unhandled protocol message: 409 Exceeded MaxRequestBatch of 2 +info: NATS-by-Example[0] + Received hello +info: NATS-by-Example[0] + Received world +info: NATS-by-Example[0] + Requested 2, got 2 +info: NATS-by-Example[0] + --- max waiting requests (n=1) --- +info: NATS-by-Example[0] + --- max fetch timeout (d=1s) --- +info: NATS-by-Example[0] + Got 0 messages in 00:00:01.0012871 +warn: NATS.Client.JetStream.Internal.NatsJSFetch[2005] + Unhandled protocol message: 409 Exceeded MaxRequestExpires of 1s +info: NATS-by-Example[0] + Got 0 messages in 00:00:04.9971654 +info: NATS-by-Example[0] + --- max total bytes per fetch (n=4) --- +warn: NATS.Client.JetStream.Internal.NatsJSFetch[2005] + Unhandled protocol message: 409 Exceeded MaxRequestMaxBytes of 3 +info: NATS-by-Example[0] + Bye! +info: NATS.Client.Core.NatsConnection[1001] + Disposing connection NATS-by-Example diff --git a/examples/jetstream/pull-consumer/dotnet2/Main.cs b/examples/jetstream/pull-consumer/dotnet2/Main.cs new file mode 100644 index 00000000..485a8664 --- /dev/null +++ b/examples/jetstream/pull-consumer/dotnet2/Main.cs @@ -0,0 +1,139 @@ +// Install NuGet packages `NATS.Net` and `Microsoft.Extensions.Logging.Console`. + +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using NATS.Client.Core; +using NATS.Client.JetStream; +using NATS.Client.JetStream.Models; + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("NATS-by-Example"); + +// `NATS_URL` environment variable can be used to pass the locations of the NATS servers. +var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222"; + +// Connect to NATS server. Since connection is disposable at the end of our scope we should flush +// our buffers and close connection cleanly. +var opts = new NatsOpts +{ + Url = url, + LoggerFactory = loggerFactory, + Name = "NATS-by-Example", +}; +await using var nats = new NatsConnection(opts); + +// Access JetStream for managing streams and consumers as well as for +// publishing and consuming messages to and from the stream. +var js = new NatsJSContext(nats); + +var streamName = "EVENTS"; + +// Declare a simple [limits-based stream](/examples/jetstream/limits-stream/dotnet2/). +var stream = await js.CreateStreamAsync(new StreamConfig(streamName, new[] { "events.>" })); + +// Publish a few messages for the example. +await js.PublishAsync(subject: "events.1", data: "event-data-1"); +await js.PublishAsync(subject: "events.2", data: "event-data-2"); +await js.PublishAsync(subject: "events.3", data: "event-data-3"); + +// Create the consumer bound to the previously created stream. If durable +// name is not supplied, consumer will be removed after InactiveThreshold +// (defaults to 5 seconds) is reached when not actively consuming messages. +// `Name` is optional, if not provided it will be auto-generated. +// For this example, let's use the consumer with no options, which will +// be ephemeral with auto-generated name. +var consumer = await stream.CreateConsumerAsync(new ConsumerConfig()); + +// Messages can be _consumed_ continuously in a loop using `Consume` +// method. `Consume` can be supplied with various options, but for this +// example we will use the default ones.`break` is used as part of this +// example to make sure to stop processing after we process 3 messages (so +// that it does not interfere with other examples). +var count = 0; +await foreach (var msg in consumer.ConsumeAsync()) +{ + await msg.AckAsync(); + logger.LogInformation("received msg on {Subject} with data {Data}", msg.Subject, msg.Data); + if (++count == 3) + break; +} + +// Publish more messages. +await js.PublishAsync(subject: "events.1", data: "event-data-1"); +await js.PublishAsync(subject: "events.2", data: "event-data-2"); +await js.PublishAsync(subject: "events.3", data: "event-data-3"); + +// We can _fetch_ messages in batches. The first argument being the +// batch size which is the _maximum_ number of messages that should +// be returned. For this first fetch, we ask for two and we will get +// those since they are in the stream. +var fetchCount = 0; +await foreach (var msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 2 })) +{ + await msg.AckAsync(); + fetchCount++; +} + +logger.LogInformation("Got {Count} messages", fetchCount); + +// `Fetch` puts messages on the returned `Messages()` channel. This channel +// will only be closed when the requested number of messages have been +// received or the operation times out. If we do not want to wait for the +// rest of the messages and want to quickly return as many messages as there +// are available (up to provided batch size), we can use `FetchNoWait` +// instead. +// Here, because we have already received two messages, we will only get +// one more. +// NOTE: `FetchNoWait` usage is discouraged since it can cause unnecessary load +// if not used correctly e.g. in a loop without a backoff it will continuously +// try to get messages even if there is no new messages in the stream. +fetchCount = 0; +await foreach (var msg in ((NatsJSConsumer)consumer).FetchNoWaitAsync(opts: new NatsJSFetchOpts { MaxMsgs = 100 })) +{ + await msg.AckAsync(); + fetchCount++; +} +logger.LogInformation("Got {Count} messages", fetchCount); + +// Finally, if we are at the end of the stream and we call fetch, +// the call will be blocked until the "max wait" time which is 30 +// seconds by default, but this can be set explicitly as an option. +var fetchStopwatch = Stopwatch.StartNew(); +fetchCount = 0; +await foreach (var msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 100, Expires = TimeSpan.FromSeconds(1) })) +{ + await msg.AckAsync(); + fetchCount++; +} +logger.LogInformation("Got {Count} messages in {Elapsed}", fetchCount, fetchStopwatch.Elapsed); + +// Durable consumers can be created by specifying the Durable name. +// Durable consumers are not removed automatically regardless of the +// InactiveThreshold. They can be removed by calling `DeleteConsumer`. +var durable = await stream.CreateConsumerAsync(new ConsumerConfig("processor")); + +// Consume and fetch work the same way for durable consumers. +await foreach (var msg in durable.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 1 })) +{ + logger.LogInformation("Received {Subject} from durable consumer", msg.Subject); +} + +// While ephemeral consumers will be removed after InactiveThreshold, durable +// consumers have to be removed explicitly if no longer needed. +await stream.DeleteConsumerAsync("processor"); + +// Let's try to get the consumer to make sure it's gone. +try +{ + await stream.GetConsumerAsync("processor"); +} +catch (NatsJSApiException e) +{ + if (e.Error.Code == 404) + { + logger.LogInformation("Consumer is gone"); + } +} + +// That's it! +logger.LogInformation("Bye!"); diff --git a/examples/jetstream/pull-consumer/dotnet2/jetstream-pull-consumer.csproj b/examples/jetstream/pull-consumer/dotnet2/jetstream-pull-consumer.csproj new file mode 100644 index 00000000..2eeef688 --- /dev/null +++ b/examples/jetstream/pull-consumer/dotnet2/jetstream-pull-consumer.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + + diff --git a/examples/jetstream/pull-consumer/dotnet2/output.cast b/examples/jetstream/pull-consumer/dotnet2/output.cast new file mode 100644 index 00000000..664233a8 --- /dev/null +++ b/examples/jetstream/pull-consumer/dotnet2/output.cast @@ -0,0 +1,12 @@ +{"version": 2, "width": 104, "height": 51, "timestamp": 1700974724, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: jetstream/pull-consumer/dotnet2"} +[2.684686, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Try to connect NATS nats://nats:4222\r\n"] +[2.720752, "o", "info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005]\r\n Received server info: ServerInfo { Id = NCOG3JTGFJ2TBMN7ONAKUXU6L7NSHWOPW2A3NMA265XE22C4QEXWJ545, Name = NCOG3JTGFJ2TBMN7ONAKUXU6L7NSHWOPW2A3NMA265XE22C4QEXWJ545, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.176.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False }\r\n"] +[2.73788, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Connect succeed NATS-by-Example, NATS nats://nats:4222\r\n"] +[2.870285, "o", "info: NATS-by-Example[0]\r\n received msg on events.1 with data event-data-1\r\n"] +[2.870507, "o", "info: NATS-by-Example[0]\r\n received msg on events.2 with data event-data-2\r\ninfo: NATS-by-Example[0]\r\n received msg on events.3 with data event-data-3\r\n"] +[2.884117, "o", "info: NATS-by-Example[0]\r\n Got 2 messages\r\n"] +[2.892321, "o", "info: NATS-by-Example[0]\r\n Got 1 messages\r\n"] +[3.895207, "o", "info: NATS-by-Example[0]\r\n Got 0 messages in 00:00:01.0016194\r\n"] +[3.898406, "o", "info: NATS-by-Example[0]\r\n Received events.1 from durable consumer\r\n"] +[3.905078, "o", "info: NATS-by-Example[0]\r\n Consumer is gone\r\ninfo: NATS-by-Example[0]\r\n Bye!\r\n"] +[3.905922, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Disposing connection NATS-by-Example\r\n"] diff --git a/examples/jetstream/pull-consumer/dotnet2/output.txt b/examples/jetstream/pull-consumer/dotnet2/output.txt new file mode 100644 index 00000000..e773077a --- /dev/null +++ b/examples/jetstream/pull-consumer/dotnet2/output.txt @@ -0,0 +1,26 @@ +info: NATS.Client.Core.NatsConnection[1001] + Try to connect NATS nats://nats:4222 +info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005] + Received server info: ServerInfo { Id = NCOG3JTGFJ2TBMN7ONAKUXU6L7NSHWOPW2A3NMA265XE22C4QEXWJ545, Name = NCOG3JTGFJ2TBMN7ONAKUXU6L7NSHWOPW2A3NMA265XE22C4QEXWJ545, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.176.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False } +info: NATS.Client.Core.NatsConnection[1001] + Connect succeed NATS-by-Example, NATS nats://nats:4222 +info: NATS-by-Example[0] + received msg on events.1 with data event-data-1 +info: NATS-by-Example[0] + received msg on events.2 with data event-data-2 +info: NATS-by-Example[0] + received msg on events.3 with data event-data-3 +info: NATS-by-Example[0] + Got 2 messages +info: NATS-by-Example[0] + Got 1 messages +info: NATS-by-Example[0] + Got 0 messages in 00:00:01.0016194 +info: NATS-by-Example[0] + Received events.1 from durable consumer +info: NATS-by-Example[0] + Consumer is gone +info: NATS-by-Example[0] + Bye! +info: NATS.Client.Core.NatsConnection[1001] + Disposing connection NATS-by-Example diff --git a/examples/jetstream/workqueue-stream/dotnet2/Main.cs b/examples/jetstream/workqueue-stream/dotnet2/Main.cs new file mode 100644 index 00000000..57728a0d --- /dev/null +++ b/examples/jetstream/workqueue-stream/dotnet2/Main.cs @@ -0,0 +1,128 @@ +// Install NuGet packages `NATS.Net` and `Microsoft.Extensions.Logging.Console`. +using Microsoft.Extensions.Logging; +using NATS.Client.Core; +using NATS.Client.JetStream; +using NATS.Client.JetStream.Models; + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("NATS-by-Example"); + +// `NATS_URL` environment variable can be used to pass the locations of the NATS servers. +var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222"; + +// Connect to NATS server. Since connection is disposable at the end of our scope we should flush +// our buffers and close connection cleanly. +var opts = new NatsOpts +{ + Url = url, + LoggerFactory = loggerFactory, + Name = "NATS-by-Example", +}; +await using var nats = new NatsConnection(opts); + +// Access JetStream for managing streams and consumers as well as for +// publishing and consuming messages to and from the stream. +var js = new NatsJSContext(nats); + +var streamName = "EVENTS"; + +// ### Creating the stream +// Define the stream configuration, specifying `WorkQueuePolicy` for +// retention, and create the stream. +var stream = await js.CreateStreamAsync(new StreamConfig(streamName, new[] { "events.>" }) +{ + Retention = StreamConfigRetention.Workqueue, +}); + +// ### Queue messages +// Publish a few messages. +await js.PublishAsync("events.us.page_loaded", "event-data"); +await js.PublishAsync("events.us.mouse_clicked", "event-data"); +await js.PublishAsync("events.us.input_focused", "event-data"); +logger.LogInformation("published 3 messages"); + +// Checking the stream info, we see three messages have been queued. +logger.LogInformation("# Stream info without any consumers"); +await PrintStreamStateAsync(stream); + +// ### Adding a consumer +// Now let's add a consumer and publish a few more messages. +// [pull](/examples/jetstream/pull-consumer/dotnet2) +var consumer = await stream.CreateConsumerAsync(new ConsumerConfig("processor-1")); + +// Fetch and ack the queued messages. +await foreach (var msg in consumer.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 3 })) +{ + await msg.AckAsync(); + /* await msg.AckAsync(new AckOpts { DoubleAck = true }); */ +} + +// Checking the stream info again, we will notice no messages +// are available. +logger.LogInformation("# Stream info with one consumer"); +await PrintStreamStateAsync(stream); + +// ### Exclusive non-filtered consumer +// As noted in the description above, work-queue streams can only have +// at most one consumer with interest on a subject at any given time. +// Since the pull consumer above is not filtered, if we try to create +// another one, it will fail. +logger.LogInformation("# Create an overlapping consumer"); +try +{ + await stream.CreateConsumerAsync(new ConsumerConfig("processor-2")); +} +catch (NatsJSApiException e) +{ + logger.LogInformation("Error: {Message}", e.Error); +} + +// However if we delete the first one, we can then add the new one. +await stream.DeleteConsumerAsync("processor-1"); +await stream.CreateConsumerAsync(new ConsumerConfig("processor-2")); +logger.LogInformation("Created the new consumer"); + +await stream.DeleteConsumerAsync("processor-2"); + +// ### Multiple filtered consumers +// To create multiple consumers, a subject filter needs to be applied. +// For this example, we could scope each consumer to the geo that the +// event was published from, in this case `us` or `eu`. +logger.LogInformation("# Create non-overlapping consumers"); + +var consumer1 = await stream.CreateConsumerAsync(new ConsumerConfig("processor-us") { FilterSubject = "events.us.>" }); +var consumer2 = await stream.CreateConsumerAsync(new ConsumerConfig("processor-eu") { FilterSubject = "events.eu.>" }); + +await js.PublishAsync("events.eu.mouse_clicked", "event-data"); +await js.PublishAsync("events.us.page_loaded", "event-data"); +await js.PublishAsync("events.us.input_focused", "event-data"); +await js.PublishAsync("events.eu.page_loaded", "event-data"); +logger.LogInformation("Published 4 messages"); + +await foreach (var msg in consumer1.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 2 })) +{ + logger.LogInformation("us sub got: {Subject}", msg.Subject); + await msg.AckAsync(); +} + +await foreach (var msg in consumer2.FetchAsync(opts: new NatsJSFetchOpts { MaxMsgs = 2 })) +{ + logger.LogInformation("eu sub got: {Subject}", msg.Subject); + await msg.AckAsync(); +} + +// That's it! +logger.LogInformation("Bye!"); + +async Task PrintStreamStateAsync(INatsJSStream jsStream) +{ + await jsStream.RefreshAsync(); + var state = jsStream.Info.State; + logger.LogInformation( + "Stream has messages:{Messages} first:{FirstSeq} last:{LastSeq} consumer_count:{ConsumerCount} num_subjects:{NumSubjects}", + state.Messages, + state.FirstSeq, + state.LastSeq, + state.ConsumerCount, + state.NumSubjects); +} diff --git a/examples/jetstream/workqueue-stream/dotnet2/jetstream-workqueue-stream.csproj b/examples/jetstream/workqueue-stream/dotnet2/jetstream-workqueue-stream.csproj new file mode 100644 index 00000000..2eeef688 --- /dev/null +++ b/examples/jetstream/workqueue-stream/dotnet2/jetstream-workqueue-stream.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + + diff --git a/examples/jetstream/workqueue-stream/dotnet2/output.cast b/examples/jetstream/workqueue-stream/dotnet2/output.cast new file mode 100644 index 00000000..6e70eada --- /dev/null +++ b/examples/jetstream/workqueue-stream/dotnet2/output.cast @@ -0,0 +1,17 @@ +{"version": 2, "width": 104, "height": 51, "timestamp": 1700974713, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: jetstream/workqueue-stream/dotnet2"} +[2.647326, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Try to connect NATS nats://nats:4222\r\n"] +[2.68379, "o", "info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005]\r\n Received server info: ServerInfo { Id = NBRUEGKDSZN4IWXND263LPPDW32QPCB56M7GVBSXKOO42CLAWJAYFXNW, Name = NBRUEGKDSZN4IWXND263LPPDW32QPCB56M7GVBSXKOO42CLAWJAYFXNW, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.160.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False }\r\n"] +[2.701546, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Connect succeed NATS-by-Example, NATS nats://nats:4222\r\n"] +[2.790643, "o", "info: NATS-by-Example[0]\r\n published 3 messages\r\ninfo: NATS-by-Example[0]\r\n # Stream info without any consumers\r\n"] +[2.796505, "o", "info: NATS-by-Example[0]\r\n Stream has messages:3 first:1 last:3 consumer_count:0 num_subjects:3\r\n"] +[2.841433, "o", "info: NATS-by-Example[0]\r\n # Stream info with one consumer\r\n"] +[2.842665, "o", "info: NATS-by-Example[0]\r\n Stream has messages:0 first:4 last:3 consumer_count:1 num_subjects:0\r\ninfo: NATS-by-Example[0]\r\n # Create an overlapping consumer\r\n"] +[2.845559, "o", "info: NATS-by-Example[0]\r\n Error: ApiError { Code = 400, Description = multiple non-filtered consumers not allowed on workqueue stream, ErrCode = 10099 }\r\n"] +[2.849281, "o", "info: NATS-by-Example[0]\r\n Created the new consumer\r\n"] +[2.84999, "o", "info: NATS-by-Example[0]\r\n # Create non-overlapping consumers\r\n"] +[2.85452, "o", "info: NATS-by-Example[0]\r\n Published 4 messages\r\n"] +[2.855352, "o", "info: NATS-by-Example[0]\r\n us sub got: events.us.page_loaded\r\ninfo: NATS-by-Example[0]\r\n us sub got: events.us.input_focused\r\n"] +[2.856074, "o", "info: NATS-by-Example[0]\r\n eu sub got: events.eu.mouse_clicked\r\n"] +[2.856213, "o", "info: NATS-by-Example[0]\r\n eu sub got: events.eu.page_loaded\r\n"] +[2.856299, "o", "info: NATS-by-Example[0]\r\n Bye!\r\n"] +[2.857662, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Disposing connection NATS-by-Example\r\n"] diff --git a/examples/jetstream/workqueue-stream/dotnet2/output.txt b/examples/jetstream/workqueue-stream/dotnet2/output.txt new file mode 100644 index 00000000..e8647890 --- /dev/null +++ b/examples/jetstream/workqueue-stream/dotnet2/output.txt @@ -0,0 +1,38 @@ +info: NATS.Client.Core.NatsConnection[1001] + Try to connect NATS nats://nats:4222 +info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005] + Received server info: ServerInfo { Id = NBRUEGKDSZN4IWXND263LPPDW32QPCB56M7GVBSXKOO42CLAWJAYFXNW, Name = NBRUEGKDSZN4IWXND263LPPDW32QPCB56M7GVBSXKOO42CLAWJAYFXNW, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.160.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False } +info: NATS.Client.Core.NatsConnection[1001] + Connect succeed NATS-by-Example, NATS nats://nats:4222 +info: NATS-by-Example[0] + published 3 messages +info: NATS-by-Example[0] + # Stream info without any consumers +info: NATS-by-Example[0] + Stream has messages:3 first:1 last:3 consumer_count:0 num_subjects:3 +info: NATS-by-Example[0] + # Stream info with one consumer +info: NATS-by-Example[0] + Stream has messages:0 first:4 last:3 consumer_count:1 num_subjects:0 +info: NATS-by-Example[0] + # Create an overlapping consumer +info: NATS-by-Example[0] + Error: ApiError { Code = 400, Description = multiple non-filtered consumers not allowed on workqueue stream, ErrCode = 10099 } +info: NATS-by-Example[0] + Created the new consumer +info: NATS-by-Example[0] + # Create non-overlapping consumers +info: NATS-by-Example[0] + Published 4 messages +info: NATS-by-Example[0] + us sub got: events.us.page_loaded +info: NATS-by-Example[0] + us sub got: events.us.input_focused +info: NATS-by-Example[0] + eu sub got: events.eu.mouse_clicked +info: NATS-by-Example[0] + eu sub got: events.eu.page_loaded +info: NATS-by-Example[0] + Bye! +info: NATS.Client.Core.NatsConnection[1001] + Disposing connection NATS-by-Example diff --git a/examples/kv/intro/dotnet2/Main.cs b/examples/kv/intro/dotnet2/Main.cs new file mode 100644 index 00000000..b0b8300a --- /dev/null +++ b/examples/kv/intro/dotnet2/Main.cs @@ -0,0 +1,156 @@ +// Install NuGet packages `NATS.Net` and `Microsoft.Extensions.Logging.Console`. +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Configuration; +using NATS.Client.Core; +using NATS.Client.JetStream; +using NATS.Client.JetStream.Models; +using NATS.Client.KeyValueStore; + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("NATS-by-Example"); + +// `NATS_URL` environment variable can be used to pass the locations of the NATS servers. +var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222"; + +// Connect to NATS server. Since connection is disposable at the end of our scope we should flush +// our buffers and close connection cleanly. +var opts = new NatsOpts +{ + Url = url, + LoggerFactory = loggerFactory, + Name = "NATS-by-Example", +}; +await using var nats = new NatsConnection(opts); +var js = new NatsJSContext(nats); +var kv = new NatsKVContext(js); + +// ### Bucket basics +// A key-value (KV) bucket is created by specifying a bucket name. +var profiles = await kv.CreateStoreAsync(new NatsKVConfig("profiles")); + +// As one would expect, the `KeyValue` interface provides the +// standard `Put` and `Get` methods. However, unlike most KV +// stores, a revision number of the entry is tracked. +await profiles.PutAsync("sue.color", "blue"); +var entry = await profiles.GetEntryAsync("sue.color"); +logger.LogInformation("{Key} @ {Revision} ->{Value}\n", entry.Key, entry.Revision, entry.Value); + +await profiles.PutAsync("sue.color", "green"); +entry = await profiles.GetEntryAsync("sue.color"); +logger.LogInformation("{Key} @ {Revision} ->{Value}\n", entry.Key, entry.Revision, entry.Value); + +// A revision number is useful when you need to enforce [optimistic +// concurrency control][occ] on a specific key-value entry. In short, +// if there are multiple actors attempting to put a new value for a +// key concurrently, we want to prevent the "last writer wins" behavior +// which is non-deterministic. To guard against this, we can use the +// `kv.Update` method and specify the expected revision. Only if this +// matches on the server, will the value be updated. +// [occ]: https://en.wikipedia.org/wiki/Optimistic_concurrency_control +try +{ + await profiles.UpdateAsync("sue.color", "red", 1); +} +catch (NatsKVWrongLastRevisionException e) +{ + logger.LogInformation("Expected error: {Error}", e.Message); +} + +await profiles.UpdateAsync("sue.color", "red", 2); +entry = await profiles.GetEntryAsync("sue.color"); +logger.LogInformation("{Key} @ {Revision} ->{Value}\n", entry.Key, entry.Revision, entry.Value); + +// ### Stream abstraction +// Before moving on, it is important to understand that a KV bucket is +// light abstraction over a standard stream. This is by design since it +// enables some powerful features which we will observe in a minute. +// +// **How exactly is a KV bucket modeled as a stream?** +// When one is created, internally, a stream is created using the `KV_` +// prefix as convention. Appropriate stream configuration are used that +// are optimized for the KV access patterns, so you can ignore the +// details. +await foreach (var name in js.ListStreamNamesAsync()) +{ + logger.LogInformation("KV stream name: {Name}", name); +} + +// Since it is a normal stream, we can create a consumer and +// fetch messages. +// If we look at the subject, we will notice that first token is a +// special reserved prefix, the second token is the bucket name, and +// remaining suffix is the actually key. The bucket name is inherently +// a namespace for all keys and thus there is no concern for conflict +// across buckets. This is different from what we need to do for a stream +// which is to bind a set of _public_ subjects to a stream. +var consumer = await js.CreateConsumerAsync("KV_profiles", new ConsumerConfig +{ + AckPolicy = ConsumerConfigAckPolicy.None, +}); + +{ + var next = await consumer.NextAsync(); + if (next is { Metadata: { } metadata } msg) + { + logger.LogInformation("{Subject} @ {Sequence} -> {Data}", msg.Subject, metadata.Sequence.Stream, msg.Data); + } +} + +// Let's put a new value for this key and see what we get from the subscription. +await profiles.PutAsync("sue.color", "yellow"); +{ + var next = await consumer.NextAsync(); + if (next is { Metadata: { } metadata } msg) + { + logger.LogInformation("{Subject} @ {Sequence} -> {Data}", msg.Subject, metadata.Sequence.Stream, msg.Data); + } +} + +// Unsurprisingly, we get the new updated value as a message. +// Since it's KV interface, we should be able to delete a key as well. +// Does this result in a new message? +await profiles.DeleteAsync("sue.color"); +{ + var next = await consumer.NextAsync(); + if (next is { Metadata: { } metadata } msg) + { + logger.LogInformation("{Subject} @ {Sequence} -> {Data}", msg.Subject, metadata.Sequence.Stream, msg.Data); + + // 🤔 That is useful to get a message that something happened to that key, + // and that this is considered a new revision. + // However, how do we know if the new value was set to be `nil` or the key + // was deleted? + // To differentiate, delete-based messages contain a header. Notice the `KV-Operation: DEL` + // header. + logger.LogInformation("Headers: {Headers}", msg.Headers); + } +} + +// ### Watching for changes +// Although one could subscribe to the stream directly, it is more convenient +// to use a `KeyWatcher` which provides a deliberate API and types for tracking +// changes over time. Notice that we can use a wildcard which we will come back to.. +var watcher = Task.Run(async () => { + await foreach (var kve in profiles.WatchAsync()) + { + logger.LogInformation("{Key} @ {Revision} -> {Value} (op: {Op})", kve.Key, kve.Revision, kve.Value, kve.Operation); + if (kve.Key == "sue.food") + break; + } +}); + +// Even though we deleted the key, of course we can put a new value. +await profiles.PutAsync("sue.color", "purple"); + +// To finish this short intro, since we know that keys are subjects under the covers, if we +// put another key, we can observe the change through the watcher. One other detail to call out +// is notice the revision for this *new* key is not `1`. It relies on the underlying stream's +// message sequence number to indicate the _revision_. The guarantee being that it is always +// monotonically increasing, but numbers will be shared across keys (like subjects) rather +// than sequence numbers relative to each key. +await profiles.PutAsync("sue.food", "pizza"); + +await watcher; + +// That's it! +logger.LogInformation("Bye!"); diff --git a/examples/kv/intro/dotnet2/kv-intro.csproj b/examples/kv/intro/dotnet2/kv-intro.csproj new file mode 100644 index 00000000..2eeef688 --- /dev/null +++ b/examples/kv/intro/dotnet2/kv-intro.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + + diff --git a/examples/kv/intro/dotnet2/output.cast b/examples/kv/intro/dotnet2/output.cast new file mode 100644 index 00000000..d2b0cd51 --- /dev/null +++ b/examples/kv/intro/dotnet2/output.cast @@ -0,0 +1,18 @@ +{"version": 2, "width": 104, "height": 51, "timestamp": 1700974771, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: kv/intro/dotnet2"} +[2.860775, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Try to connect NATS nats://nats:4222\r\n"] +[2.898722, "o", "info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005]\r\n Received server info: ServerInfo { Id = NDHQPVNXQXWK4CSELBSZ3HQVB7IY5222P5MTJRPPDOP4Q4KOLHCLMUJP, Name = NDHQPVNXQXWK4CSELBSZ3HQVB7IY5222P5MTJRPPDOP4Q4KOLHCLMUJP, Version = 2.10.4, Proto"] +[2.898934, "o", "colVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId ="] +[2.899052, "o", " 5, ClientIp = 192.168.208.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False }\r\n"] +[2.916899, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Connect succeed NATS-by-Example, NATS nats://nats:4222\r\n"] +[3.02259, "o", "info: NATS-by-Example[0]\r\n sue.color @ 1 ->blue\r\n \r\n"] +[3.023784, "o", "info: NATS-by-Example[0]\r\n sue.color @ 2 ->green\r\n \r\n"] +[3.029158, "o", "info: NATS-by-Example[0]\r\n Expected error: Wrong last revision\r\n"] +[3.030143, "o", "info: NATS-by-Example[0]\r\n sue.color @ 3 ->red\r\n \r\n"] +[3.034603, "o", "info: NATS-by-Example[0]\r\n KV stream name: KV_profiles\r\n"] +[3.078091, "o", "info: NATS-by-Example[0]\r\n $KV.profiles.sue.color @ 3 -> red\r\n"] +[3.079538, "o", "info: NATS-by-Example[0]\r\n $KV.profiles.sue.color @ 4 -> yellow\r\n"] +[3.082446, "o", "info: NATS-by-Example[0]\r\n $KV.profiles.sue.color @ 5 -> (null)\r\n"] +[3.082708, "o", "info: NATS-by-Example[0]\r\n Headers: [KV-Operation, DEL]\r\n"] +[3.097757, "o", "info: NATS-by-Example[0]\r\n sue.color @ 6 -> purple (op: Put)\r\ninfo: NATS-by-Example[0]\r\n sue.food @ 7 -> pizza (op: Put)\r\n"] +[3.099533, "o", "info: NATS-by-Example[0]\r\n Bye!\r\n"] +[3.100259, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Disposing connection NATS-by-Example\r\n"] diff --git a/examples/kv/intro/dotnet2/output.txt b/examples/kv/intro/dotnet2/output.txt new file mode 100644 index 00000000..7d26e989 --- /dev/null +++ b/examples/kv/intro/dotnet2/output.txt @@ -0,0 +1,35 @@ +info: NATS.Client.Core.NatsConnection[1001] + Try to connect NATS nats://nats:4222 +info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005] + Received server info: ServerInfo { Id = NDHQPVNXQXWK4CSELBSZ3HQVB7IY5222P5MTJRPPDOP4Q4KOLHCLMUJP, Name = NDHQPVNXQXWK4CSELBSZ3HQVB7IY5222P5MTJRPPDOP4Q4KOLHCLMUJP, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.208.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False } +info: NATS.Client.Core.NatsConnection[1001] + Connect succeed NATS-by-Example, NATS nats://nats:4222 +info: NATS-by-Example[0] + sue.color @ 1 ->blue + +info: NATS-by-Example[0] + sue.color @ 2 ->green + +info: NATS-by-Example[0] + Expected error: Wrong last revision +info: NATS-by-Example[0] + sue.color @ 3 ->red + +info: NATS-by-Example[0] + KV stream name: KV_profiles +info: NATS-by-Example[0] + $KV.profiles.sue.color @ 3 -> red +info: NATS-by-Example[0] + $KV.profiles.sue.color @ 4 -> yellow +info: NATS-by-Example[0] + $KV.profiles.sue.color @ 5 -> (null) +info: NATS-by-Example[0] + Headers: [KV-Operation, DEL] +info: NATS-by-Example[0] + sue.color @ 6 -> purple (op: Put) +info: NATS-by-Example[0] + sue.food @ 7 -> pizza (op: Put) +info: NATS-by-Example[0] + Bye! +info: NATS.Client.Core.NatsConnection[1001] + Disposing connection NATS-by-Example diff --git a/examples/messaging/concurrent/dotnet2/Main.cs b/examples/messaging/concurrent/dotnet2/Main.cs new file mode 100644 index 00000000..d3688701 --- /dev/null +++ b/examples/messaging/concurrent/dotnet2/Main.cs @@ -0,0 +1,53 @@ +// Install NuGet packages `NATS.Net` and `Microsoft.Extensions.Logging.Console`. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using NATS.Client.Core; + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("NATS-by-Example"); + +// `NATS_URL` environment variable can be used to pass the locations of the NATS servers. +var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222"; + +// Connect to NATS server. Since connection is disposable at the end of our scope we should flush +// our buffers and close connection cleanly. +var opts = new NatsOpts +{ + Url = url, + LoggerFactory = loggerFactory, + Name = "NATS-by-Example", +}; +await using var nats = new NatsConnection(opts); + +using var cts = new CancellationTokenSource(); + +// Subscribe to a subject and start waiting for messages in the background and +// start processing messages in parallel. +var subscription = Task.Run(async () => +{ + await Parallel.ForEachAsync(nats.SubscribeAsync("greet", cancellationToken: cts.Token), async (msg, _) => + { + Console.WriteLine($"Received {msg.Data}"); + }); +}); + +// Give some time for the subscription to start. +await Task.Delay(TimeSpan.FromSeconds(1)); + +for (int i = 0; i < 50; i++) +{ + await nats.PublishAsync("greet", $"hello {i}"); +} + +// Give some time for the subscription to receive all the messages. +await Task.Delay(TimeSpan.FromSeconds(1)); + +await cts.CancelAsync(); + +await subscription; + +// That's it! +logger.LogInformation("Bye!"); diff --git a/examples/messaging/concurrent/dotnet2/messaging-concurrent.csproj b/examples/messaging/concurrent/dotnet2/messaging-concurrent.csproj new file mode 100644 index 00000000..2eeef688 --- /dev/null +++ b/examples/messaging/concurrent/dotnet2/messaging-concurrent.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + + diff --git a/examples/messaging/concurrent/dotnet2/output.cast b/examples/messaging/concurrent/dotnet2/output.cast new file mode 100644 index 00000000..1134bad0 --- /dev/null +++ b/examples/messaging/concurrent/dotnet2/output.cast @@ -0,0 +1,13 @@ +{"version": 2, "width": 104, "height": 51, "timestamp": 1700975014, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: messaging/concurrent/dotnet2"} +[2.679196, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Try to connect NATS nats://nats:4222\r\n"] +[2.720665, "o", "info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005]\r\n Received server info: ServerInfo { Id = NBV3DDRVEHSPKTV7ZQV6PBYT6FAPORASTCDJLA4CCSC5XKKYEUVPJUI4, Name = NBV3DDRVEHSPKTV7ZQV6PBYT6FAPORASTCDJLA4CCSC5XKKYEUVPJUI4, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 172.22.0.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False }\r\n"] +[2.739914, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Connect succeed NATS-by-Example, NATS nats://nats:4222\r\n"] +[3.650716, "o", "Received hello 2\r\nReceived hello 3\r\nReceived hello 0\r\n"] +[3.650921, "o", "Received hello 1\r\nReceived hello 4\r\nReceived hello 8\r\nReceived hello 9\r\nReceived hello 10\r\nReceived hello 11\r\nReceived hello 12\r\nReceived hello 13\r\nReceived hello 14\r\n"] +[3.651213, "o", "Received hello 15\r\nReceived hello 6\r\nReceived hello 17\r\nReceived hello 18\r\nReceived hello 19\r\nReceived hello 20\r\nReceived hello 21\r\nReceived hello 22\r\nReceived hello 23\r\nReceived hello 24\r\nReceived hello 25\r\nReceived hello 26\r\nReceived hello 27\r\nReceived hello 28\r\nReceived hello 29\r\nReceived hello 30\r\nReceived hello 16\r\nReceived hello 31\r\n"] +[3.651476, "o", "Received hello 32\r\nReceived hello 33\r\nReceived hello 35\r\nReceived hello 36\r\nReceived hello 37\r\nReceived hello 38\r\nReceived hello 39\r\n"] +[3.65158, "o", "Received hello 40\r\n"] +[3.651736, "o", "Received hello 41\r\nReceived hello 42\r\nReceived hello 43\r\nReceived hello 44\r\nReceived hello 45\r\nReceived hello 46\r\nReceived hello 47\r\nReceived hello 48\r\nReceived hello 49\r\nReceived hello 7\r\nReceived hello 5\r\n"] +[3.652255, "o", "Received hello 34\r\n"] +[4.64603, "o", "info: NATS-by-Example[0]\r\n Bye!\r\n"] +[4.647799, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Disposing connection NATS-by-Example\r\n"] diff --git a/examples/messaging/concurrent/dotnet2/output.txt b/examples/messaging/concurrent/dotnet2/output.txt new file mode 100644 index 00000000..907e3ff5 --- /dev/null +++ b/examples/messaging/concurrent/dotnet2/output.txt @@ -0,0 +1,60 @@ +info: NATS.Client.Core.NatsConnection[1001] + Try to connect NATS nats://nats:4222 +info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005] + Received server info: ServerInfo { Id = NBV3DDRVEHSPKTV7ZQV6PBYT6FAPORASTCDJLA4CCSC5XKKYEUVPJUI4, Name = NBV3DDRVEHSPKTV7ZQV6PBYT6FAPORASTCDJLA4CCSC5XKKYEUVPJUI4, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 172.22.0.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False } +info: NATS.Client.Core.NatsConnection[1001] + Connect succeed NATS-by-Example, NATS nats://nats:4222 +Received hello 2 +Received hello 3 +Received hello 0 +Received hello 1 +Received hello 4 +Received hello 8 +Received hello 9 +Received hello 10 +Received hello 11 +Received hello 12 +Received hello 13 +Received hello 14 +Received hello 15 +Received hello 6 +Received hello 17 +Received hello 18 +Received hello 19 +Received hello 20 +Received hello 21 +Received hello 22 +Received hello 23 +Received hello 24 +Received hello 25 +Received hello 26 +Received hello 27 +Received hello 28 +Received hello 29 +Received hello 30 +Received hello 16 +Received hello 31 +Received hello 32 +Received hello 33 +Received hello 35 +Received hello 36 +Received hello 37 +Received hello 38 +Received hello 39 +Received hello 40 +Received hello 41 +Received hello 42 +Received hello 43 +Received hello 44 +Received hello 45 +Received hello 46 +Received hello 47 +Received hello 48 +Received hello 49 +Received hello 7 +Received hello 5 +Received hello 34 +info: NATS-by-Example[0] + Bye! +info: NATS.Client.Core.NatsConnection[1001] + Disposing connection NATS-by-Example diff --git a/examples/messaging/iterating-multiple-subscriptions/dotnet2/Main.cs b/examples/messaging/iterating-multiple-subscriptions/dotnet2/Main.cs new file mode 100644 index 00000000..f1859711 --- /dev/null +++ b/examples/messaging/iterating-multiple-subscriptions/dotnet2/Main.cs @@ -0,0 +1,63 @@ +// Install NuGet packages `NATS.Net`, `System.Interactive.Async` and `Microsoft.Extensions.Logging.Console`. + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using NATS.Client.Core; + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("NATS-by-Example"); + +// `NATS_URL` environment variable can be used to pass the locations of the NATS servers. +var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222"; + +// Connect to NATS server. Since connection is disposable at the end of our scope we should flush +// our buffers and close connection cleanly. +var opts = new NatsOpts +{ + Url = url, + LoggerFactory = loggerFactory, + Name = "NATS-by-Example", +}; +await using var nats = new NatsConnection(opts); + +await nats.ConnectAsync(); + +using var cts = new CancellationTokenSource(); + +var s1 = nats.SubscribeAsync("s1", cancellationToken: cts.Token); +var s2 = nats.SubscribeAsync("s2", cancellationToken: cts.Token); +var s3 = nats.SubscribeAsync("s3", cancellationToken: cts.Token); +var s4 = nats.SubscribeAsync("s4", cancellationToken: cts.Token); + +const int total = 80; + +var subs = Task.Run(async () => +{ + var count = 0; + await foreach (var msg in AsyncEnumerableEx.Merge(s1, s2, s3, s4)) + { + Console.WriteLine($"Received {msg.Subject}: {msg.Data}"); + + if (++count == total) + await cts.CancelAsync(); + } +}); + +await Task.Delay(1000); + +for (int i = 0; i < total / 4; i++) +{ + await nats.PublishAsync("s1", i); + await nats.PublishAsync("s2", i); + await nats.PublishAsync("s3", i); + await nats.PublishAsync("s4", i); + await Task.Delay(100); +} + +await subs; + +// That's it! +logger.LogInformation("Bye!"); diff --git a/examples/messaging/iterating-multiple-subscriptions/dotnet2/messaging-iterating-multiple-subscriptions.csproj b/examples/messaging/iterating-multiple-subscriptions/dotnet2/messaging-iterating-multiple-subscriptions.csproj new file mode 100644 index 00000000..f7d7d296 --- /dev/null +++ b/examples/messaging/iterating-multiple-subscriptions/dotnet2/messaging-iterating-multiple-subscriptions.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + + diff --git a/examples/messaging/iterating-multiple-subscriptions/dotnet2/output.cast b/examples/messaging/iterating-multiple-subscriptions/dotnet2/output.cast new file mode 100644 index 00000000..755b672c --- /dev/null +++ b/examples/messaging/iterating-multiple-subscriptions/dotnet2/output.cast @@ -0,0 +1,35 @@ +{"version": 2, "width": 104, "height": 51, "timestamp": 1700975026, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: messaging/iterating-multiple-subscriptions/dotnet2"} +[2.644196, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Try to connect NATS nats://nats:4222\r\n"] +[2.685333, "o", "info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005]\r\n Received server info: ServerInfo { Id = NCL2Z2LULLH7O54YP6SHWYAETV5NDRNW4DJZHVGTERHCZXVO43QRMEDN, Name = NCL2Z2LULLH7O54YP6SHWYAETV5NDRNW4DJZHVGTERHCZXVO43QRMEDN, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 172.23.0.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False }\r\n"] +[2.703783, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Connect succeed NATS-by-Example, NATS nats://nats:4222\r\n"] +[3.716245, "o", "Received s1: 0\r\nReceived s2: 0\r\n"] +[3.716392, "o", "Received s3: 0\r\nReceived s4: 0\r\n"] +[3.809504, "o", "Received s1: 1\r\nReceived s2: 1\r\nReceived s3: 1\r\nReceived s4: 1\r\n"] +[3.90989, "o", "Received s2: 2\r\n"] +[3.910146, "o", "Received s1: 2\r\nReceived s3: 2\r\nReceived s4: 2\r\n"] +[4.010057, "o", "Received s1: 3\r\nReceived s2: 3\r\n"] +[4.010328, "o", "Received s3: 3\r\nReceived s4: 3\r\n"] +[4.110611, "o", "Received s2: 4\r\nReceived s1: 4\r\nReceived s3: 4\r\nReceived s4: 4\r\n"] +[4.214751, "o", "Received s1: 5\r\nReceived s2: 5\r\nReceived s3: 5\r\nReceived s4: 5\r\n"] +[4.31934, "o", "Received s1: 6\r\n"] +[4.31967, "o", "Received s2: 6\r\nReceived s3: 6\r\nReceived s4: 6\r\n"] +[4.417908, "o", "Received s1: 7\r\nReceived s2: 7\r\nReceived s3: 7\r\nReceived s4: 7\r\n"] +[4.518282, "o", "Received s3: 8\r\nReceived s1: 8\r\nReceived s2: 8\r\nReceived s4: 8\r\n"] +[4.61827, "o", "Received s1: 9\r\n"] +[4.61849, "o", "Received s2: 9\r\n"] +[4.618698, "o", "Received s3: 9\r\nReceived s4: 9\r\n"] +[4.720887, "o", "Received s1: 10\r\nReceived s2: 10\r\nReceived s3: 10\r\nReceived s4: 10\r\n"] +[4.824856, "o", "Received s1: 11\r\nReceived s2: 11\r\nReceived s3: 11\r\nReceived s4: 11\r\n"] +[4.92508, "o", "Received s1: 12\r\nReceived s2: 12\r\nReceived s3: 12\r\nReceived s4: 12\r\n"] +[5.029602, "o", "Received s1: 13\r\nReceived s2: 13\r\nReceived s3: 13\r\nReceived s4: 13\r\n"] +[5.129982, "o", "Received s1: 14\r\nReceived s3: 14\r\nReceived s4: 14\r\nReceived s2: 14\r\n"] +[5.2343, "o", "Received s3: 15\r\n"] +[5.234439, "o", "Received s2: 15\r\nReceived s4: 15\r\n"] +[5.23473, "o", "Received s1: 15\r\n"] +[5.335154, "o", "Received s1: 16\r\nReceived s2: 16\r\nReceived s3: 16\r\nReceived s4: 16\r\n"] +[5.437368, "o", "Received s1: 17\r\nReceived s2: 17\r\nReceived s3: 17\r\nReceived s4: 17\r\n"] +[5.535923, "o", "Received s1: 18\r\n"] +[5.536207, "o", "Received s2: 18\r\nReceived s3: 18\r\nReceived s4: 18\r\n"] +[5.636074, "o", "Received s1: 19\r\nReceived s2: 19\r\nReceived s3: 19\r\nReceived s4: 19\r\n"] +[5.734341, "o", "info: NATS-by-Example[0]\r\n Bye!\r\n"] +[5.734984, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Disposing connection NATS-by-Example\r\n"] diff --git a/examples/messaging/iterating-multiple-subscriptions/dotnet2/output.txt b/examples/messaging/iterating-multiple-subscriptions/dotnet2/output.txt new file mode 100644 index 00000000..25fbd729 --- /dev/null +++ b/examples/messaging/iterating-multiple-subscriptions/dotnet2/output.txt @@ -0,0 +1,90 @@ +info: NATS.Client.Core.NatsConnection[1001] + Try to connect NATS nats://nats:4222 +info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005] + Received server info: ServerInfo { Id = NCL2Z2LULLH7O54YP6SHWYAETV5NDRNW4DJZHVGTERHCZXVO43QRMEDN, Name = NCL2Z2LULLH7O54YP6SHWYAETV5NDRNW4DJZHVGTERHCZXVO43QRMEDN, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 172.23.0.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False } +info: NATS.Client.Core.NatsConnection[1001] + Connect succeed NATS-by-Example, NATS nats://nats:4222 +Received s1: 0 +Received s2: 0 +Received s3: 0 +Received s4: 0 +Received s1: 1 +Received s2: 1 +Received s3: 1 +Received s4: 1 +Received s2: 2 +Received s1: 2 +Received s3: 2 +Received s4: 2 +Received s1: 3 +Received s2: 3 +Received s3: 3 +Received s4: 3 +Received s2: 4 +Received s1: 4 +Received s3: 4 +Received s4: 4 +Received s1: 5 +Received s2: 5 +Received s3: 5 +Received s4: 5 +Received s1: 6 +Received s2: 6 +Received s3: 6 +Received s4: 6 +Received s1: 7 +Received s2: 7 +Received s3: 7 +Received s4: 7 +Received s3: 8 +Received s1: 8 +Received s2: 8 +Received s4: 8 +Received s1: 9 +Received s2: 9 +Received s3: 9 +Received s4: 9 +Received s1: 10 +Received s2: 10 +Received s3: 10 +Received s4: 10 +Received s1: 11 +Received s2: 11 +Received s3: 11 +Received s4: 11 +Received s1: 12 +Received s2: 12 +Received s3: 12 +Received s4: 12 +Received s1: 13 +Received s2: 13 +Received s3: 13 +Received s4: 13 +Received s1: 14 +Received s3: 14 +Received s4: 14 +Received s2: 14 +Received s3: 15 +Received s2: 15 +Received s4: 15 +Received s1: 15 +Received s1: 16 +Received s2: 16 +Received s3: 16 +Received s4: 16 +Received s1: 17 +Received s2: 17 +Received s3: 17 +Received s4: 17 +Received s1: 18 +Received s2: 18 +Received s3: 18 +Received s4: 18 +Received s1: 19 +Received s2: 19 +Received s3: 19 +Received s4: 19 +info: NATS-by-Example[0] + Bye! +info: NATS.Client.Core.NatsConnection[1001] + Disposing connection NATS-by-Example diff --git a/examples/messaging/json/dotnet2/Main.cs b/examples/messaging/json/dotnet2/Main.cs new file mode 100644 index 00000000..50b18c9c --- /dev/null +++ b/examples/messaging/json/dotnet2/Main.cs @@ -0,0 +1,100 @@ +// Install NuGet packages `NATS.Net` and `Microsoft.Extensions.Logging.Console`. + +using System; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using NATS.Client.Core; + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("NATS-by-Example"); + +// `NATS_URL` environment variable can be used to pass the locations of the NATS servers. +var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222"; + +// Connect to NATS server. Since connection is disposable at the end of our scope we should flush +// our buffers and close connection cleanly. +var opts = new NatsOpts +{ + Url = url, + LoggerFactory = loggerFactory, + Name = "NATS-by-Example", +}; +await using var nats = new NatsConnection(opts); + +await nats.ConnectAsync(); + +// When subscribing or publishing you can use the generated JSON serializer to deserialize the JSON payload. +// We can also demonstrate how to use the raw JSON payload and how to use a binary data. +// For more information about the serializers see see our [documentation](https://nats-io.github.io/nats.net/serializers/). +var mySerializer = new NatsJsonContextSerializer(MyJsonContext.Default); + +var subIterator1 = await nats.SubscribeCoreAsync("data", serializer: mySerializer); + +var subTask1 = Task.Run(async () => +{ + logger.LogInformation("Waiting for messages..."); + await foreach (var msg in subIterator1.Msgs.ReadAllAsync()) + { + if (msg.Data is null) + { + logger.LogInformation("Received empty payload: End of messages"); + break; + } + var data = msg.Data; + logger.LogInformation("Received deserialized object {Data}", data); + } +}); + +var subIterator2 = await nats.SubscribeCoreAsync>("data"); + +var subTask2 = Task.Run(async () => +{ + logger.LogInformation("Waiting for messages..."); + await foreach (var msg in subIterator2.Msgs.ReadAllAsync()) + { + using var memoryOwner = msg.Data; + + if (memoryOwner.Length == 0) + { + logger.LogInformation("Received empty payload: End of messages"); + break; + } + + var json = Encoding.UTF8.GetString(memoryOwner.Span); + + logger.LogInformation("Received raw JSON {Json}", json); + } +}); + +await nats.PublishAsync(subject: "data", data: new MyData{ Id = 1, Name = "Bob" }, serializer: mySerializer); +await nats.PublishAsync(subject: "data", data: Encoding.UTF8.GetBytes("""{"id":2,"name":"Joe"}""")); + +var alice = """{"id":3,"name":"Alice"}"""; +var bw = new NatsBufferWriter(); +var byteCount = Encoding.UTF8.GetByteCount(alice); +var memory = bw.GetMemory(byteCount); +Encoding.UTF8.GetBytes(alice, memory.Span); +bw.Advance(byteCount); +await nats.PublishAsync>(subject: "data", data: bw); + +await nats.PublishAsync(subject: "data"); + +await Task.WhenAll(subTask1, subTask2); + +// That's it! +logger.LogInformation("Bye!"); + +// ## Serializer generator +[JsonSerializable(typeof(MyData))] +internal partial class MyJsonContext : JsonSerializerContext; + +public record MyData +{ + [JsonPropertyName("id")] + public int Id { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } +} diff --git a/examples/messaging/json/dotnet2/messaging-json.csproj b/examples/messaging/json/dotnet2/messaging-json.csproj new file mode 100644 index 00000000..2eeef688 --- /dev/null +++ b/examples/messaging/json/dotnet2/messaging-json.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + + diff --git a/examples/messaging/json/dotnet2/output.cast b/examples/messaging/json/dotnet2/output.cast new file mode 100644 index 00000000..d2f01454 --- /dev/null +++ b/examples/messaging/json/dotnet2/output.cast @@ -0,0 +1,12 @@ +{"version": 2, "width": 104, "height": 51, "timestamp": 1700974992, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: messaging/json/dotnet2"} +[2.792935, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Try to connect NATS nats://nats:4222\r\n"] +[2.832408, "o", "info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005]\r\n Received server info: ServerInfo { Id = NCGBX2DWLQ6UCLYUSRL7CE4C5U4OIFNM3XPHG3IEVLHT7BN6AFOPAEBP, Name = NCGBX2DWLQ6UCLYUSRL7CE4C5U4OIFNM3XPHG3IEVLHT7BN6AFOPAEBP, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 172.20.0.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False }\r\n"] +[2.851455, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Connect succeed NATS-by-Example, NATS nats://nats:4222\r\n"] +[2.862472, "o", "info: NATS-by-Example[0]\r\n Waiting for messages...\r\n"] +[2.865272, "o", "info: NATS-by-Example[0]\r\n Waiting for messages...\r\n"] +[2.876427, "o", "info: NATS-by-Example[0]\r\n Received deserialized object MyData { Id = 1, Name = Bob }\r\n"] +[2.877626, "o", "info: NATS-by-Example[0]\r\n Received deserialized object MyData { Id = 2, Name = Joe }\r\ninfo: NATS-by-Example[0]\r\n Received deserialized object MyData { Id = 3, Name = Alice }\r\ninfo: NATS-by-Example[0]\r\n Received empty payload: End of messages\r\n"] +[2.878258, "o", "info: NATS-by-Example[0]\r\n Received raw JSON {\"id\":1,\"name\":\"Bob\"}\r\ninfo: NATS-by-Example[0]\r\n Received raw JSON {\"id\":2,\"name\":\"Joe\"}\r\n"] +[2.878401, "o", "info: NATS-by-Example[0]\r\n Received raw JSON {\"id\":3,\"name\":\"Alice\"}\r\ninfo: NATS-by-Example[0]\r\n Received empty payload: End of messages\r\n"] +[2.87861, "o", "info: NATS-by-Example[0]\r\n Bye!\r\n"] +[2.879416, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Disposing connection NATS-by-Example\r\n"] diff --git a/examples/messaging/json/dotnet2/output.txt b/examples/messaging/json/dotnet2/output.txt new file mode 100644 index 00000000..23c785da --- /dev/null +++ b/examples/messaging/json/dotnet2/output.txt @@ -0,0 +1,30 @@ +info: NATS.Client.Core.NatsConnection[1001] + Try to connect NATS nats://nats:4222 +info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005] + Received server info: ServerInfo { Id = NCGBX2DWLQ6UCLYUSRL7CE4C5U4OIFNM3XPHG3IEVLHT7BN6AFOPAEBP, Name = NCGBX2DWLQ6UCLYUSRL7CE4C5U4OIFNM3XPHG3IEVLHT7BN6AFOPAEBP, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 172.20.0.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False } +info: NATS.Client.Core.NatsConnection[1001] + Connect succeed NATS-by-Example, NATS nats://nats:4222 +info: NATS-by-Example[0] + Waiting for messages... +info: NATS-by-Example[0] + Waiting for messages... +info: NATS-by-Example[0] + Received deserialized object MyData { Id = 1, Name = Bob } +info: NATS-by-Example[0] + Received deserialized object MyData { Id = 2, Name = Joe } +info: NATS-by-Example[0] + Received deserialized object MyData { Id = 3, Name = Alice } +info: NATS-by-Example[0] + Received empty payload: End of messages +info: NATS-by-Example[0] + Received raw JSON {"id":1,"name":"Bob"} +info: NATS-by-Example[0] + Received raw JSON {"id":2,"name":"Joe"} +info: NATS-by-Example[0] + Received raw JSON {"id":3,"name":"Alice"} +info: NATS-by-Example[0] + Received empty payload: End of messages +info: NATS-by-Example[0] + Bye! +info: NATS.Client.Core.NatsConnection[1001] + Disposing connection NATS-by-Example diff --git a/examples/messaging/protobuf/dotnet2/Main.cs b/examples/messaging/protobuf/dotnet2/Main.cs new file mode 100644 index 00000000..c4d7765d --- /dev/null +++ b/examples/messaging/protobuf/dotnet2/Main.cs @@ -0,0 +1,201 @@ +// Install NuGet packages `NATS.Net`, `Google.Protobuf` and `Microsoft.Extensions.Logging.Console`. + +using System; +using System.Buffers; +using System.Threading.Tasks; +using Google.Protobuf; +using Google.Protobuf.Reflection; +using Microsoft.Extensions.Logging; +using NATS.Client.Core; + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("NATS-by-Example"); + +// `NATS_URL` environment variable can be used to pass the locations of the NATS servers. +var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222"; + +// Connect to NATS server. Since connection is disposable at the end of our scope we should flush +// our buffers and close connection cleanly. Notice the use of custom serializer registry. +var opts = new NatsOpts +{ + Url = url, + LoggerFactory = loggerFactory, + SerializerRegistry = new MyProtoBufSerializerRegistry(), + Name = "NATS-by-Example", +}; +await using var nats = new NatsConnection(opts); + +// Subscribe to a subject and start waiting for messages in the background. +// Notice that we are using a custom serializer for the subscription. +var sub = Task.Run(async () => +{ + logger.LogInformation("Waiting for messages..."); + await foreach (var msg in nats.SubscribeAsync(subject: "greet", serializer: MyProtoBufSerializer.Default)) + { + if (msg.Data is null) + { + logger.LogInformation("Received empty payload: End of messages"); + break; + } + var request = msg.Data; + var reply = new GreetReply { Text = $"hello {request.Name}"}; + await msg.ReplyAsync(reply, serializer: MyProtoBufSerializer.Default); + } +}); + +// This request uses the default serializer for the connection assigned to connection options above. +// Alternatively we could've passed the individual serializer to the request method. +var reply = await nats.RequestAsync(subject: "greet", new GreetRequest { Name = "bob" }); +logger.LogInformation("Response = {Response}...", reply.Data.Text); + +// Send an empty message to indicate we are done. +await nats.PublishAsync("greet"); + +// We can unsubscribe now all orders are published. Unsubscribing or disposing the subscription +// should complete the message loop and exit the background task cleanly. +await sub; + +// That's it! +logger.LogInformation("Bye!"); + +// ## Serializer Registry +public class MyProtoBufSerializerRegistry : INatsSerializerRegistry +{ + public INatsSerialize GetSerializer() => MyProtoBufSerializer.Default; + + public INatsDeserialize GetDeserializer() => MyProtoBufSerializer.Default; +} + +// ## Serializer +public class MyProtoBufSerializer : INatsSerializer +{ + public static readonly INatsSerializer Default = new MyProtoBufSerializer(); + + public void Serialize(IBufferWriter bufferWriter, T value) + { + if (value is IMessage message) + { + message.WriteTo(bufferWriter); + } + else + { + throw new NatsException($"Can't serialize {typeof(T)}"); + } + } + + public T? Deserialize(in ReadOnlySequence buffer) + { + if (typeof(T) == typeof(GreetRequest)) + { + return (T)(object)GreetRequest.Parser.ParseFrom(buffer); + } + + if (typeof(T) == typeof(GreetReply)) + { + return (T)(object)GreetReply.Parser.ParseFrom(buffer); + } + + throw new NatsException($"Can't deserialize {typeof(T)}"); + } +} + +// ## Protobuf Messages +// The following messages would normally be generated using `protoc`. For the sake of example +// we defined simplified versions here. Usually you would use `.proto` files to define your +// messages and generate the code using `Grpc.Tools` NuGet package to generate them in separate +// project. See [gRPC documentation](https://grpc.io/docs/languages/csharp/basics/) and +// [ASP.NET Core Tooling Support](https://learn.microsoft.com/en-us/aspnet/core/grpc/basics?view=aspnetcore-8.0#c-tooling-support-for-proto-files) +// for more details. +public class GreetRequest : IMessage, IBufferMessage +{ + public static readonly MessageParser Parser = new(() => new GreetRequest()); + + public string Name { get; set; } + + public void MergeFrom(GreetRequest message) => Name = message.Name; + + public void MergeFrom(CodedInputStream input) + { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if (tag == 10) + Name = input.ReadString(); + } + } + + public void WriteTo(CodedOutputStream output) + { + output.WriteRawTag(10); + output.WriteString(Name); + } + + public int CalculateSize() => CodedOutputStream.ComputeStringSize(Name) + 1; + + public MessageDescriptor Descriptor => null!; + + public bool Equals(GreetRequest other) => string.Equals(other?.Name, Name); + + public GreetRequest Clone() => new() { Name = Name }; + + public void InternalMergeFrom(ref ParseContext input) + { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if (tag == 10) + Name = input.ReadString(); + } + } + + public void InternalWriteTo(ref WriteContext output) + { + output.WriteRawTag(10); + output.WriteString(Name); + } +} + +public class GreetReply : IMessage, IBufferMessage +{ + public static readonly MessageParser Parser = new(() => new GreetReply()); + + public string Text { get; set; } + + public void MergeFrom(GreetReply message) => Text = message.Text; + + public void MergeFrom(CodedInputStream input) + { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if (tag == 10) + Text = input.ReadString(); + } + } + + public void WriteTo(CodedOutputStream output) + { + output.WriteRawTag(10); + output.WriteString(Text); + } + + public int CalculateSize() => CodedOutputStream.ComputeStringSize(Text) + 1; + + public MessageDescriptor Descriptor => null!; + + public bool Equals(GreetReply other) => string.Equals(other?.Text, Text); + + public GreetReply Clone() => new() { Text = Text }; + + public void InternalMergeFrom(ref ParseContext input) + { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if (tag == 10) + Text = input.ReadString(); + } + } + + public void InternalWriteTo(ref WriteContext output) + { + output.WriteRawTag(10); + output.WriteString(Text); + } +} diff --git a/examples/messaging/protobuf/dotnet2/messaging-protobuf.csproj b/examples/messaging/protobuf/dotnet2/messaging-protobuf.csproj new file mode 100644 index 00000000..7c663736 --- /dev/null +++ b/examples/messaging/protobuf/dotnet2/messaging-protobuf.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + + diff --git a/examples/messaging/protobuf/dotnet2/output.cast b/examples/messaging/protobuf/dotnet2/output.cast new file mode 100644 index 00000000..8bc458e2 --- /dev/null +++ b/examples/messaging/protobuf/dotnet2/output.cast @@ -0,0 +1,9 @@ +{"version": 2, "width": 104, "height": 51, "timestamp": 1700975003, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: messaging/protobuf/dotnet2"} +[2.733536, "o", "info: NATS-by-Example[0]\r\n Waiting for messages...\r\n"] +[2.758129, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Try to connect NATS nats://nats:4222\r\n"] +[2.799622, "o", "info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005]\r\n Received server info: ServerInfo { Id = NBG2DC6XCA2465Y3XIFCGH4YL7NFAEK4RLAKJD54G3YPD277HEEGD3W2, Name = NBG2DC6XCA2465Y3XIFCGH4YL7NFAEK4RLAKJD54G3YPD277HEEGD3W2, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 172.21.0.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False }\r\n"] +[2.818939, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Connect succeed NATS-by-Example, NATS nats://nats:4222\r\n"] +[2.841516, "o", "info: NATS-by-Example[0]\r\n Response = hello bob...\r\n"] +[2.843661, "o", "info: NATS-by-Example[0]\r\n Received empty payload: End of messages\r\n"] +[2.845359, "o", "info: NATS-by-Example[0]\r\n Bye!\r\n"] +[2.846114, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Disposing connection NATS-by-Example\r\n"] diff --git a/examples/messaging/protobuf/dotnet2/output.txt b/examples/messaging/protobuf/dotnet2/output.txt new file mode 100644 index 00000000..d79ac765 --- /dev/null +++ b/examples/messaging/protobuf/dotnet2/output.txt @@ -0,0 +1,16 @@ +info: NATS-by-Example[0] + Waiting for messages... +info: NATS.Client.Core.NatsConnection[1001] + Try to connect NATS nats://nats:4222 +info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005] + Received server info: ServerInfo { Id = NBG2DC6XCA2465Y3XIFCGH4YL7NFAEK4RLAKJD54G3YPD277HEEGD3W2, Name = NBG2DC6XCA2465Y3XIFCGH4YL7NFAEK4RLAKJD54G3YPD277HEEGD3W2, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 172.21.0.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False } +info: NATS.Client.Core.NatsConnection[1001] + Connect succeed NATS-by-Example, NATS nats://nats:4222 +info: NATS-by-Example[0] + Response = hello bob... +info: NATS-by-Example[0] + Received empty payload: End of messages +info: NATS-by-Example[0] + Bye! +info: NATS.Client.Core.NatsConnection[1001] + Disposing connection NATS-by-Example diff --git a/examples/messaging/pub-sub/dotnet2/Main.cs b/examples/messaging/pub-sub/dotnet2/Main.cs index 9de6070b..7c775eeb 100644 --- a/examples/messaging/pub-sub/dotnet2/Main.cs +++ b/examples/messaging/pub-sub/dotnet2/Main.cs @@ -1,36 +1,46 @@ -// Install `NATS.Client.Core` from NuGet. +// Install NuGet packages `NATS.Net`, `NATS.Client.Serializers.Json` and `Microsoft.Extensions.Logging.Console`. +using Microsoft.Extensions.Logging; using NATS.Client.Core; +using NATS.Client.Serializers.Json; + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("NATS-by-Example"); // `NATS_URL` environment variable can be used to pass the locations of the NATS servers. var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222"; -Console.WriteLine($"Connecting to {url}..."); // Connect to NATS server. Since connection is disposable at the end of our scope we should flush // our buffers and close connection cleanly. -var opts = NatsOpts.Default with { Url = url }; +var opts = new NatsOpts +{ + Url = url, + LoggerFactory = loggerFactory, + SerializerRegistry = NatsJsonSerializerRegistry.Default, + Name = "NATS-by-Example", +}; await using var nats = new NatsConnection(opts); // Subscribe to a subject and start waiting for messages in the background. -await using var sub = await nats.SubscribeAsync("orders.>"); +await using var sub = await nats.SubscribeCoreAsync("orders.>"); -Console.WriteLine("[SUB] waiting for messages..."); +logger.LogInformation("Waiting for messages..."); var task = Task.Run(async () => { await foreach (var msg in sub.Msgs.ReadAllAsync()) { var order = msg.Data; - Console.WriteLine($"[SUB] received {msg.Subject}: {order}"); + logger.LogInformation("Subscriber received {Subject}: {Order}", msg.Subject, order); } - Console.WriteLine($"[SUB] unsubscribed"); + logger.LogInformation("Unsubscribed"); }); // Let's publish a few orders. for (int i = 0; i < 5; i++) { - Console.WriteLine($"[PUB] publishing order {i}..."); + logger.LogInformation("Publishing order {Index}...", i); await nats.PublishAsync($"orders.new.{i}", new Order(OrderId: i)); - await Task.Delay(1_000); + await Task.Delay(500); } // We can unsubscribe now all orders are published. Unsubscribing or disposing the subscription @@ -38,8 +48,8 @@ await sub.UnsubscribeAsync(); await task; -// That's it! We saw how we can subscribe to a subject and publish messages that would be seen by the subscribers -// based on matching subjects. -Console.WriteLine("Bye!"); +// That's it! We saw how we can subscribe to a subject and publish messages that would +// be seen by the subscribers based on matching subjects. +logger.LogInformation("Bye!"); public record Order(int OrderId); diff --git a/examples/messaging/pub-sub/dotnet2/messaging-pub-sub.csproj b/examples/messaging/pub-sub/dotnet2/messaging-pub-sub.csproj new file mode 100644 index 00000000..2eeef688 --- /dev/null +++ b/examples/messaging/pub-sub/dotnet2/messaging-pub-sub.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + + diff --git a/examples/messaging/pub-sub/dotnet2/output.cast b/examples/messaging/pub-sub/dotnet2/output.cast index f6a1655d..0cea88a3 100644 --- a/examples/messaging/pub-sub/dotnet2/output.cast +++ b/examples/messaging/pub-sub/dotnet2/output.cast @@ -1,17 +1,19 @@ -{"version": 2, "width": 180, "height": 10, "timestamp": 1694795896, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: messaging/pub-sub/dotnet2"} -[0.030622, "o", "Connecting to nats://nats:4222...\r\n"] -[0.258811, "o", "[SUB] waiting for messages...\r\n"] -[0.259365, "o", "[PUB] publishing order 0...\r\n"] -[0.296535, "o", "[SUB] received orders.new.0: Order { OrderId = 0 }\r\n"] -[1.263683, "o", "[PUB] publishing order 1...\r\n"] -[1.265654, "o", "[SUB] received orders.new.1: Order { OrderId = 1 }\r\n"] -[2.269119, "o", "[PUB] publishing order 2..."] -[2.269455, "o", "\r\n"] -[2.270738, "o", "[SUB] received orders.new.2: Order { OrderId = 2 }\r\n"] -[3.269758, "o", "[PUB] publishing order 3...\r\n"] -[3.270553, "o", "[SUB] received orders.new.3: Order { OrderId = 3 }\r\n"] -[4.271277, "o", "[PUB] publishing order 4..."] -[4.272023, "o", "\r\n"] -[4.272352, "o", "[SUB] received orders.new.4: Order { OrderId = 4 }\r\n"] -[5.287359, "o", "[SUB] unsubscribed\r\n"] -[5.287903, "o", "Bye!\r\n"] +{"version": 2, "width": 104, "height": 51, "timestamp": 1700974964, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: messaging/pub-sub/dotnet2"} +[2.848065, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Try to connect NATS nats://nats:4222\r\n"] +[2.88914, "o", "info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005]\r\n Received server info: ServerInfo { Id = NA5KDEZVXJXTIYMQAXRROBZL3WMLCO63YO2Z5QJOABXIFQZDN3AKTBF2, Name = NA5KDEZVXJXTIYMQAXRROBZL3WMLCO63YO2Z5QJOABXIFQZDN3AKTBF2, Version = 2.10.4, Proto"] +[2.889203, "o", "colVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 172.18.0.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False }\r\n"] +[2.908367, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Connect succeed NATS-by-Example, NATS nats://nats:4222\r\n"] +[2.914974, "o", "info: NATS-by-Example[0]\r\n Waiting for messages...\r\n"] +[2.915678, "o", "info: NATS-by-Example[0]\r\n Publishing order 0...\r\n"] +[2.936345, "o", "info: NATS-by-Example[0]\r\n Subscriber received orders.new.0: Order { OrderId = 0 }\r\n"] +[3.417846, "o", "info: NATS-by-Example[0]\r\n Publishing order 1...\r\n"] +[3.418748, "o", "info: NATS-by-Example[0]\r\n Subscriber received orders.new.1: Order { OrderId = 1 }\r\n"] +[3.918322, "o", "info: NATS-by-Example[0]\r\n Publishing order 2...\r\n"] +[3.918956, "o", "info: NATS-by-Example[0]\r\n Subscriber received orders.new.2: Order { OrderId = 2 }\r\n"] +[4.419149, "o", "info: NATS-by-Example[0]\r\n Publishing order 3...\r\n"] +[4.419546, "o", "info: NATS-by-Example[0]\r\n Subscriber received orders.new.3: Order { OrderId = 3 }\r\n"] +[4.919704, "o", "info: NATS-by-Example[0]\r\n Publishing order 4...\r\n"] +[4.920498, "o", "info: NATS-by-Example[0]\r\n Subscriber received orders.new.4: Order { OrderId = 4 }\r\n"] +[5.421789, "o", "info: NATS-by-Example[0]\r\n Unsubscribed\r\n"] +[5.422361, "o", "info: NATS-by-Example[0]\r\n Bye!\r\n"] +[5.423561, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Disposing connection NATS-by-Example\r\n"] diff --git a/examples/messaging/pub-sub/dotnet2/output.txt b/examples/messaging/pub-sub/dotnet2/output.txt index 80916a0a..75a87f8f 100644 --- a/examples/messaging/pub-sub/dotnet2/output.txt +++ b/examples/messaging/pub-sub/dotnet2/output.txt @@ -1,14 +1,34 @@ -Connecting to nats://nats:4222... -[SUB] waiting for messages... -[PUB] publishing order 0... -[SUB] received orders.new.0: Order { OrderId = 0 } -[PUB] publishing order 1... -[SUB] received orders.new.1: Order { OrderId = 1 } -[PUB] publishing order 2... -[SUB] received orders.new.2: Order { OrderId = 2 } -[PUB] publishing order 3... -[SUB] received orders.new.3: Order { OrderId = 3 } -[PUB] publishing order 4... -[SUB] received orders.new.4: Order { OrderId = 4 } -[SUB] unsubscribed -Bye! +info: NATS.Client.Core.NatsConnection[1001] + Try to connect NATS nats://nats:4222 +info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005] + Received server info: ServerInfo { Id = NA5KDEZVXJXTIYMQAXRROBZL3WMLCO63YO2Z5QJOABXIFQZDN3AKTBF2, Name = NA5KDEZVXJXTIYMQAXRROBZL3WMLCO63YO2Z5QJOABXIFQZDN3AKTBF2, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 172.18.0.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False } +info: NATS.Client.Core.NatsConnection[1001] + Connect succeed NATS-by-Example, NATS nats://nats:4222 +info: NATS-by-Example[0] + Waiting for messages... +info: NATS-by-Example[0] + Publishing order 0... +info: NATS-by-Example[0] + Subscriber received orders.new.0: Order { OrderId = 0 } +info: NATS-by-Example[0] + Publishing order 1... +info: NATS-by-Example[0] + Subscriber received orders.new.1: Order { OrderId = 1 } +info: NATS-by-Example[0] + Publishing order 2... +info: NATS-by-Example[0] + Subscriber received orders.new.2: Order { OrderId = 2 } +info: NATS-by-Example[0] + Publishing order 3... +info: NATS-by-Example[0] + Subscriber received orders.new.3: Order { OrderId = 3 } +info: NATS-by-Example[0] + Publishing order 4... +info: NATS-by-Example[0] + Subscriber received orders.new.4: Order { OrderId = 4 } +info: NATS-by-Example[0] + Unsubscribed +info: NATS-by-Example[0] + Bye! +info: NATS.Client.Core.NatsConnection[1001] + Disposing connection NATS-by-Example diff --git a/examples/messaging/request-reply/dotnet2/Main.cs b/examples/messaging/request-reply/dotnet2/Main.cs new file mode 100644 index 00000000..17eac4f4 --- /dev/null +++ b/examples/messaging/request-reply/dotnet2/Main.cs @@ -0,0 +1,77 @@ +// Install `NATS.Net` from NuGet. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using NATS.Client.Core; + +var stopwatch = Stopwatch.StartNew(); + +// `NATS_URL` environment variable can be used to pass the locations of the NATS servers. +var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222"; +Log($"[CON] Connecting to {url}..."); + +// Connect to NATS server. Since connection is disposable at the end of our scope we should flush +// our buffers and close connection cleanly. +var opts = NatsOpts.Default with { Url = url }; +await using var nats = new NatsConnection(opts); + +// Create a message event handler and then subscribe to the target +// subject which leverages a wildcard `greet.*`. +// When a user makes a "request", the client populates +// the reply-to field and then listens (subscribes) to that +// as a subject. +// The responder simply publishes a message to that reply-to. + await using var sub = await nats.SubscribeCoreAsync("greet.*"); + + var reader = sub.Msgs; + var responder = Task.Run(async () => + { + await foreach (var msg in reader.ReadAllAsync()) + { + var name = msg.Subject.Split('.')[1]; + Log($"[REP] Received {msg.Subject}"); + await Task.Delay(500); + await msg.ReplyAsync($"Hello {name}!"); + } + }); + +// Make a request and wait a most 1 second for a response. +var replyOpts = new NatsSubOpts { Timeout = TimeSpan.FromSeconds(2) }; + +Log("[REQ] From joe"); +var reply = await nats.RequestAsync("greet.joe", 0, replyOpts: replyOpts); +Log($"[REQ] {reply.Data}"); + +Log("[REQ] From sue"); +reply = await nats.RequestAsync("greet.sue", 0, replyOpts: replyOpts); +Log($"[REQ] {reply.Data}"); + +Log("[REQ] From bob"); +reply = await nats.RequestAsync("greet.bob", 0, replyOpts: replyOpts); +Log($"[REQ] {reply.Data}"); + +// Once we unsubscribe there will be no subscriptions to reply. +await sub.UnsubscribeAsync(); + +await responder; + +// Now there is no responder our request will timeout. + +try +{ + reply = await nats.RequestAsync("greet.joe", 0, replyOpts: replyOpts); + Log($"[REQ] {reply.Data} - This will timeout. We should not see this message."); +} +catch (NatsNoReplyException) +{ + Log("[REQ] timed out!"); +} + +// That's it! We saw how we can create a responder and request data from it. We also set +// request timeouts to make sure we can move on when there is no response to our requests. +Log("Bye!"); + +return; + +void Log(string log) => Console.WriteLine($"{stopwatch.Elapsed} {log}"); \ No newline at end of file diff --git a/examples/messaging/request-reply/dotnet2/messaging-request-reply.csproj b/examples/messaging/request-reply/dotnet2/messaging-request-reply.csproj new file mode 100644 index 00000000..2eeef688 --- /dev/null +++ b/examples/messaging/request-reply/dotnet2/messaging-request-reply.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + + diff --git a/examples/messaging/request-reply/dotnet2/output.cast b/examples/messaging/request-reply/dotnet2/output.cast new file mode 100644 index 00000000..43f9514c --- /dev/null +++ b/examples/messaging/request-reply/dotnet2/output.cast @@ -0,0 +1,10 @@ +{"version": 2, "width": 104, "height": 51, "timestamp": 1700974978, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: messaging/request-reply/dotnet2"} +[2.476822, "o", "00:00:00.0008194 [CON] Connecting to nats://nats:4222...\r\n"] +[2.594173, "o", "00:00:00.1225386 [REQ] From joe\r\n"] +[2.608938, "o", "00:00:00.1374172 [REP] Received greet.joe\r\n"] +[3.12051, "o", "00:00:00.6489792 [REQ] Hello joe!\r\n00:00:00.6490418 [REQ] From sue\r\n"] +[3.121268, "o", "00:00:00.6498312 [REP] Received greet.sue\r\n"] +[3.622743, "o", "00:00:01.1512245 [REQ] Hello sue!\r\n00:00:01.1512731 [REQ] From bob\r\n"] +[3.623312, "o", "00:00:01.1518886 [REP] Received greet.bob\r\n"] +[4.124807, "o", "00:00:01.6531626 [REQ] Hello bob!\r\n"] +[6.126315, "o", "00:00:03.6546766 [REQ] timed out!\r\n00:00:03.6547387 Bye!\r\n"] diff --git a/examples/messaging/request-reply/dotnet2/output.txt b/examples/messaging/request-reply/dotnet2/output.txt new file mode 100644 index 00000000..e8069bee --- /dev/null +++ b/examples/messaging/request-reply/dotnet2/output.txt @@ -0,0 +1,12 @@ +00:00:00.0008194 [CON] Connecting to nats://nats:4222... +00:00:00.1225386 [REQ] From joe +00:00:00.1374172 [REP] Received greet.joe +00:00:00.6489792 [REQ] Hello joe! +00:00:00.6490418 [REQ] From sue +00:00:00.6498312 [REP] Received greet.sue +00:00:01.1512245 [REQ] Hello sue! +00:00:01.1512731 [REQ] From bob +00:00:01.1518886 [REP] Received greet.bob +00:00:01.6531626 [REQ] Hello bob! +00:00:03.6546766 [REQ] timed out! +00:00:03.6547387 Bye! diff --git a/examples/os/intro/dotnet2/Main.cs b/examples/os/intro/dotnet2/Main.cs new file mode 100644 index 00000000..98c490c4 --- /dev/null +++ b/examples/os/intro/dotnet2/Main.cs @@ -0,0 +1,88 @@ +// Install NuGet packages `NATS.Net` and `Microsoft.Extensions.Logging.Console`. +using Microsoft.Extensions.Logging; +using NATS.Client.Core; +using NATS.Client.JetStream; +using NATS.Client.ObjectStore; +using NATS.Client.ObjectStore.Models; + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("NATS-by-Example"); + +// `NATS_URL` environment variable can be used to pass the locations of the NATS servers. +var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222"; + +// Connect to NATS server. Since connection is disposable at the end of our scope we should flush +// our buffers and close connection cleanly. +var opts = new NatsOpts +{ + Url = url, + LoggerFactory = loggerFactory, + Name = "NATS-by-Example", +}; +await using var nats = new NatsConnection(opts); +var js = new NatsJSContext(nats); +var obj = new NatsObjContext(js); + +// ### Object store basics +// An object-store (OS) bucket is created by specifying a bucket name. +// Here we try to access a store called "configs", if it doesn't exist +// the API will create it: +var store = await obj.CreateObjectStore("configs"); + +// You can get information on the object store by getting its info: +var status = await store.GetStatusAsync(); +logger.LogInformation("The object store has {Size} bytes", status.Info.State.Bytes); + +// 10MiB +const int bytes = 10_000_000; +var data = new byte[bytes]; + +// Let's add an entry to the object store +var info = await store.PutAsync(key: "a", data); +logger.LogInformation("Added entry {Name} ({Size} bytes)- '{Description}'", info.Name, info.Size, info.Description); + +// Entries in an object store are made from a "metadata" that describes the object +// And the payload. This allows you to store information about the significance of the +// entry separate from the raw data. +// You can update the metadata directly +await store.UpdateMetaAsync("a", new ObjectMetadata { Name = "a", Description = "still large data" }); + +// we expect this store to only contain one entry +// You can list its contents: +var count = 0; +await foreach (var entry in store.ListAsync()) +{ + logger.LogInformation("Entry {Name} ({Size} bytes)- '{Description}'", info.Name, info.Size, info.Description); + count++; +} +logger.LogInformation("The object store contains {Count} entries", count); + +// Now lets retrieve the item we added +var data1 = await store.GetBytesAsync("a"); +logger.LogInformation("Data has {Size} bytes", data1.Length); + +// You can watch an object store for changes: +var watcher = Task.Run(async () => +{ + await foreach (var m in store.WatchAsync(new NatsObjWatchOpts{IncludeHistory = false})) + { + logger.LogInformation(">>>>>>>> Watch: {Bucket} changed '{Name}' {Op}", m.Bucket, m.Name, m.Deleted ? "was deleted" : "was updated"); + } +}); + +// To delete an entry: +await store.DeleteAsync("a"); + +// Because the client may be working with large assets, ObjectStore +// normally presents a "Stream" based API. +info = await store.PutAsync(new ObjectMetadata { Name = "b", Description = "set with a stream" }, new MemoryStream(data)); +logger.LogInformation("Added entry {Name} ({Size} bytes)- '{Description}'", info.Name, info.Size, info.Description); + +var ms = new MemoryStream(); +info = await store.GetAsync("b", ms); +logger.LogInformation("Got entry {Name} ({Size} bytes)- '{Description}'", info.Name, info.Size, info.Description); + +await obj.DeleteObjectStore("configs", CancellationToken.None); + +// That's it! +logger.LogInformation("Bye!"); diff --git a/examples/os/intro/dotnet2/os-intro.csproj b/examples/os/intro/dotnet2/os-intro.csproj new file mode 100644 index 00000000..2eeef688 --- /dev/null +++ b/examples/os/intro/dotnet2/os-intro.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + + diff --git a/examples/os/intro/dotnet2/output.cast b/examples/os/intro/dotnet2/output.cast new file mode 100644 index 00000000..c8d0aec6 --- /dev/null +++ b/examples/os/intro/dotnet2/output.cast @@ -0,0 +1,14 @@ +{"version": 2, "width": 104, "height": 51, "timestamp": 1700974782, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: os/intro/dotnet2"} +[2.520731, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Try to connect NATS nats://nats:4222\r\n"] +[2.557586, "o", "info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005]\r\n Received server info: ServerInfo { Id = NAQ5RG6LC5UEKIQUEUJF7ZEODY33CYLHKAJEAP2GNYWLBVSJDCAQOACP, Name = NAQ5RG6LC5UEKIQUEUJF7ZEODY33CYLHKAJEAP2GNYWLBVSJDCAQOACP, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.224.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False }\r\n"] +[2.575568, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Connect succeed NATS-by-Example, NATS nats://nats:4222\r\n"] +[2.663127, "o", "info: NATS-by-Example[0]\r\n The object store has 0 bytes\r\n"] +[2.764165, "o", "info: NATS-by-Example[0]\r\n Added entry a (10000000 bytes)- '(null)'\r\n"] +[2.812681, "o", "info: NATS-by-Example[0]\r\n Entry a (10000000 bytes)- '(null)'\r\n"] +[2.8188, "o", "info: NATS-by-Example[0]\r\n The object store contains 1 entries\r\n"] +[2.854779, "o", "info: NATS-by-Example[0]\r\n Data has 10000000 bytes\r\n"] +[2.856988, "o", "info: NATS-by-Example[0]\r\n >>>>>>>> Watch: configs changed 'a' was updated\r\n"] +[2.934139, "o", "info: NATS-by-Example[0]\r\n Added entry b (10000000 bytes)- 'set with a stream'\r\n"] +[2.963641, "o", "info: NATS-by-Example[0]\r\n Got entry b (10000000 bytes)- 'set with a stream'\r\n"] +[2.968577, "o", "info: NATS-by-Example[0]\r\n Bye!\r\n"] +[2.969469, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Disposing connection NATS-by-Example\r\n"] diff --git a/examples/os/intro/dotnet2/output.txt b/examples/os/intro/dotnet2/output.txt new file mode 100644 index 00000000..578653ba --- /dev/null +++ b/examples/os/intro/dotnet2/output.txt @@ -0,0 +1,26 @@ +info: NATS.Client.Core.NatsConnection[1001] + Try to connect NATS nats://nats:4222 +info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005] + Received server info: ServerInfo { Id = NAQ5RG6LC5UEKIQUEUJF7ZEODY33CYLHKAJEAP2GNYWLBVSJDCAQOACP, Name = NAQ5RG6LC5UEKIQUEUJF7ZEODY33CYLHKAJEAP2GNYWLBVSJDCAQOACP, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.224.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False } +info: NATS.Client.Core.NatsConnection[1001] + Connect succeed NATS-by-Example, NATS nats://nats:4222 +info: NATS-by-Example[0] + The object store has 0 bytes +info: NATS-by-Example[0] + Added entry a (10000000 bytes)- '(null)' +info: NATS-by-Example[0] + Entry a (10000000 bytes)- '(null)' +info: NATS-by-Example[0] + The object store contains 1 entries +info: NATS-by-Example[0] + Data has 10000000 bytes +info: NATS-by-Example[0] + >>>>>>>> Watch: configs changed 'a' was updated +info: NATS-by-Example[0] + Added entry b (10000000 bytes)- 'set with a stream' +info: NATS-by-Example[0] + Got entry b (10000000 bytes)- 'set with a stream' +info: NATS-by-Example[0] + Bye! +info: NATS.Client.Core.NatsConnection[1001] + Disposing connection NATS-by-Example diff --git a/examples/services/intro/dotnet2/Main.cs b/examples/services/intro/dotnet2/Main.cs new file mode 100644 index 00000000..73ed8a3f --- /dev/null +++ b/examples/services/intro/dotnet2/Main.cs @@ -0,0 +1,93 @@ +// Install NuGet packages `NATS.Net`, `NATS.Client.Serializers.Json` and `Microsoft.Extensions.Logging.Console`. + +using System; +using Microsoft.Extensions.Logging; +using NATS.Client.Core; +using NATS.Client.Serializers.Json; +using NATS.Client.Services; + +using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole()); +var logger = loggerFactory.CreateLogger("NATS-by-Example"); + +// `NATS_URL` environment variable can be used to pass the locations of the NATS servers. +var url = Environment.GetEnvironmentVariable("NATS_URL") ?? "127.0.0.1:4222"; + +// Connect to NATS server. Since connection is disposable at the end of our scope we should flush +// our buffers and close connection cleanly. +var opts = new NatsOpts +{ + Url = url, + LoggerFactory = loggerFactory, + SerializerRegistry = NatsJsonSerializerRegistry.Default, + Name = "NATS-by-Example", +}; +await using var nats = new NatsConnection(opts); +var svc = new NatsSvcContext(nats); + +// ### Defining a Service +// +// This will create a service definition. Service definitions are made up of +// the service name (which can't have things like whitespace in it), a version, +// and a description. Even with no running endpoints, this service is discoverable +// via the micro protocol and by service discovery tools like `nats micro`. +// All of the default background handlers for discovery, PING, and stats are +// started at this point. +var service = await svc.AddServiceAsync(new NatsSvcConfig("minmax", "0.0.1") +{ + Description = "Returns the min/max number in a request" +}); + +// Each time we create a service, it will be given a new unique identifier. If multiple +// copies of the `minmax` service are running across a NATS subject space, then tools +// like `nats micro` will consider them like unique instances of the one service and the +// endpoint subscriptions are queue subscribed, so requests will only be sent to one +// endpoint _instance_ at a time. +// TODO: service.Info + +// ### Adding endpoints +// +// Groups serve as namespaces and are used as a subject prefix when endpoints +// don't supply fixed subjects. In this case, all endpoints will be listening +// on a subject that starts with `minmax.` +var root = await service.AddGroupAsync("minmax"); + +// Adds two endpoints to the service, one for the `min` operation and one for +// the `max` operation. Each endpoint represents a subscription. The supplied handlers +// will respond to `minmax.min` and `minmax.max`, respectively. +await root.AddEndpointAsync(HandleMin, "min", serializer: NatsJsonSerializer.Default); +await root.AddEndpointAsync(HandleMax, "max", serializer: NatsJsonSerializer.Default); + +// Make a request of the `min` endpoint of the `minmax` service, within the `minmax` group. +// Note that there's nothing special about this request, it's just a regular NATS +// request. +var min = await nats.RequestAsync("minmax.min", new[] +{ + -1, 2, 100, -2000 +}, requestSerializer: NatsJsonSerializer.Default); +logger.LogInformation("Requested min value, got {Min}", min.Data); + +// Make a request of the `max` endpoint of the `minmax` service, within the `minmax` group. +var max = await nats.RequestAsync("minmax.max", new[] +{ + -1, 2, 100, -2000 +}, requestSerializer: NatsJsonSerializer.Default); +logger.LogInformation("Requested max value, got {Max}", max.Data); + +// The statistics being managed by micro should now reflect the call made +// to each endpoint, and we didn't have to write any code to manage that. +// TODO: service.Stats + +// That's it! +logger.LogInformation("Bye!"); + +ValueTask HandleMin(NatsSvcMsg msg) +{ + var min = msg.Data.Min(); + return msg.ReplyAsync(min); +} + +ValueTask HandleMax(NatsSvcMsg msg) +{ + var min = msg.Data.Max(); + return msg.ReplyAsync(min); +} \ No newline at end of file diff --git a/examples/services/intro/dotnet2/output.cast b/examples/services/intro/dotnet2/output.cast new file mode 100644 index 00000000..9401427e --- /dev/null +++ b/examples/services/intro/dotnet2/output.cast @@ -0,0 +1,8 @@ +{"version": 2, "width": 104, "height": 51, "timestamp": 1700974792, "env": {"SHELL": "/usr/bin/zsh", "TERM": "xterm-256color"}, "title": "NATS by Example: services/intro/dotnet2"} +[2.709669, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Try to connect NATS nats://nats:4222\r\n"] +[2.747896, "o", "info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005]\r\n Received server info: ServerInfo { Id = NBNKAKMI2JMCWMZGM53FPYI2VYTYKXRKICP6BA37JEKXHKVQPJPKDLYV, Name = NBNKAKMI2JMCWMZGM53FPYI2VYTYKXRKICP6BA37JEKXHKVQPJPKDLYV, Version = 2.10.4, Proto"] +[2.748109, "o", "colVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.240.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False }\r\n"] +[2.766606, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Connect succeed NATS-by-Example, NATS nats://nats:4222\r\n"] +[2.809869, "o", "info: NATS-by-Example[0]\r\n Requested min value, got -2000\r\n"] +[2.812844, "o", "info: NATS-by-Example[0]\r\n Requested max value, got 100\r\ninfo: NATS-by-Example[0]\r\n Bye!\r\n"] +[2.813616, "o", "info: NATS.Client.Core.NatsConnection[1001]\r\n Disposing connection NATS-by-Example\r\n"] diff --git a/examples/services/intro/dotnet2/output.txt b/examples/services/intro/dotnet2/output.txt new file mode 100644 index 00000000..f9b86cf0 --- /dev/null +++ b/examples/services/intro/dotnet2/output.txt @@ -0,0 +1,14 @@ +info: NATS.Client.Core.NatsConnection[1001] + Try to connect NATS nats://nats:4222 +info: NATS.Client.Core.Internal.NatsReadProtocolProcessor[1005] + Received server info: ServerInfo { Id = NBNKAKMI2JMCWMZGM53FPYI2VYTYKXRKICP6BA37JEKXHKVQPJPKDLYV, Name = NBNKAKMI2JMCWMZGM53FPYI2VYTYKXRKICP6BA37JEKXHKVQPJPKDLYV, Version = 2.10.4, ProtocolVersion = 1, GitCommit = abc47f7, GoVersion = go1.21.3, Host = 0.0.0.0, Port = 4222, HeadersSupported = True, AuthRequired = False, TlsRequired = False, TlsVerify = False, TlsAvailable = False, MaxPayload = 1048576, JetStreamAvailable = True, ClientId = 5, ClientIp = 192.168.240.3, Nonce = , Cluster = , ClusterDynamic = False, ClientConnectUrls = , WebSocketConnectUrls = , LameDuckMode = False } +info: NATS.Client.Core.NatsConnection[1001] + Connect succeed NATS-by-Example, NATS nats://nats:4222 +info: NATS-by-Example[0] + Requested min value, got -2000 +info: NATS-by-Example[0] + Requested max value, got 100 +info: NATS-by-Example[0] + Bye! +info: NATS.Client.Core.NatsConnection[1001] + Disposing connection NATS-by-Example diff --git a/examples/services/intro/dotnet2/services-intro.csproj b/examples/services/intro/dotnet2/services-intro.csproj new file mode 100644 index 00000000..2eeef688 --- /dev/null +++ b/examples/services/intro/dotnet2/services-intro.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + + + + + + + + +