Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0434d7a
Add .NET 10 Target
mtmk Feb 13, 2026
e98549e
Release 3.0.0-preview.1 (#1071)
mtmk Feb 18, 2026
5227390
Merge branch 'main' into release/3.0
mtmk Feb 25, 2026
90eac9a
Clean up NET6 and optimize NETSTANDARD (#1072)
mtmk Feb 27, 2026
7f80782
Update DI package dependencies and documentation (#1075)
mtmk Feb 27, 2026
87f755c
Merge branch 'main' into release/3.0
mtmk Mar 4, 2026
f4813d4
Merge branch 'main' into release/3.0
mtmk Mar 13, 2026
294cbae
Merge branch 'main' into release/3.0
mtmk Mar 13, 2026
4f68635
Release 3.0.0-preview.2 (#1091)
mtmk Mar 13, 2026
9f24089
Merge branch 'main' into release/3.0
mtmk Mar 31, 2026
854da34
Merge branch 'main' into release/3.0
mtmk Apr 2, 2026
a0a7270
Merge branch 'main' into release/3.0
mtmk Apr 10, 2026
212f6e6
Merge branch 'main' into release/3.0
mtmk Apr 16, 2026
710bbef
Add message context to serialization interfaces (#1082)
mtmk Apr 16, 2026
357a705
Release 3.0.0-preview.3 (#1113)
mtmk Apr 17, 2026
376abf8
Use System.Threading.Lock on NET9_0_OR_GREATER (#1118)
mtmk May 1, 2026
c7b71f9
Merge branch 'main' into release/3.0
mtmk May 1, 2026
a5c1121
Release 3.0.0-preview.4 (#1123)
mtmk May 1, 2026
7c6eb55
merge main into release/3.0
mtmk May 7, 2026
446820b
Release 3.0.0-preview.5 (#1130)
mtmk May 7, 2026
d7d3059
merge main into release/3.0
mtmk May 8, 2026
fc8ee8d
Merge remote-tracking branch 'origin/main' into release/3.0
mtmk May 13, 2026
dce37ca
Merge remote-tracking branch 'origin/main' into release/3.0
mtmk May 13, 2026
741f458
Release 3.0.0-preview.6 (#1141)
mtmk May 14, 2026
ac8a455
Merge branch 'main' into release/3.0
mtmk May 15, 2026
7c8d90f
di: clarify package descriptions for NuGet [no ci]
mtmk May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

<!-- Do not package by default to avoid accidental publishing -->
<IsPackable>false</IsPackable>

<!-- NuGet version used by ABI compatibility checks -->
<NatsAbiCheckVersion>2.6.0</NatsAbiCheckVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@

# NATS .NET

> [!IMPORTANT]
> **NATS .NET v3 is in development on the [`release/3.0`](https://github.com/nats-io/nats.net/tree/release/3.0) branch!**
> v3 adds .NET 10 support and drops .NET 6.0. Target frameworks: `netstandard2.0`, `netstandard2.1`, `net8.0`, `net10.0`.

NATS .NET is the .NET client for NATS, a distributed messaging system.
It provides pub/sub and request/reply (Core NATS), streaming and persistence (JetStream),
Key-Value Store, Object Store, and Services.
Expand Down Expand Up @@ -83,19 +87,14 @@ var obj = nc.CreateObjectStoreContext();
var svc = nc.CreateServicesContext();
```

> [!NOTE]
> **We are not testing with .NET 6.0 target anymore** even though it is still targeted by the library.
> This is to reduce the number of test runs and speed up the CI process as well as to prepare for
> the next major version, possibly dropping .NET 6.0 support.

> [!NOTE]
> **Don't confuse NuGet packages!**
> NATS .NET package on NuGet is called [NATS.Net](https://www.nuget.org/packages/NATS.Net).
> There is another package called `NATS.Client` which is the older version of the client library
> and will be deprecated eventually.

> [!TIP]
> NATS .NET now supports **.NET Standard** 2.0 and 2.1 along with .NET 6.0 and 8.0,
> NATS .NET supports **.NET Standard** 2.0 and 2.1 along with .NET 8.0 and 10.0,
> which means you can also use it with **.NET Framework** 4.6.2+ and **Unity** 2018.1+.

### What is NATS?
Expand All @@ -111,7 +110,7 @@ Head over to [NATS documentation](https://docs.nats.io/nats-concepts/overview) f
## NATS .NET Goals

- Only support Async I/O (async/await)
- Target .NET Standard 2.0, 2.1, and the two most recent .NET LTS releases (currently .NET 6.0 & .NET 8.0)
- Target .NET Standard 2.0, 2.1, and the two most recent .NET LTS releases (currently .NET 8.0 & .NET 10.0)

## Packages

Expand All @@ -123,7 +122,8 @@ Head over to [NATS documentation](https://docs.nats.io/nats-concepts/overview) f
- **NATS.Client.Services**: [Services](https://docs.nats.io/using-nats/developer/services)
- **NATS.Client.Simplified**: simplify common use cases especially for beginners
- **NATS.Client.Serializers.Json**: JSON serializer for ad-hoc types
- **NATS.Extensions.Microsoft.DependencyInjection**: extension to configure DI container
- **NATS.Client.Hosting**: DI integration for AOT-compatible and minimal-dependency scenarios
- **NATS.Extensions.Microsoft.DependencyInjection**: DI integration with ad-hoc JSON serialization enabled by default

## Client and Orbit

Expand Down
143 changes: 143 additions & 0 deletions sandbox/MicroBenchmark/EncodingPolyfillBench.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using System.Buffers;
using System.Text;
using BenchmarkDotNet.Attributes;

namespace MicroBenchmark;

/// <summary>
/// Benchmarks comparing old (allocating) vs new (zero-alloc) encoding polyfills
/// used in NETSTANDARD builds for ProtocolWriter, HeaderWriter, and ReadProtocolProcessor.
/// </summary>
[MemoryDiagnoser]
public class EncodingPolyfillBench
{
private const string ShortSubject = "foo.bar.baz";
private const string MediumSubject = "my-service.requests.user.profile.update";
private const string LongHeader = "X-Nats-Stream: my-persistent-stream-name-that-is-quite-long";

private static readonly Encoding Utf8 = Encoding.UTF8;
private static readonly Encoding Ascii = Encoding.ASCII;

private byte[] _shortBytes = null!;
private byte[] _mediumBytes = null!;
private byte[] _longBytes = null!;
private byte[] _destBuffer = null!;

[GlobalSetup]
public void Setup()
{
_shortBytes = Ascii.GetBytes(ShortSubject);
_mediumBytes = Ascii.GetBytes(MediumSubject);
_longBytes = Utf8.GetBytes(LongHeader);
_destBuffer = new byte[256];
}

[Benchmark(Description = "GetBytes_Old_Short")]
public int GetBytes_Old_Short() => GetBytesOld(Ascii, ShortSubject, _destBuffer);

[Benchmark(Description = "GetBytes_New_Short")]
public int GetBytes_New_Short() => GetBytesNew(Ascii, ShortSubject, _destBuffer);

[Benchmark(Description = "GetBytes_Old_Medium")]
public int GetBytes_Old_Medium() => GetBytesOld(Ascii, MediumSubject, _destBuffer);

[Benchmark(Description = "GetBytes_New_Medium")]
public int GetBytes_New_Medium() => GetBytesNew(Ascii, MediumSubject, _destBuffer);

[Benchmark(Description = "GetBytes_Old_Long")]
public int GetBytes_Old_Long() => GetBytesOld(Utf8, LongHeader, _destBuffer);

[Benchmark(Description = "GetBytes_New_Long")]
public int GetBytes_New_Long() => GetBytesNew(Utf8, LongHeader, _destBuffer);

[Benchmark(Description = "GetString_Old_Short")]
public string GetString_Old_Short() => GetStringOld(Ascii, _shortBytes);

[Benchmark(Description = "GetString_New_Short")]
public string GetString_New_Short() => GetStringNew(Ascii, _shortBytes);

[Benchmark(Description = "GetString_Old_Medium")]
public string GetString_Old_Medium() => GetStringOld(Ascii, _mediumBytes);

[Benchmark(Description = "GetString_New_Medium")]
public string GetString_New_Medium() => GetStringNew(Ascii, _mediumBytes);

[Benchmark(Description = "GetString_Old_Long")]
public string GetString_Old_Long() => GetStringOld(Utf8, _longBytes);

[Benchmark(Description = "GetString_New_Long")]
public string GetString_New_Long() => GetStringNew(Utf8, _longBytes);

[Benchmark(Description = "GetBytesWriter_Old_Medium")]
public void GetBytesWriter_Old_Medium()
{
var writer = new ArrayBufferWriter<byte>(256);
GetBytesWriterOld(Utf8, MediumSubject, writer);
}

[Benchmark(Description = "GetBytesWriter_New_Medium")]
public void GetBytesWriter_New_Medium()
{
var writer = new ArrayBufferWriter<byte>(256);
GetBytesWriterNew(Utf8, MediumSubject, writer);
}

private static int GetBytesOld(Encoding encoding, string chars, Span<byte> bytes)
{
var buffer = encoding.GetBytes(chars);
buffer.AsSpan().CopyTo(bytes);
return buffer.Length;
}

private static string GetStringOld(Encoding encoding, ReadOnlySpan<byte> buffer)
{
return encoding.GetString(buffer.ToArray());
}

private static void GetBytesWriterOld(Encoding encoding, string chars, IBufferWriter<byte> bw)
{
var buffer = encoding.GetBytes(chars);
bw.Write(buffer);
}

private static unsafe int GetBytesNew(Encoding encoding, string chars, Span<byte> bytes)
{
if (chars.Length == 0)
{
return 0;
}

fixed (char* charPtr = chars)
{
fixed (byte* bytePtr = bytes)
{
return encoding.GetBytes(charPtr, chars.Length, bytePtr, bytes.Length);
}
}
}

private static unsafe string GetStringNew(Encoding encoding, ReadOnlySpan<byte> buffer)
{
if (buffer.IsEmpty)
{
return string.Empty;
}

fixed (byte* ptr = buffer)
{
return encoding.GetString(ptr, buffer.Length);
}
}

private static void GetBytesWriterNew(Encoding encoding, string chars, IBufferWriter<byte> bw)
{
var byteCount = encoding.GetByteCount(chars);
var span = bw.GetSpan(byteCount);
#if NET8_0_OR_GREATER
encoding.GetBytes(chars.AsSpan(), span);
#else
GetBytesNew(encoding, chars, span);
#endif
bw.Advance(byteCount);
}
}
61 changes: 61 additions & 0 deletions sandbox/MicroBenchmark/LockBench.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace MicroBenchmark;

// Compares object lock (all TFMs) vs System.Threading.Lock (NET9+).
// On net8.0 both methods use object, so they serve as a same-TFM baseline.
// On net10.0 SystemLock uses System.Threading.Lock whose EnterScope() path
// avoids the Monitor sync-block machinery.
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net10_0)]
[MemoryDiagnoser]
public class LockBench
{
private readonly object _objectLock = new();

#if NET9_0_OR_GREATER
private readonly System.Threading.Lock _systemLock = new();
#else
private readonly object _systemLock = new();
#endif

private int _value;

// Uncontended empty lock/unlock -- measures raw locking overhead.
[Benchmark(Baseline = true)]
public void ObjectLock_Empty()
{
lock (_objectLock)
{
}
}

[Benchmark]
public void SystemLock_Empty()
{
lock (_systemLock)
{
}
}

// Uncontended lock with a trivial increment -- closer to real usage where
// the lock body does a small amount of work.
[Benchmark]
public void ObjectLock_Increment()
{
lock (_objectLock)
{
_value++;
}
}

[Benchmark]
public void SystemLock_Increment()
{
lock (_systemLock)
{
_value++;
}
}
}
8 changes: 4 additions & 4 deletions sandbox/MicroBenchmark/MicroBenchmark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net8.0</TargetFrameworks>
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<TargetFrameworks Condition="'$(OS)' == 'Windows_NT'">$(TargetFrameworks);net481</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>12</LangVersion>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<!-- Resolve version conflict between BenchmarkDotNet dependencies on net481 -->
<PackageReference Include="System.Reflection.Metadata" Version="8.0.1" Condition="'$(TargetFramework)' == 'net481'" />
<PackageReference Include="System.Reflection.Metadata" Version="9.0.0" Condition="'$(TargetFramework)' == 'net481'" />
<PackageReference Include="StackExchange.Redis" Version="2.8.24" />
<PackageReference Include="ZLogger" Version="2.5.10" />
<PackageReference Include="NATS.Client" Version="1.1.6" />
Expand Down
44 changes: 44 additions & 0 deletions sandbox/MicroBenchmark/PublishRuntimeBench.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using NATS.Client.Core;

#pragma warning disable CS8618

namespace MicroBenchmark;

// Tight serial publish loop across runtimes.
// Measures end-to-end throughput including buffering, pipe writes, and socket I/O.
// The Lock change affects CommandWriter._lock on each publish, but the body under
// the lock dominates, so any visible delta here reflects runtime improvements broadly.
// Requires a running NATS server on localhost:4222.
[SimpleJob(RuntimeMoniker.Net80)]
[SimpleJob(RuntimeMoniker.Net10_0)]
[MemoryDiagnoser]
public class PublishRuntimeBench
{
private const int Msgs = 100_000;
private static readonly byte[] Payload = new byte[128];

private NatsConnection _nats;

[GlobalSetup]
public async Task Setup()
{
_nats = new NatsConnection();
await _nats.ConnectAsync();
}

[GlobalCleanup]
public async Task Cleanup() => await _nats.DisposeAsync();

[Benchmark]
public async Task Publish100k()
{
for (var i = 0; i < Msgs; i++)
{
await _nats.PublishAsync("bench", Payload);
}

await _nats.PingAsync();
}
}
8 changes: 4 additions & 4 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
<Import Project="..\Directory.Build.props" />

<PropertyGroup>
<TargetFrameworks>netstandard2.0;netstandard2.1;net6.0;net8.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

<IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">true</IsTrimmable>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible>
<IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsTrimmable>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>

<!-- NuGet Packaging -->
<Version>$([System.IO.File]::ReadAllText("$(MSBuildThisFileDirectory)..\version.txt"))</Version>
<PackageVersion>$(Version)</PackageVersion>
Expand Down
10 changes: 10 additions & 0 deletions src/NATS.Client.Abstractions/INatsHeaders.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.Extensions.Primitives;

namespace NATS.Client.Core;

/// <summary>
/// Represents NATS message headers as a dictionary of string keys and <see cref="StringValues"/> values.
/// </summary>
public interface INatsHeaders : IDictionary<string, StringValues>
{
}
Loading