{"id":13677801,"url":"https://github.com/Cysharp/AlterNats","last_synced_at":"2025-04-29T12:32:17.227Z","repository":{"id":37466919,"uuid":"472553845","full_name":"Cysharp/AlterNats","owner":"Cysharp","description":"An alternative high performance NATS client for .NET.","archived":true,"fork":false,"pushed_at":"2023-07-17T07:54:29.000Z","size":17689,"stargazers_count":284,"open_issues_count":5,"forks_count":26,"subscribers_count":12,"default_branch":"master","last_synced_at":"2024-10-28T22:41:56.916Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Cysharp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2022-03-22T00:03:23.000Z","updated_at":"2024-10-04T01:16:31.000Z","dependencies_parsed_at":"2024-01-14T17:24:20.942Z","dependency_job_id":null,"html_url":"https://github.com/Cysharp/AlterNats","commit_stats":{"total_commits":132,"total_committers":7,"mean_commits":"18.857142857142858","dds":"0.20454545454545459","last_synced_commit":"ca4976b78585bf129b2dd578e6a5aaaa9745b82e"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cysharp%2FAlterNats","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cysharp%2FAlterNats/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cysharp%2FAlterNats/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Cysharp%2FAlterNats/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Cysharp","download_url":"https://codeload.github.com/Cysharp/AlterNats/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224173224,"owners_count":17268072,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-02T13:00:47.245Z","updated_at":"2024-11-11T20:30:31.572Z","avatar_url":"https://github.com/Cysharp.png","language":"C#","funding_links":[],"categories":["NATS"],"sub_categories":["NATS C# Development"],"readme":"# **AlterNats is no longer maintained. Please check the official v2 client [nats-io/nats.net.v2](https://github.com/nats-io/nats.net.v2) and [announcement blog](https://nats.io/blog/nats-dotnet-v2-alpha-release/).**\n\n# AlterNats\n[![GitHub Actions](https://github.com/Cysharp/AlterNats/workflows/Build-Debug/badge.svg)](https://github.com/Cysharp/AlterNats/actions) [![Releases](https://img.shields.io/github/release/Cysharp/AlterNats.svg)](https://github.com/Cysharp/AlterNats/releases)\n\nAn alternative high performance [NATS](https://nats.io/) client for .NET. Zero Allocation and Zero Copy Architecture to achive x2~4 performance compare with [official NATS client](https://github.com/nats-io/nats.net) and [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis)'s PubSub.\n\n![image](https://user-images.githubusercontent.com/46207/164392256-46d09111-ec70-4cf3-b33d-38dc5d258455.png)\n\nThe most major PubSub Solution in C# is using Redis and StackExchange.Redis. Redis also has a managed service and has a well-designed client library. However, Redis is primarily a KVS and functions poorly as a PubSub.\n\n* Lack of monitoring for PubSub\n* Lack of clustering support for PubSub\n* Unbalanced pricing for managed services(not much memory is needed for PubSub)\n* Performance\n\nSince NATS is specialized for PubSub, it has a rich system for that purpose, and its performance seems to be perfect. The only drawback is that there is no managed service, but if you use NATS as a pure PubSub, you do not need to think about the persistence process, so I think it is one of the easiest middlewares to operate.\n\n[nats.net](https://github.com/nats-io/nats.net) works well enough, but the API has some weirdness that is not C#-like. The reason for this is that it is a port from Go, as noted in the documentation.\n\n\u003e The NATS C# .NET client was originally developed with the idea in mind that it would support the .NET 4.0 code base for increased adoption, and closely parallel the GO client (internally) for maintenance purposes. So, some of the nice .NET APIs/features were intentionally left out.\n\nAlterNats pursues high performance by utilizing a C#-like API and the latest API in .NET 6. See the [Performance](#performance) section for architectural innovations related to performance.\n\nGetting Started\n---\nPackage is published on NuGet.\n\n\u003e PM\u003e Install-Package [AlterNats](https://www.nuget.org/packages/AlterNats)\n\nHere is the simple example.\n\n```csharp\n// create connection(default, connect to nats://localhost:4222)\nawait using var conn = new NatsConnection();\n\n// for subscriber. await register to NATS server(not means await complete)\nvar subscription = await conn.SubscribeAsync\u003cPerson\u003e(\"foo\", x =\u003e\n{\n    Console.WriteLine($\"Received {x}\");\n});\n\n// for publisher.\nawait conn.PublishAsync(\"foo\", new Person(30, \"bar\"));\n\n// unsubscribe\nsubscription.Dipose();\n```\n\nAll value is automatically serialized via `System.Text.Json`. If you want to more performance, you can use [MessagePack for C#](https://github.com/neuecc/MessagePack-CSharp) as builtin serializer(requires `AlterNats.MessagePack` package).\n\nConfigure option adopts immutable options with `with` operator.\n \n```csharp\n// Options is immutable and can configure `with` operator\nvar options = NatsOptions.Default with\n{\n    Url = \"nats://127.0.0.1:9999\",\n    LoggerFactory = new MinimumConsoleLoggerFactory(LogLevel.Information),\n    Serializer = new MessagePackNatsSerializer(),\n    ConnectOptions = ConnectOptions.Default with\n    {\n        Echo = true,\n        Username = \"foo\",\n        Password = \"bar\",\n    }\n};\n\nawait using var conn = new NatsConnection(options);\n```\n\nConnections is thread-safe, physically connected as one connection to one connection, and all commands are automatically multiplexed. Therefore, connection is usually held as singleton. Connection is automatically connected on the first call and automatically reconnected if disconnected.\n\nIn addition to keeping it in a static field, it can be used integrated with a generic host by the `AlterNats.Hosting` package when registered with a DI.\n\n\u003e PM\u003e Install-Package [AlterNats](https://www.nuget.org/packages/AlterNats.Hosting)\n\n```csharp\nusing AlterNats;\n\nvar builder = WebApplication.CreateBuilder(args);\n\n// Register NatsConnectionPool, NatsConnection, INatsCommand to ServiceCollection\nbuilder.Services.AddNats();\n\nvar app = builder.Build();\n\napp.MapGet(\"/subscribe\", (INatsCommand command) =\u003e command.SubscribeAsync(\"foo\", (int x) =\u003e Console.WriteLine($\"received {x}\")));\napp.MapGet(\"/publish\", (INatsCommand command) =\u003e command.PublishAsync(\"foo\", 99));\n\napp.Run();\n```\n\n`AddNats` register NatsConnection and setup correct loggerfactory.\n\nMinimum RPC\n---\nNats supports request/response protocol, it will become simple RPC.\n\n```csharp\n// Server\nawait connection.SubscribeRequestAsync\u003cFooRequest, FooResponse\u003e(\"foobar.key\", req =\u003e\n{\n    return new FooResponse();\n});\n\n// Client\nvar response = await connection.RequestAsync\u003cFooRequest, FooResponse\u003e(\"foobar.key\", new FooRequest());\n```\n\nLogging\n---\nIn default, internal error is not logged so recommend to setup LoggerFactory. AlterNats has builtin simple `MinimumConsoleLoggerFactory`.\n\n```csharp\nvar options = NatsOptions.Default with\n{\n    LoggerFactory = new MinimumConsoleLoggerFactory(LogLevel.Information)\n};\n```\n\nHowever for production use, use `Microsoft.Extensions.Logging` and setup your favorite logger. [Cysharp/ZLogger](https://github.com/Cysharp/ZLogger/) is especially recommended to keep high performance.\n\n```csharp\n// You're using Generic Host(Microsoft.Extensions.Hositng) or ASP.NET Core, you can get ILoggerFactory from the built-in pipeline.\n// Otherwise, build it yourself from ServiceCollection.\nvar provider = new ServiceCollection()\n    .AddLogging(x =\u003e\n    {\n        x.ClearProviders();\n        x.SetMinimumLevel(LogLevel.Information);\n        x.AddZLoggerConsole();\n    })\n    .BuildServiceProvider();\n\nvar loggerFactory = provider.GetRequiredService\u003cILoggerFactory\u003e();\n\nvar options = NatsOptions.Default with\n{\n    LoggerFactory = loggerFactory\n};\n\nawait using var connection = new NatsConnection(options);\n```\n\nIntegrated with generic host(ApplicationBuilder), `AddNats` automatically setup logger-factory.\n\n```csharp\nvar builder = WebApplication.CreateBuilder(args);\n\n// will setup ApplicationBuilder's LoggerFactory\nbuilder.Services.AddNats();\n```\n\nNatsConnection\n---\nNatsConnection is 1:1 physical socket connection of nats. It will connect automatically when first operation called. The connection can also be initiated explicitly with `ConnectAsync`. Executing `DisposeAsync` disconnects the connection. If the network is disconnected, it will automatically reconnect.\n\n```csharp\npublic class NatsConnection : IAsyncDisposable, INatsCommand\n{\n    public NatsConnection()\n        : this(NatsOptions.Default)\n    public NatsConnection(NatsOptions options)\n    \n    public NatsOptions Options { get; }\n    public NatsConnectionState ConnectionState { get; }\n    public ServerInfo? ServerInfo { get; }\n    public async ValueTask ConnectAsync()\n    public NatsStats GetStats()\n    public async ValueTask DisposeAsync()\n}\n```\n\n## INatsCommand\n\nINatsCommand is operation commands of NATS. key is available in `NatsKey` and `string` variants, see the [NatsKey](#natskey) section for details.\n\nPost*** is fire-and-forget command, ***Async is await network send.\n\n```csharp\npublic interface INatsCommand\n{\n    IObservable\u003cT\u003e AsObservable\u003cT\u003e(in NatsKey key);\n    IObservable\u003cT\u003e AsObservable\u003cT\u003e(string key);\n    ValueTask FlushAsync();\n    ValueTask\u003cTimeSpan\u003e PingAsync();\n    void PostDirectWrite(byte[] protocol);\n    void PostDirectWrite(DirectWriteCommand command);\n    void PostDirectWrite(string protocol, int repeatCount = 1);\n    void PostPing();\n    void PostPublish(in NatsKey key);\n    void PostPublish(in NatsKey key, byte[] value);\n    void PostPublish(in NatsKey key, ReadOnlyMemory\u003cbyte\u003e value);\n    void PostPublish(string key);\n    void PostPublish(string key, byte[] value);\n    void PostPublish(string key, ReadOnlyMemory\u003cbyte\u003e value);\n    void PostPublish\u003cT\u003e(in NatsKey key, T value);\n    void PostPublish\u003cT\u003e(string key, T value);\n    void PostPublishBatch\u003cT\u003e(IEnumerable\u003c(NatsKey, T?)\u003e values);\n    void PostPublishBatch\u003cT\u003e(IEnumerable\u003c(string, T?)\u003e values);\n    ValueTask PublishAsync(in NatsKey key);\n    ValueTask PublishAsync(in NatsKey key, byte[] value);\n    ValueTask PublishAsync(in NatsKey key, ReadOnlyMemory\u003cbyte\u003e value);\n    ValueTask PublishAsync(string key);\n    ValueTask PublishAsync(string key, byte[] value);\n    ValueTask PublishAsync(string key, ReadOnlyMemory\u003cbyte\u003e value);\n    ValueTask PublishAsync\u003cT\u003e(in NatsKey key, T value);\n    ValueTask PublishAsync\u003cT\u003e(string key, T value);\n    ValueTask PublishBatchAsync\u003cT\u003e(IEnumerable\u003c(NatsKey, T?)\u003e values);\n    ValueTask PublishBatchAsync\u003cT\u003e(IEnumerable\u003c(string, T?)\u003e values);\n    ValueTask\u003cIDisposable\u003e QueueSubscribeAsync\u003cT\u003e(in NatsKey key, in NatsKey queueGroup, Action\u003cT\u003e handler);\n    ValueTask\u003cIDisposable\u003e QueueSubscribeAsync\u003cT\u003e(in NatsKey key, in NatsKey queueGroup, Func\u003cT, Task\u003e asyncHandler);\n    ValueTask\u003cIDisposable\u003e QueueSubscribeAsync\u003cT\u003e(string key, string queueGroup, Action\u003cT\u003e handler);\n    ValueTask\u003cIDisposable\u003e QueueSubscribeAsync\u003cT\u003e(string key, string queueGroup, Func\u003cT, Task\u003e asyncHandler);\n    ValueTask\u003cTResponse?\u003e RequestAsync\u003cTRequest, TResponse\u003e(NatsKey key, TRequest request, CancellationToken cancellationToken = default);\n    ValueTask\u003cTResponse?\u003e RequestAsync\u003cTRequest, TResponse\u003e(string key, TRequest request, CancellationToken cancellationToken = default);\n    ValueTask\u003cIDisposable\u003e SubscribeAsync(in NatsKey key, Action handler);\n    ValueTask\u003cIDisposable\u003e SubscribeAsync(string key, Action handler);\n    ValueTask\u003cIDisposable\u003e SubscribeAsync\u003cT\u003e(in NatsKey key, Action\u003cT\u003e handler);\n    ValueTask\u003cIDisposable\u003e SubscribeAsync\u003cT\u003e(in NatsKey key, Func\u003cT, Task\u003e asyncHandler);\n    ValueTask\u003cIDisposable\u003e SubscribeAsync\u003cT\u003e(string key, Action\u003cT\u003e handler);\n    ValueTask\u003cIDisposable\u003e SubscribeAsync\u003cT\u003e(string key, Func\u003cT, Task\u003e asyncHandler);\n    ValueTask\u003cIDisposable\u003e SubscribeRequestAsync\u003cTRequest, TResponse\u003e(in NatsKey key, Func\u003cTRequest, Task\u003cTResponse\u003e\u003e requestHandler);\n    ValueTask\u003cIDisposable\u003e SubscribeRequestAsync\u003cTRequest, TResponse\u003e(in NatsKey key, Func\u003cTRequest, TResponse\u003e requestHandler);\n    ValueTask\u003cIDisposable\u003e SubscribeRequestAsync\u003cTRequest, TResponse\u003e(string key, Func\u003cTRequest, Task\u003cTResponse\u003e\u003e requestHandler);\n    ValueTask\u003cIDisposable\u003e SubscribeRequestAsync\u003cTRequest, TResponse\u003e(string key, Func\u003cTRequest, TResponse\u003e requestHandler);\n}\n```\n\n`AsObservable` is similar as subscribe but handler will be `IObservable\u003cT\u003e`.\n\nPublish(`byte[]`) or Publish(`ReadOnlyMemory\u003cbyte\u003e`) is special, in these cases, the value is sent as is, without passing through the serializer.\n\n## Hook\n\n`NatsConnection` has some hook events.\n\n```csharp\npublic event EventHandler\u003cstring\u003e? ConnectionDisconnected;\npublic event EventHandler\u003cstring\u003e? ConnectionOpened;\npublic event EventHandler\u003cstring\u003e? ReconnectFailed;\npublic Func\u003c(string Host, int Port), ValueTask\u003c(string Host, int Port)\u003e\u003e? OnConnectingAsync;\n```\n\n`ConnectionOpened`, `ConnectionDisconnected`, `ReconnectFailed` is called when occurs there event. `OnConnectingAsync` is called before connect to NATS server. For example, check health by HTTP before connect server as TCP.\n\n```csharp\n// NATS server requires `-m 8222` option\nawait using var conn = new NatsConnection();\nconn.OnConnectingAsync = async x =\u003e // (host, port)\n{\n    var health = await new HttpClient().GetFromJsonAsync\u003cNatsHealth\u003e($\"http://{x.Host}:8222/healthz\");\n    if (health == null || health.status != \"ok\") throw new Exception();\n\n    // if returning another (host, port), TCP connection will use it.\n    return x;\n};\n\npublic record NatsHealth(string status);\n```\n\n### Stats\n\nYou can monitor network stats by `NatsConnection.GetStats()`. It can get these stats counter.\n\n```csharp\npublic readonly record struct NatsStats\n(\n    long SentBytes,\n    long ReceivedBytes,\n    long PendingMessages,\n    long SentMessages,\n    long ReceivedMessages,\n    long SubscriptionCount\n);\n```\n\nNatsConnectionPool\n---\n`NatsConnection` is 1:1 physical connection and internally create single pair of read/write async loop. Single connections are effective for multiplexing, but when flow rates are high, being a single loop is a negative performance factor.\n\n`NatsConnectionPool` is a simple round-robin connection pool that manages multiple connections internally.\n\n```csharp\npublic sealed class NatsConnectionPool : IAsyncDisposable\n{\n    public NatsConnectionPool()\n        : this(Environment.ProcessorCount / 2, NatsOptions.Default)\n    public NatsConnectionPool(int poolSize)\n        : this(poolSize, NatsOptions.Default)\n    public NatsConnectionPool(NatsOptions options)\n        : this(Environment.ProcessorCount / 2, options)\n    public NatsConnectionPool(int poolSize, NatsOptions options)\n\n    public IEnumerable\u003cNatsConnection\u003e GetConnections()\n    public NatsConnection GetConnection()\n    public INatsCommand GetCommand()\n```\n\nBy default, it creates `ProcessorCount / 2` connections and returns a different connection in a round-robin each time `GetConnection()` or `GetCommand()` is called.\n\nIf you are using `AlterNats.Hosting`, `AddNats(poolSize)` will use `NatsConnectionPool`.\n\n```csharp\nbuilder.Services.AddNats(poolSize: 4);\n\n// DI injection can get INatsCommand(or NatsConnection) that call pool.Command() as transient.\npublic MyController(INatsCommand command)\n```\n\nSharding\n---\nSupports horizontal sharding on the client side using Key. If you are using Sharding, be aware that you cannot use wildcards for Key.\n\n```csharp\npublic sealed class NatsShardingConnection : IAsyncDisposable\n{\n    public NatsShardingConnection(int poolSize, NatsOptions options, string[] urls)\n\n    public IEnumerable\u003cNatsConnection\u003e GetConnections()\n    public ShardringNatsCommand GetCommand(in NatsKey key)\n    public ShardringNatsCommand GetCommand(string key)\n}\n```\n\nIf you are using `AlterNats.Hosting`, `AddNats(string[] urls)` will use `NatsShardingConnection`.\n\n```csharp\nbuilder.Services.AddNats(poolSize: 4, urls: new[]{\"nats://foo,nats://bar\"});\n\n// DI injection can get NatsShardingConnection as singleton\npublic MyController(NatsShardingConnection connection)\n{\n    var cmd = connection.GetCommand(\"foobarbaz\");\n}\n```\n\nNatsKey\n---\nNatsKey is encoded key string. If you store string key as const, use NatsKey instead achieves better performance.\n\n```csharp\npublic static class Keys\n{\n    public static readonly NatsKey FooKey = new NatsKey(\"foo\");\n}\n```\n\nNatsOptions\n---\nNatsOptions is configure behaviour of AlterNats.\n\n```csharp\npublic sealed record NatsOptions\n(\n    string Url,\n    ConnectOptions ConnectOptions,\n    INatsSerializer Serializer,\n    ILoggerFactory LoggerFactory,\n    int WriterBufferSize,\n    int ReaderBufferSize,\n    bool UseThreadPoolCallback,\n    string InboxPrefix,\n    bool NoRandomize,\n    TimeSpan PingInterval,\n    int MaxPingOut,\n    TimeSpan ReconnectWait,\n    TimeSpan ReconnectJitter,\n    TimeSpan ConnectTimeout,\n    int CommandPoolSize,\n    TimeSpan RequestTimeout\n)\n    \npublic static NatsOptions Default = new NatsOptions(\n    Url: \"nats://localhost:4222\",\n    ConnectOptions: ConnectOptions.Default,\n    Serializer: new JsonNatsSerializer(new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }),\n    LoggerFactory: NullLoggerFactory.Instance,\n    WriterBufferSize: 65534,\n    ReaderBufferSize: 1048576,\n    UseThreadPoolCallback: false,\n    InboxPrefix: \"_INBOX.\",\n    NoRandomize: false,\n    PingInterval: TimeSpan.FromMinutes(2),\n    MaxPingOut: 2,\n    ReconnectWait: TimeSpan.FromSeconds(2),\n    ReconnectJitter: TimeSpan.FromMilliseconds(100),\n    ConnectTimeout: TimeSpan.FromSeconds(2),\n    CommandPoolSize: 256,\n    RequestTimeout: TimeSpan.FromMinutes(1)\n)\n```\n\nConnectOptions\n---\nConnectOptions is client -\u003e server configuration of NATS.\n\n```csharp\npublic sealed record ConnectOptions\n{\n    /// \u003csummary\u003eOptional boolean. If set to true, the server (version 1.2.0+) will not send originating messages from this connection to its own subscriptions. Clients should set this to true only for server supporting this feature, which is when proto in the INFO protocol is set to at least 1.\u003c/summary\u003e\n    [JsonPropertyName(\"echo\")]\n    public bool Echo { get; init; } = true;\n\n    /// \u003csummary\u003eTurns on +OK protocol acknowledgements.\u003c/summary\u003e\n    [JsonPropertyName(\"verbose\")]\n    public bool Verbose { get; init; }\n\n    /// \u003csummary\u003eTurns on additional strict format checking, e.g. for properly formed subjects\u003c/summary\u003e\n    [JsonPropertyName(\"pedantic\")]\n    public bool Pedantic { get; init; }\n\n    /// \u003csummary\u003eIndicates whether the client requires an SSL connection.\u003c/summary\u003e\n    [JsonPropertyName(\"tls_required\")]\n    public bool TLSRequired { get; init; }\n\n    [JsonPropertyName(\"nkey\")]\n    public string? Nkey { get; init; } = null;\n\n    /// \u003csummary\u003eThe JWT that identifies a user permissions and acccount.\u003c/summary\u003e\n    [JsonPropertyName(\"jwt\")]\n    public string? JWT { get; init; } = null;\n\n    /// \u003csummary\u003eIn case the server has responded with a nonce on INFO, then a NATS client must use this field to reply with the signed nonce.\u003c/summary\u003e\n    [JsonPropertyName(\"sig\")]\n    public string? Sig { get; init; } = null;\n\n    /// \u003csummary\u003eClient authorization token (if auth_required is set)\u003c/summary\u003e\n    [JsonPropertyName(\"auth_token\")]\n    public string? AuthToken { get; init; } = null;\n\n    /// \u003csummary\u003eConnection username (if auth_required is set)\u003c/summary\u003e\n    [JsonPropertyName(\"user\")]\n    public string? Username { get; init; } = null;\n\n    /// \u003csummary\u003eConnection password (if auth_required is set)\u003c/summary\u003e\n    [JsonPropertyName(\"pass\")]\n    public string? Password { get; init; } = null;\n\n    /// \u003csummary\u003eOptional client name\u003c/summary\u003e\n    [JsonPropertyName(\"name\")]\n    public string? Name { get; init; } = null;\n\n    /// \u003csummary\u003eThe implementation language of the client.\u003c/summary\u003e\n    [JsonPropertyName(\"lang\")]\n    public string ClientLang { get; init; } = \"C#\";\n\n    /// \u003csummary\u003eThe version of the client.\u003c/summary\u003e\n    [JsonPropertyName(\"version\")]\n    public string ClientVersion { get; init; } = GetAssemblyVersion();\n\n    /// \u003csummary\u003eoptional int. Sending 0 (or absent) indicates client supports original protocol. Sending 1 indicates that the client supports dynamic reconfiguration of cluster topology changes by asynchronously receiving INFO messages with known servers it can reconnect to.\u003c/summary\u003e\n    [JsonPropertyName(\"protocol\")]\n    public int Protocol { get; init; } = 1;\n\n    [JsonPropertyName(\"account\")]\n    public string? Account { get; init; } = null;\n\n    [JsonPropertyName(\"new_account\")]\n    public bool? AccountNew { get; init; }\n\n    [JsonPropertyName(\"headers\")]\n    public bool Headers { get; init; } = false;\n\n    [JsonPropertyName(\"no_responders\")]\n    public bool NoResponders { get; init; } = false;\n}\n```\n\nSerialization\n---\nIn default, serializer uses System.Text.Json. for more performant serialization, you can use MessagePack extension package.\n\n\u003e PM\u003e Install-Package [AlterNats.MessagePack](https://www.nuget.org/packages/AlterNats.MessagePack)\n\n```csharp\nvar options = NatsOptions.Default with\n{\n    Serializer = new MessagePackNatsSerializer(),\n};\n```\n\nPerformance\n---\n\n### Binary compare of TextProtocol\n\n[NATS Protocol](https://docs.nats.io/reference/reference-protocols/nats-protocol) is text-protocol, However, by parsing it like binary code, it provides a fast decision.\n\nNATS can determine the type of message coming in by the leading string (`INFO`, `MSG`, `PING`, `+OK`, `-ERR`, etc.). It can be converted to a 4-character Int for comparison.\n\n```csharp\n// msg = ReadOnlySpan\u003cbyte\u003e\nif(Unsafe.ReadUnaligned\u003cint\u003e(ref MemoryMarshal.GetReference\u003cbyte\u003e(msg)) == 1330007625) // INFO\n{\n}\n```\n\nAlterNats using this constant.\n\n```csharp\ninternal static class ServerOpCodes\n{\n    public const int Info = 1330007625;  // Encoding.ASCII.GetBytes(\"INFO\") |\u003e MemoryMarshal.Read\u003cint\u003e\n    public const int Msg = 541545293;    // Encoding.ASCII.GetBytes(\"MSG \") |\u003e MemoryMarshal.Read\u003cint\u003e\n    public const int Ping = 1196312912;  // Encoding.ASCII.GetBytes(\"PING\") |\u003e MemoryMarshal.Read\u003cint\u003e\n    public const int Pong = 1196314448;  // Encoding.ASCII.GetBytes(\"PONG\") |\u003e MemoryMarshal.Read\u003cint\u003e\n    public const int Ok = 223039275;     // Encoding.ASCII.GetBytes(\"+OK\\r\") |\u003e MemoryMarshal.Read\u003cint\u003e\n    public const int Error = 1381123373; // Encoding.ASCII.GetBytes(\"-ERR\") |\u003e MemoryMarshal.Read\u003cint\u003e\n}\n```\n\n### Automatically pipelining\n\nAll NATS protocol writes and reads are pipelined (batch).\n\n![image](https://user-images.githubusercontent.com/46207/167585601-5634057e-812d-4b60-ab5b-61d9c8c37063.png)\n\nThis is not only effective in reducing round trip time, but also in reducing the number of consecutive system calls.\n\n### All one object\n\nFor the convenience of packing into a Channel, we need to put the data into a write message object and hold it in the heap. We also need a Promise for an asynchronous method that waits until the write is complete.\n\n```csharp\nawait connection.PublishAsync(value);\n```\n\nTo implement such an API efficiently, let's cohabitate and pack all the functions into a single message object (internally named Command) that must be allocated.\n\n```csharp\nclass AsyncPublishCommand\u003cT\u003e : ICommand, IValueTaskSource, IThreadPoolWorkItem, IObjectPoolNode\u003cAsyncPublishCommand\u003cT\u003e\u003e\n\ninternal interface ICommand\n{\n    void Write(ProtocolWriter writer);\n}\n\ninternal interface IObjectPoolNode\u003cT\u003e\n{\n    ref T? NextNode { get; }\n}\n```\n\nAnd by putting this one object itself into the object pool, it is asynchronous programming with zero allocation.\n\n### Zero-copy Architecture\n\nThe data to be Publish/Subscribe is usually serialized C# types to JSON, MessagePack, and so on. In this case, it is inevitably exchanged in `byte[]`, for example, the content of `RedisValue` in StackExchange.Redis is actually byte[], and whether sending or receiving, `byte[]` will be generated and retained.\n\nAlterNats serializer uses `IBufferWriter\u003cbyte\u003e` for Write, `ReadOnlySequence\u003cbyte\u003e` for Read.\n\n```csharp\npublic interface INatsSerializer\n{\n    int Serialize\u003cT\u003e(ICountableBufferWriter bufferWriter, T? value);\n    T? Deserialize\u003cT\u003e(in ReadOnlySequence\u003cbyte\u003e buffer);\n}\n\npublic interface ICountableBufferWriter : IBufferWriter\u003cbyte\u003e\n{\n    int WrittenCount { get; }\n}\n```\n\n```csharp\n// Implementation of MessagePack for C#\npublic class MessagePackNatsSerializer : INatsSerializer\n{\n    public int Serialize\u003cT\u003e(ICountableBufferWriter bufferWriter, T? value)\n    {\n        var before = bufferWriter.WrittenCount;\n        MessagePackSerializer.Serialize(bufferWriter, value);\n        return bufferWriter.WrittenCount - before;\n    }\n\n    public T? Deserialize\u003cT\u003e(in ReadOnlySequence\u003cbyte\u003e buffer)\n    {\n        return MessagePackSerializer.Deserialize\u003cT\u003e(buffer);\n    }\n}\n```\n\nThe Serialize methods of System.Text.Json and MessagePack for C# provide an overload that accepts `IBufferWriter\u003cbyte\u003e`. The serializer directly accesses and writes to the buffer provided for writing to the Socket via `IBufferWriter\u003cbyte\u003e`, eliminating the copying of bytes[] between the Socket and the serializer.\n\n![image](https://user-images.githubusercontent.com/46207/167587816-c50b0af3-edaa-4a2a-b536-67aed0a5f908.png)\n\nTips\n---\nAs mentioned in the [NatsConnectionPool](#natsconnectionpool) section, the connection is driven by a pair of single async read/write loop. Although it can be a balance with multiplexing, proper use of NatsConnectionPool can improve performance.\n\nThe Subscribe handler also runs on the read-loop, so if the handler blocks, the read-loop will also block. Be sure to use await when I/O, etc. occurs, and consider a separate Task.Run for CPU-intensive callbacks.\n\nIf `NatsOptions.UseThreadPoolCallback` is set to true, all callback handlers are executed on the thread pool, so the read loop is not blocked. However, performance under high workloads is reduced compared to the false case. Therefore, the default is false.\n\nArchitecture guide\n---\n\nCysharp creating [MagicOnion](https://github.com/Cysharp/MagicOnion) network framework that run both .NET and Unity. For example, with Unity (or other .NET Client) and Server (MagicOnion).\n\n![image](https://user-images.githubusercontent.com/46207/164406771-58318153-c6a7-49c0-b3af-2b8389e2c9c1.png)\n\nSingle server is simple, however real-case needs multiple server. This pattern uses loadbalancer and pubsub backend.\n\n![image](https://user-images.githubusercontent.com/46207/164409016-b6e99f36-bdf7-47a9-80a6-558010963a36.png)\n\nThis pattern is used in [Socket.IO](https://socket.io/)'s Redis adaptor, [SignalR](https://docs.microsoft.com/ja-jp/aspnet/signalr/overview/getting-started/introduction-to-signalr)'s Redis backplane, and also MagicOnion's Redis backplane. What you can do with PubSub in Redis, you can do with NATS.\n\nThis pattern can be server as stateless and easy to scale-out. The disadvantage is that the server cannot have state. If states are required, there are architectures that let you connect to specific servers.\n\n![image](https://user-images.githubusercontent.com/46207/164417937-7d1adedb-36ee-453b-9ca6-9d41aded50af.png)\n\nIn this case, you can have full in-memory state in the server to connect to the same server, or you can run a so-called game loop inside to process the game. You can also host a headless Unity or similar without a screen, and run the client itself on the server.\n\nHowever, it is difficult to achieve this pattern with plain Kubernetes, and there is a solution called [Agones](https://agones.dev/site/) for this purpose.\n\nHowever, this has its drawbacks. The game server that Agones envisions is for hosting one game session per process, so it cannot be used as is to host many game sessions in a single process. A lightweight game server (MagicOnion) can pack many game sessions into a single process, and there is a big difference in cost if this is set up in separate containers.\n\nCysharp is providing state-full server utility for game-loop called [LogicLooper](https://github.com/Cysharp/LogicLooper). Hosting LogicLooper as [Worker Service](https://docs.microsoft.com/en-us/dotnet/core/extensions/workers) in .NET 6 will be simpler pattern.\n\n![image](https://user-images.githubusercontent.com/46207/164417734-f2ec80e7-f12f-4a84-8252-ce28f9b53f05.png)\n\nSince the state of the entire game is managed by LogicLooper itself, MagicOnion itself, which is directly connected to the client, is stateless. Therefore, from an infrastructure standpoint, MagicOnion only needs to be placed under a load balancer, and all the hassles associated with connections between servers can be pushed to NATS, making infrastructure management itself a fairly simple configuration.\n\nAlso, MagicOnion itself is a stateful system, and it is easy for each user to have their own state (as long as they do not cross servers). Therefore, data that arrives from LogicLooper and does not need to be delivered to connected users can be culled using the user states that MagicOnion has, and the amount of traffic can be reduced by not transferring the data in the first place or by thinning it out, thereby improving the user experience. This improves the user experience.\n\nThis pattern is ideal for MORPGs, MMORPGs and creating Metaverse.\n\nLimitation\n---\nCurrently, AlterNats is not oriented toward compatibility and comprehensiveness, specializes in performance against Core NATS. Therefore, these features are not supported.\n\n* TLS\n* JetStream\n* Header\n\nLicense\n---\nThis library is licensed under the MIT License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FCysharp%2FAlterNats","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FCysharp%2FAlterNats","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FCysharp%2FAlterNats/lists"}