[RELEASE] 3.0#1070
Merged
Merged
Conversation
- Initial release with .NET 10 target - Also dropped .NET 6 target
* Clean up NET6 and optimize NETSTANDARD - Remove .NET 5/6/7 from `#if` directives and build - Delete commented-out file - Optimize NETSTANDARD encoding polyfills used in hot paths to eliminate intermediate array allocations - Fix `PeriodicTimer` CancellationTokenRegistration leak - Add encoding polyfill benchmark * Fix `PeriodicTimer` cancellation to prevent leak - Ensure static lambda used to avoid allocation.
Switch project dependency from `NATS.Net` to `NATS.Client.Simplified` for better modularity. Add `NATS.Extensions.Microsoft.DependencyInjection` to `NATS.Net` dependencies. Update documentation to include comparisons between `NATS.Extensions.Microsoft.DependencyInjection` and `NATS.Client.Hosting`, clarifying usage scenarios. Add new DI guide to advanced docs.
# Conflicts: # src/NATS.Client.Core/NATS.Client.Core.csproj
# Conflicts: # version.txt
Merged main
# Conflicts: # src/NATS.Client.Core/NATS.Client.Core.csproj # src/NATS.Client.Core/NKeyPair.cs # version.txt
* Add support for optional NATS headers in serialization/deserialization * Make `Serialize` method header-aware and add unit tests for header serialization logic * Add recursion guard to prevent infinite loops in default interface methods for `Serialize`/`Deserialize`, update tests and examples accordingly * Fix `Content-Type` header deserialization by updating to use `StringValues` * Fix NuidTests flap * Improve NuidTests stability * Remove old default methods * Remove `IDictionary` implementation from `NatsHeaders` * Add INatsSerializeWithHeaders * serializer: remove unnecessary obsolete warning pragmas The CS0618 suppressions were leftover from the DIM approach where interface members were marked obsolete. No longer needed with the separate interface design. * tests: add serialization extension and ABI checks Add unit tests for NatsSerializationExtensions covering both the header-aware path and the fallback path for serialize and deserialize. Make the ABI check program assert correctness instead of just printing, including the new WithHeaders interfaces. * serializer: replace WithHeaders with WithContext interfaces Replace INatsSerializeWithHeaders<T>/INatsDeserializeWithHeaders<T> with INatsSerializeWithContext<T>/INatsDeserializeWithContext<T>, passing a NatsMsgContext struct that carries Subject, ReplyTo, and Headers. This gives serializers access to the full message envelope, enabling subject-based dispatch and reply-to inspection without requiring further interface changes in the future. Also fixes the null-headers asymmetry in the extension methods: both Serialize and Deserialize now always dispatch to the context overload when the interface is implemented, regardless of whether Headers is null. * Revert "Improve NuidTests stability" This reverts commit 42b8cbd. * Revert "Fix NuidTests flap" This reverts commit dcc3588. * docs: fix stale DIM references and update serialization docs Remove incorrect DIM claims from serialization docs, rewrite the headers section to describe the WithContext interface design. Clean up NativeAot example that had leftover INatsHeaders parameter overloads from the old DIM approach. Restore dropped <returns> doc comment on INatsDeserialize<T>.Deserialize. * headers: drop read-only state The read-only flag was set on NatsHeaders before passing to publish to catch accidental mutation after publish. It was a weak guarantee since headers are copied to a byte buffer synchronously inside CommandWriter, so post-publish mutation cannot affect wire data anyway. Removing it lets serializers implementing INatsSerializeWithContext<T> mutate headers (e.g. set Content-Type) during serialization, which was otherwise blocked by ThrowIfReadOnly. Telemetry's SetOverrideReadOnly escape hatch is no longer needed and uses the normal indexer instead. * tests: add net10.0 TFM and improve ABI checks Add net10.0 target to CoreUnit, Core, and Core2 test projects. Add CheckAbiOld control project that runs against the old NuGet version to prove type forwarding works by contrasting assembly versions. Centralize the old NuGet version in Directory.Build.props and make the run script assert loaded assembly versions. * tests: fix NU1510 package pruning errors on net10.0 Condition System.Net.Http and System.Text.RegularExpressions package references to net481 only, where they are actually needed. On net10.0 these are built-in and NuGet's package pruning treats the unnecessary references as errors. * serializers: address context review feedback - NatsMsgContext: require Subject via constructor; drop init setters so netstandard shim for IsExternalInit is no longer needed. - Add INatsSerializerWithContext<T> as a combined interface mirroring INatsSerializer<T>, simplifying class declarations for context-aware serializer+deserializer pairs. - Built-in NatsUtf8PrimitivesSerializer, NatsRawSerializer and NatsJsonContextSerializer now implement INatsSerializerWithContext<T> and propagate the context through _next when delegating. Without this, chaining a built-in with a context-aware leaf silently dropped context at the leaf. - Document NatsHeaders thread-safety: not safe to share across concurrent publishes, since readonly-locking was removed. - Document that header-mutating serializers must get non-null headers from the caller; the library does not allocate on their behalf. - Add chain-propagation tests for serialize and deserialize paths. * sln: remove x64/x86 platform noise
* core: use System.Threading.Lock on NET9_0_OR_GREATER * core: fix reentrant lock in PublishToClientHandlersAsync * bench: add lock and publish runtime comparison benchmarks
# Conflicts: # version.txt
* Bound reconnect-loss test by reconnect count The loss <= 1% threshold tracks reconnects/sent, so under CI CPU contention the forced-disconnect cadence balloons (seen 7% loss, 40+ reconnects) and the absolute percentage trips. Bound loss by reconnect count instead: each disconnect can drop at most the in-flight msg(s). The no-duplicate check (received <= sent) is unchanged. * Skip first ping in slow-consumer consume RTT max The warm-up ping added earlier did not fully fix the flap: the first measured ping still legitimately catches the tail of the 100-message publish burst and spikes near the 1000ms threshold. The sibling fetch test already skips i==0 from the max for this reason; apply the same guard to the consume test. All 20 pings must still succeed. * Widen fetch expiry in ordered-consumer timeout test The test swallows idle heartbeats and expects exactly one heartbeat timeout notification, which fires at 2x IdleHeartbeat (2s). With expiry at 5s the server-side request timeout (408) raced the client heartbeat timer: under CI thread-pool contention the Timer callback slips past 5s, the fetch ends on the 408 with no notification, and the count is 0. Widen expiry to 20s so the heartbeat timeout reliably wins; happy-path runtime is unchanged since the timer still fires at ~2s. * Throttle publisher in reconnect-loss test The reconnect-count bound still tripped (lost 71 vs 2x of 29 reconnects): publishes coalesce into the unflushed write buffer, so a single forced disconnect drops the whole batch and loss scales with publish rate under CI contention. Throttle the send loop so in-flight stays ~1; each disconnect then drops at most that one message and the bound holds with margin (observed 0 loss locally). The no-duplicate check is unchanged.
* core: always validate subjects Always run ValidateSubject / ValidateReplyTo / ValidateQueueGroup on the publish, subscribe, and request paths instead of gating them behind NatsOpts.SkipSubjectValidation. Subject validation rejects empty and whitespace-bearing subjects, which is what blocks CRLF protocol injection. Making that an opt-out was the wrong default to offer: the cost is negligible next to the network round trip, and a caller who controls all subjects gains nothing measurable by disabling it. SkipSubjectValidation is kept but marked obsolete and ignored, so existing code keeps compiling (with a warning) rather than breaking. It will be removed in a future release. * core: deprecate SkipSubjectValidation Mark NatsOpts.SkipSubjectValidation obsolete rather than removing it. The option still works: setting it true bypasses subject, reply-to, and queue-group validation on the publish, subscribe, and request paths. Disabling validation is discouraged. The likely failure is mundane, not exotic: a subject that merely contains a space or tab is split into tokens on the wire. "PUB foo bar 5" is read by the server as subject "foo" with reply-to "bar", and "SUB foo bar 1" as subject "foo" joined to queue group "bar", so the call is silently misrouted with no error. Validation also blocks CRLF protocol injection. Either way the saving is negligible next to the network round trip, so the obsolete warning tells callers to leave validation on and signals the option may be removed in a future release. Internal call sites and the tests/benchmark that exercise the option suppress CS0618 locally so the warnings-as-errors build stays clean.
* core: default request-reply to Direct mode Flip the NatsOpts.RequestReplyMode default from SharedInbox to Direct. Direct skips per-request subscription setup and channel-based delivery, correlating replies through the connection's inbox subscription instead. Direct mode previously ignored ThrowIfNoResponders and always returned the 503 sentinel as a message. Make it honor the flag so RequestAsync behaves the same in both modes. The JetStream publish and API paths inspect the sentinel themselves to retry, so they now opt out with ThrowIfNoResponders=false, matching the shared-inbox path. Tests that assert exact wire frames, subscription sids, or active subscription counts are pinned to SharedInbox: Direct opens an inbox subscription at connect, which shifts those values. * core: preserve no-responders behavior for explicit Direct Honoring ThrowIfNoResponders in Direct mode (previous commit) changed no-responders from "return a message" to "throw" for callers that had intentionally selected Direct. Record whether Direct was set explicitly (vs inherited as the new default) and default such requests to not throw, restoring their pre-3.x behavior. Default and SharedInbox connections keep throwing. The flag is captured in the RequestReplyMode init accessor; the field initializer bypasses it, so an unset mode is treated as the throwing default. RequestManyAsync always uses the shared inbox path and is scoped out of the opt-out. A per-call NatsSubOpts.ThrowIfNoResponders still wins. * test: assert default request-reply mode is Direct * test: disable test collection parallelization MaxParallelThreads was 1.0x (one thread per core), so collections ran concurrently. Suites that assert on process-global state, notably the OpenTelemetry tests whose ActivityListener and MeterListener observe every NATS.Net activity and measurement in the process, then see another collection's activities and counters and fail with false positives. Defaulting request-reply to Direct made this worse by routing JetStream publish through the instrumented RequestAsync path. Run collections serially across all test projects.
NatsClient and the DI builder forced SubPendingChannelFullMode to Wait, while NatsConnection defaulted to DropNewest, so overflow behavior differed by entry point. Wait applies backpressure to the socket read loop and risks the server disconnecting the client as a slow consumer. Make all entry points inherit the NatsOpts default DropNewest and raise SubPendingChannelCapacity from 1024 to 16384 so overflow stays rare. Drop the now-redundant pending parameter from the NatsClient(NatsOpts) constructor. Breaking for NatsClient and DI users under sustained overload: messages are dropped (surfaced via MessageDropped) instead of blocking.
* core: add explicit subscription drain API Expose DrainAsync on INatsSub<T> so callers can stop receiving new messages and finish reading already-buffered messages while keeping the connection usable, instead of only getting drain behaviour on the dispose path behind DrainSubscriptionsOnDispose. The drain mechanics (UNSUB, PING/PONG fence, complete channel) move into a shared core. The dispose-path entry point is renamed to DrainOnDisposeAsync and keeps the opt-in flag gate and reader wait. * core: clarify drain cancellation and DrainPingTimeout docs * jetstream: add opt-in consume drain on cancel Cancelling ConsumeAsync was an abrupt stop that abandoned messages already buffered in the consumer, so handlers could not finish post-yield work like acking on a still-open connection. Add NatsJSConsumeOpts.DrainOnCancel: when set, cancellation drains the consumer (stop pulling, fence in-flight with PING/PONG, complete the channel) and keeps the connection usable. Default false preserves the previous behavior. * test: tidy consume drain tests Drop the unused ITestOutputHelper field and tighten the no-drain assertion: cancellation fires after the first message, so assert a prompt stop rather than just fewer than the total. * test: run xunit tests single-threaded Set MaxParallelThreads to 1 (was 1.0x, i.e. one thread per core). Integration tests share a server per collection and several historic flaps came from collections racing each other under parallelism; serialising removes that axis at the cost of wall-clock time. * test: fix Reconnect_counter racing the metric increment The reconnects counter is incremented just after the ConnectionOpened event is queued, so the test, which waited on the event and read the metric once, could observe zero. Poll for the measurement after the reconnect, and make MeterTracker hand out locked snapshots since callbacks fire on background threads. Reproduced in isolation (failed 6/6, now passes). * core: fix subscription drain hang and message loss DrainCoreAsync could skip TryComplete() when the UNSUB or PING threw an unexpected exception (e.g. ObjectDisposedException if the connection is disposed mid-drain), leaving the message channel forever incomplete; a drain-on-cancel consumer reading with CancellationToken.None then hangs. Wrap the unsubscribe and ping in one try, treat the dispose race as best-effort, and complete the channel and clear routing in a finally so a reader is never left waiting. Disarm the lifecycle timers before taking the unsubscribe gate so a timeout callback is less likely to win the gate and complete the channel without the drain fence. For JetStream consume, the idle-heartbeat timer could re-arm during a drain and complete the user channel ahead of the PING/PONG fence, dropping in-flight messages that drain-on-cancel is meant to deliver. Track a draining flag so the heartbeat callback, its re-arm, and CompleteStop defer to the drain. * jetstream: drain consumer delivery via overridable hook The base drain only fenced the UNSUB with a PING/PONG; stopping a subclass delivery engine (the JetStream pull loop and idle-heartbeat timer) was done ad hoc in NatsJSConsume's DrainAsync override, so only the explicit cancel path stopped it. The dispose-drain path left the heartbeat running, where it could complete the user channel ahead of the fence and drop in-flight messages. Add a StopDelivery hook on NatsSubBase, called at the start of the shared drain core so both the explicit and dispose-drain paths stop the engine before the fence. NatsJSConsume overrides the hook instead of the whole DrainAsync. * test: cover drain in-flight message fence The existing drain tests buffer every message before asserting (publish then PING, or a count that fits a single pull), so they pass even if the UNSUB+PING fence is removed. Add a test that publishes without a pre-drain PING, so the tail is still in flight when the UNSUB is sent, and asserts all of it is delivered. * jetstream: scope DrainOnCancel docs to the pull consumer * core: clarify DrainAsync fence timeout docs * jetstream: remove redundant drain-start in consume loop The consume loop started the drain in two places: a top-of-loop check for an already-cancelled token and the WaitToReadAsync cancellation catch. WaitToReadAsync throws OperationCanceledException when the token is already cancelled (even with buffered messages), so the catch already covers the cancel-before-read case identically. Drop the top-of-loop block and let the catch be the single place that reacts to cancellation. * core: complete drain channel even if teardown throws The hang fix put TryComplete() in a finally, but the later StopDelivery hook and the lifecycle-timer disarm ran before the try, so if any of them threw (StopDelivery is virtual and calls Timer.Change, which can throw on a timer disposed by a racing DisposeAsync) the finally was never reached and a drain-on-cancel reader blocked on the channel could hang again. Take the unsubscribe gate first, then run the teardown (decrement, StopDelivery, timer disarms, UNSUB, PING) inside the try so TryComplete() always runs. The timers are now disarmed after the gate rather than before it; the gate was already the real protection (once it is taken a later timer callback bails on the unsubscribe gate), so the prior before-gate disarm only narrowed a microscopic window and is not worth skipping the completion guard for. * test: assert drain message count before order
* test: retry-gate positive-path test connections Many integration tests construct a NatsConnection and exercise it without first gating on a successful connect. The default ConnectTimeout is 2s and covers the wait for the server INFO frame; when CI runs multiple TFMs of a project in parallel, that 2s wait loses under load and the connect throws a TimeoutException before the test's own logic runs. The failure then points at an unrelated test rather than the connect, and the assertions never ran. Gate positive-path connections with the existing ConnectRetryAsync helper (retries ConnectAsync for up to 60s), and convert plain ConnectAsync setup gates to it. Negative-path tests (expected connect failures/timeouts, reconnect/cancel/TLS/auth behaviour) are left as-is. * test: fix request-reply and slow-consumer flakes Two timing flakes distinct from the connect-timeout class: Request_reply_many_* sent the request before the responder's SUB was registered server-side, so under load the request could race ahead and get zero replies. Fence the responder with a PING/PONG round-trip before requesting, matching the existing pattern in this file. SlowConsumerDetected_fires_again_after_recovery waited for an absolute slowConsumerCount >= 2, but episode 1 alone can reach 2, so the episode-2 wait could pass without a new event and the >-than assertion then failed. Wait for a count strictly greater than the episode-1 snapshot instead.
Add an Abstractions-only test project (references just NATS.Client.Abstractions, no Core) covering the issue #851 use case: a third party such as a CloudEvents adapter can deconstruct a received message and build an outgoing one using only the envelope (NatsMsgContext / INatsHeaders) and the serializer interfaces. Wire it into the Linux and Windows CI runs.
net6.0 was already removed from the target frameworks (netstandard2.0, netstandard2.1, net8.0, net10.0). Update the platform-compatibility page and fix csproj/script comments that still described netstandard dependency groups as net6.0.
consumed.messages and received.bytes count only messages delivered to the application, per the OTel messaging convention. NATS status/control frames (no-responder 503s, JetStream heartbeats, flow-control, protocol notifications) are consumed internally and never reach the application, but the base receive path counted them. Skip them via the inline status code on the version line, which covers every subscription type through one chokepoint.
* jetstream: record operation.duration for ack Wire the SendAckAsync path so every Ack* variant records the messaging.client.operation.duration histogram with messaging.operation=ack, including error.type on failure, matching the publish/request/subscribe paths. * core: add dropped-messages counter Emit nats.client.messages.dropped on the OnMessageDropped path under the NATS.Net meter, reusing the receive tag set so it correlates with consumed.messages. Pending channel depth stays on the MessageDropped event rather than becoming an unbounded tag value. * core: collapse inbox subjects in trace tags Inbox subjects (_INBOX.<nuid>) are unique per request, so emitting them on indexed activity tags (destination.name, destination_publish.name, nats.message.subject, reply_to) pushes unbounded cardinality into tracing backends. Collapse them to "inbox", matching SpanDestinationName; the raw subject remains reachable through the Enrich callback. * telemetry: document inbox-prefix and ack-op choices [no ci]
Move INatsSocketConnection and INatsTlsUpgradeableSocketConnection to NATS.Client.Abstractions so custom transport implementers can depend on the contract without referencing Core, same rationale as the serialization interfaces already there. Both types are BCL-only. Keep the NATS.Client.Core namespace on the moved types and add TypeForwardedTo in Core to preserve source and binary compatibility. The obsolete ISocketConnection and INatsSocketConnectionFactory stay in Core; the factory takes NatsOpts so it cannot move without dragging Core options along. Add Microsoft.Bcl.AsyncInterfaces to the Abstractions netstandard2.0 group for IAsyncDisposable and ValueTask, which are not in that target's BCL.
* otel: add OpenTelemetry package * otel: add options configure overload Mirrors AddRabbitMQInstrumentation(configure): exposes a discoverable entry point to set Filter and Enrich instead of reaching into the Core static. Writes the process-wide NatsInstrumentationOptions.Default, so configuration is global, not per provider. * otel: add subject-pattern trace filter helper FilterSubjects compiles NATS subject patterns (include/exclude, with * and > wildcards) into a NatsInstrumentationOptions.Filter predicate, combined with any existing filter. Document it alongside the operation.duration histogram bucket view, which stays out of the package to avoid pulling in the OpenTelemetry SDK dependency.
* Reduce allocation while calculating span name Signed-off-by: Zhen Wu <colprog@gmail.com> * Add span destination name formatter Signed-off-by: Zhen Wu <colprog@gmail.com> --------- Signed-off-by: Zhen Wu <colprog@gmail.com>
Use SearchValues for header key validation on .NET 8+ while preserving fallback behavior for older targets. Add microbenchmarks for the new SearchValues variants and a byte-boundary test for header key validation.
* objectstore: simplify base64url encoder Replace the hand-rolled bit-twiddling encode loop with the in-box Base64.EncodeToUtf8 (vectorized, available on every target framework via System.Memory) plus an in-place '+'/'/' -> '-'/'_' swap. The '=' padding is kept so output stays byte-for-byte compatible with the padded base64.URLEncoding other clients use for digests and metadata subject names. On read, compare the digest as spans instead of building a "SHA-256=" interpolated string per object. Co-authored-by: to11mtm <12536917+to11mtm@users.noreply.github.com> * Update src/NATS.Client.ObjectStore/Internal/Encoder.cs Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com> * objectstore: encode base64url straight to chars The encoder built the string by encoding to UTF8 bytes and then transcoding with Encoding.ASCII.GetString. Encode straight to a char span instead and build the string from it, dropping the transcode: net9+ uses Base64Url (url-safe in one pass), net8 and netstandard2.1 use Convert.TryToBase64Chars, and netstandard2.0 keeps the byte path since it has no span-based encode-to-chars. The per-framework branches are flattened into a single #if chain. * objectstore: cover base64url encoder on all frameworks Move the encoder test into a lean project that references only the ObjectStore assembly so it can multi-target down to net6.0; the integration test project cannot, as some of its tests do not build on net6.0. Each test TFM pins a distinct compiled branch via asset selection: net6.0 -> netstandard2.1, net8.0 -> net8.0, net10.0 -> net9+, net481 -> netstandard2.0 (Windows only). Add a BenchmarkDotNet project under sandbox that compares the same branches side by side with per-runtime jobs. * ci: run encoder tests and target net10 sdk Run the new encoder tests on Linux (net6/8/10) and Windows (also net481). Install the net10 SDK explicitly instead of relying on the runner image happening to ship it, and drop the unused net9 SDK as no project targets net9. * objectstore: stackalloc table encoder below net9 net9+ keeps Base64Url. Below net9 there is no url-safe base64 encoder, so use the lookup table; its 63rd/64th entries are '-'/'_', so it maps to the url-safe alphabet directly and needs no post-swap. Encode into a stack or pooled buffer instead of a fresh char[], so only the result string is allocated: half the allocations of the original encoder with no measurable CPU change, and net9+ is faster on top of that. * bench: compare base64url encoder on net8 and net10 Below net9 every build shares one code path (the lookup table), so net8 represents it and net10 covers Base64Url; drop the net6/net481 jobs. Keep a table baseline next to the current encoder for comparison. * bench: rename benchmark project to MicroBench2 Make it a general lean BenchmarkDotNet host (only the project under test plus BenchmarkDotNet, no heavier third-party deps like MicroBenchmark) so future minimal-dependency micro-benchmarks can be added alongside the base64url encoder one. * objectstore: use unchecked pointer loop in table encoder * ci: install net6/net8/net10 sdks for apicompat The apicompat job builds the whole solution, which now includes the encoder test project targeting net6.0. Building net6.0 needs the net6.0 apphost pack, absent when only the 8.x SDK is installed. Match the test workflows and install 6.x/8.x/10.x. * bench: add net481 to base64url encoder benchmark * objectstore: test Sha256 digest base64url encoding --------- Co-authored-by: to11mtm <12536917+to11mtm@users.noreply.github.com> Co-authored-by: Miha Zupan <mihazupan.zupan1@gmail.com>
The Default instance is configured at startup but its callbacks are read on the publish/subscribe path from other threads. Back the properties with volatile fields so a configuration change made after traffic has started is observed by those readers instead of relying on incidental barriers.
…1206) * otel: match subject filters without per-message allocation FilterSubjects split the subject into a string[] on every traced operation. Walk the subject token by token with spans instead; the patterns are still tokenized once at configuration time. Adds coverage for empty-token boundaries. * otel: clarify invalid-subject matcher comments and test names
The non-NatsConnection receive path allocated a fixed 10-slot tag array but
only filled the reply-to slot when a reply-to was present, leaving a default
{null, null} entry that was handed to StartActivity. Size the array to the
tags actually set, matching the send path.
The two DI entry points are easy to confuse. Document AddNatsClient, which had no XML doc, and rewrite the AddNats summary to state that it registers no INatsClient and no JSON serializer; each package now points at the other for the case it does not cover. Also document the public NatsBuilder surface. Drop the keyed-services usage example from the platform compatibility page since it duplicated the dependency injection page; keep the per-target-framework signature difference and link out for usage.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
NATS .NET v3.0. Adds .NET 10 and drops .NET 6 (target frameworks: netstandard2.0, netstandard2.1, net8.0, net10.0).
Changes on this branch (preview release bumps omitted):
Remaining: