https://github.com/krasin-ga/respsody
General purpose RESP client
https://github.com/krasin-ga/respsody
csharp dotnet redis-client
Last synced: 3 months ago
JSON representation
General purpose RESP client
- Host: GitHub
- URL: https://github.com/krasin-ga/respsody
- Owner: krasin-ga
- License: mit
- Created: 2025-08-01T18:45:01.000Z (11 months ago)
- Default Branch: main
- Last Pushed: 2025-12-14T09:03:44.000Z (7 months ago)
- Last Synced: 2025-12-16T13:16:14.836Z (7 months ago)
- Topics: csharp, dotnet, redis-client
- Language: C#
- Homepage:
- Size: 211 KB
- Stars: 4
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Respsody
[](LICENSE) [](https://www.nuget.org/packages/Respsody/)
**Respsody** is an experimental, high-performance, asynchronous, general-purpose [RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md) client library written in C#. It's currently in an early stage of development and intended for experimentation and community feedback. Expect breaking changes, missing features, and limited error handling.
## Features
- Compatibility with any RESP3-compliant server, including Redis, Garnet, Dragonfly, Valkey, and others
- Code generation for arbitrary commands
- Efficient memory usage through scoped buffer reuse
- Full Redis Cluster protocol support with advanced routing: primary/replica node targeting, slot-aware command dispatch, and optimized MGETs by grouping keys per responsible node
## Installation
```
dotnet add package respsody --prerelease
```
## Benchmarks
Tested against the StackExchange.Redis client. Click on the spoilers to reveal the results.
>Hardware: AMD Ryzen 9 3900X, 1 CPU, 24 logical and 12 physical cores
>RESP Server: [Garnet](https://github.com/microsoft/garnet) was run in a separate process on localhost
### BenchmarkDotNet
Windows 11, .NET 9.0.5
| Method | Mean | Op/s | Gen0 | Gen1 | Gen2 | Allocated |
|---------------------------------------------------------------------- |-----------:|------------:|-------:|-------:|-------:|----------:|
| '[when_all 12_500 tasks] [Respsody] Get (noop) commands/s' | 614.5 ns | 1,627,287.0 | 0.0200 | 0.0125 | - | 184 B |
| '[when_all 12_500 tasks] [StackExchange.Redis] Get (noop) commands/s' | 981.8 ns | 1,018,550.5 | 0.0488 | 0.0325 | - | 416 B |
| '[when_all 12_500 tasks] [Respsody] Get (json) commands/s' | 913.7 ns | 1,094,486.0 | 0.1288 | 0.0613 | 0.0025 | 1041 B |
| '[when_all 12_500 tasks] [StackExchange.Redis] Get (json) commands/s' | 1,512.3 ns | 661,241.9 | 0.1800 | 0.0675 | 0.0125 | 1464 B |
| Method | MessageSize | Mean | Op/s | Gen0 | Gen1 | Allocated |
|---------------------- |------------ |---------:|--------:|-------:|-------:|----------:|
| Respsody_Set_Get | 256 | 183.4 us | 5,453.9 | - | - | 353 B |
| SE_Set_Get | 256 | 224.8 us | 4,449.0 | - | - | 1064 B |
| Respsody_MSet_MGet_10 | 256 | 186.9 us | 5,350.5 | - | - | 1570 B |
| SE_MSet_MGet_10 | 256 | 219.2 us | 4,561.5 | - | - | 5192 B |
| Respsody_Set_Get | 32786 | 198.7 us | 5,032.4 | - | - | 377 B |
| SE_Set_Get | 32786 | 269.0 us | 3,718.1 | 0.4883 | - | 33601 B |
| Respsody_MSet_MGet_10 | 32786 | 530.5 us | 1,885.0 | - | - | 1810 B |
| SE_MSet_MGet_10 | 32786 | 608.7 us | 1,642.8 | 5.8594 | 1.9531 | 330554 B |
*SE -> StackExchange.Redis*
Ubuntu 20 LTS (Focal Fossa) *WSL Virtualization*, .NET 9.0.5
| Method | Mean | Op/s | Gen0 | Gen1 | Gen2 | Allocated |
|---------------------------------------------------------------------- |-----------:|------------:|-------:|-------:|-------:|----------:|
| '[when_all 12_500 tasks] [Respsody] Get (noop) commands/s' | 736.9 ns | 1,357,001.3 | 0.0200 | 0.0100 | - | 184 B |
| '[when_all 12_500 tasks] [StackExchange.Redis] Get (noop) commands/s' | 1,283.0 ns | 779,449.1 | 0.0475 | 0.0325 | - | 416 B |
| '[when_all 12_500 tasks] [Respsody] Get (json) commands/s' | 1,148.7 ns | 870,574.8 | 0.1288 | 0.0563 | 0.0025 | 1042 B |
| '[when_all 12_500 tasks] [StackExchange.Redis] Get (json) commands/s' | 1,737.4 ns | 575,570.6 | 0.1825 | 0.0950 | 0.0200 | 1464 B |
#### Non-scientific
These benchmarks run a series of test scenarios and measure total metrics.
Windows 11, .NET 9.0.5
| Scenario | Target | Dataset | Iterations | TotalElapsed | BestRun | CpuTime | TotalAllocated |
|---------------------------------------|--------------------|----------------------------|------------|--------------|----------|----------|----------------|
| SetGet_Sequential | Respsody | small_20K 20000KV 2.45 MiB | 50 | 157.19s | 2.97s | 179.31s | 46.77 MiB |
| SetGet_Sequential | StackOverflowRedis | small_20K 20000KV 2.45 MiB | 50 | 209.82s | 3.82s | 218.55s | 667.54 MiB |
| - | - | - | - | - | - | - | - |
| SetGet_Sequential | Respsody | large_300 300KV 151.62 MiB | 50 | 13.61s | 184.48ms | 8.78s | 9.69 MiB |
| SetGet_Sequential | StackOverflowRedis | large_300 300KV 151.62 MiB | 50 | 24.22s | 372.82ms | 14.38s | 7.42 GiB |
| - | - | - | - | - | - | - | - |
| SetGet_Sequential_WithJsonDeserialize | Respsody | jsons_10K 10000KV 2.25 MiB | 50 | 90.31s | 1.73s | 107.67s | 461.92 MiB |
| SetGet_Sequential_WithJsonDeserialize | StackOverflowRedis | jsons_10K 10000KV 2.25 MiB | 50 | 128.20s | 2.16s | 128.30s | 1.04 GiB |
| - | - | - | - | - | - | - | - |
| SetGet_WhenEach | Respsody | small_20K 20000KV 2.45 MiB | 50 | 1.47s | 24.02ms | 9.09s | 316.49 MiB |
| SetGet_WhenEach | StackOverflowRedis | small_20K 20000KV 2.45 MiB | 50 | 2.38s | 34.12ms | 15.62s | 852.86 MiB |
| - | - | - | - | - | - | - | - |
| OneSetTwoGets_WhenEach | Respsody | small_20K 20000KV 2.45 MiB | 50 | 1.74s | 30.78ms | 16.52s | 387.12 MiB |
| OneSetTwoGets_WhenEach | StackOverflowRedis | small_20K 20000KV 2.45 MiB | 50 | 3.29s | 52.22ms | 21.67s | 1.24 GiB |
| - | - | - | - | - | - | - | - |
| SetGet_WhenEach_WithJsonDeserialize | Respsody | jsons_10K 10000KV 2.25 MiB | 50 | 763.14ms | 13.88ms | 4.33s | 597.01 MiB |
| SetGet_WhenEach_WithJsonDeserialize | StackOverflowRedis | jsons_10K 10000KV 2.25 MiB | 50 | 1.24s | 20.73ms | 7.42s | 1.12 GiB |
| - | - | - | - | - | - | - | - |
| OneSetTwoGets_WhenEach | Respsody | large_300 300KV 151.62 MiB | 50 | 18.14s | 280.61ms | 12.23s | 74.49 MiB |
| OneSetTwoGets_WhenEach | StackOverflowRedis | large_300 300KV 151.62 MiB | 50 | 23.55s | 339.96ms | 22.36s | 15.23 GiB |
| - | - | - | - | - | - | - | - |
| MSetGet | Respsody | small_20K 20000KV 2.45 MiB | 50 | 649.52ms | 8.35ms | 890.62ms | 157.21 KiB |
| MSetGet | StackOverflowRedis | small_20K 20000KV 2.45 MiB | 50 | 1.07s | 16.51ms | 1.78s | 241.06 MiB |
| - | - | - | - | - | - | - | - |
| MSetGet | Respsody | large_300 300KV 151.62 MiB | 50 | 9.27s | 165.54ms | 5.00s | 11.91 MiB |
| MSetGet | StackOverflowRedis | large_300 300KV 151.62 MiB | 50 | 22.07s | 297.64ms | 43.58s | 27.06 GiB |
## Quick Start
```csharp
using Respsody;
using Respsody.Resp;
using System.Net;
using System.Text;
using static System.Console;
var clientFactory = new RespClientFactory();
using var client = await clientFactory.Create(new IPEndPoint(IPAddress.Loopback, 6379));
var utf8Key = Key.Utf8("respsody");
Key strKey = "gob_key"; // same as Key.Utf8
var arrKey = Key.ByteArray([1, 0, 1]);
var utf16Key = Key.Unicode("‼️");
await client.Set(utf8Key, Value.Utf8("hello, respsody!"));
await client.Set(arrKey, Value.ByteArray([0, 3, 0, 3, 6, 6]));
await client.Set(strKey, Value.Utf8("we need a gimmick!"));
await client.Set(utf16Key, Value.Unicode("⚡"));
//dispose non-void responses to release underlying buffers
using var getResponse = await client.Get(utf8Key);
WriteLine("- GET result -");
WriteLine(getResponse.ToString());
//for aggregate responses(arrays, maps, sets) dispose only root response
using var mgetResponse = await client.Mget([strKey, utf16Key, arrKey]);
OutputEncoding = Encoding.Unicode;
WriteLine("- MGET result -");
Write("strings: ");
Write(mgetResponse[0].ToRespString());
WriteLine(mgetResponse[1].ToRespString().AsUnicodeSpan());
Write("byte[]: ");
WriteLine($"{string.Join(",", [..mgetResponse[2].ToRespString().GetSpan()])}");
//define and generate commands:
[RespCommand("GET key", ResponseType.String)]
[RespCommand("SET key value", ResponseType.Void)]
[RespCommand("MGET key [key ...]", ResponseType.Array)]
[RespCommand("MSET key value [key value ...]", ResponseType.Void)]
[RespCommand("COMMAND DOCS [command-name:string [command-name:string ...]]", ResponseType.Map)]
public static class Commands;
```
Cluster usage:
```csharp
using Respsody.Cluster;
var clusterRouter = new ClusterRouter(
new ClusterRouterOptions
{
SeedEndpoints = ["some_cluster_host1:6379", "some_cluster_host2:6379"],
ClientOptions = new RespClientOptions()
});
await clusterRouter.Initialize();
// execute command on primary node that is responsible for key
using var v1 = await clusterRouter.RouteTo(RolePreference.Primary).Get(Key.Utf8("some_key_1"));
// pick random node and execute COMMAND DOCS on it
using var docs = await clusterRouter.PickRandom().Command(COMMAND.DOCS);
//group objects by node and execute commands on it
(Key Key, Value Value)[] objects = [(Key.Utf8("k_1"), Value.Utf8("v_1")), /* ... */ (Key.Utf8("k_n"), Value.Utf8("v_n"))];
foreach (var (node, nodeObjects)in clusterRouter.RouteTo(RolePreference.Primary).GroupBy(objects, o => o.Key))
await node.Mset(nodeObjects);
```
## Known Limitations
* Pub/Sub API is limited to the RESP3 variant
* No support for secure connections
* No specialized API for selecting logical databases
* No flow control/backpressure — use an external rate limiter
* No support for Sentinel
* No dedicated API for transactions. They can be made with command combos, though the approach is a bit hacky:
```
using var comboResult = await Combo.Build(client)
.Cmd(r => r.MultiCommand())
.Cmd(r => r.IncrVoidCommand(key)) // Void overload used to avoid processing the -QUEUED response
.Cmd(r => r.ExecCommand())
.Execute();
```
## Contributing
Contributions, bug reports, and feedback are welcome and appreciated.
## Disclaimer
This project is an independent work and is not endorsed, supported, or certified by Redis.
## License
This project is licensed under the [MIT License](LICENSE).