A Redis client for .NET. Your calls go out as RESP, the replies come back as string?, long, and your own types.
dotnet add package Respire --prereleaseOn the wire 55 bytes
*5\r\n$3\r\nSET\r\n$8\r\ngreeting\r\n$5\r\nhello\r\n$2\r\nEX\r\n$3\r\n300\r\nIn Respire
await redis.SetAsync("greeting", "hello", expiry: TimeSpan.FromSeconds(300));Connect, set, get. That is the setup.
One package, one connection string. There is no multiplexer to configure and no value wrapper to unwrap.
Build a small app in five minutesawait using var redis =
await RespireClient.ConnectAsync("redis://localhost");
await redis.SetAsync("user:1", new User("Ada", 36),
expiry: TimeSpan.FromMinutes(5));
User? user = await redis.GetAsync<User>("user:1");
- Real return types
string?,long,bool, or your ownT?. No value wrapper to unwrap. - Reads from memoryServer-assisted client caching. A cached string GET takes about 150 ns and allocates nothing.
- Pipelined by defaultConcurrent calls share socket writes. There is no batching switch.
- Blocking commands that work
BLPOPand blocking stream reads get their own connections. - Coordination built inLocks, rate limiters, semaphores, leader election, queues, and stream workers.
- Tests without DockerAn in-memory server with a fake clock runs your real client code.
The hard parts, one call each
Locks, rate limits, queues, and retrying transactions are where Redis code usually gets long. In Respire each one is a method on the client you already have.
See every coordination primitiveMemory hit, else Redis, else your factory. Concurrent misses share one factory call.
var redis = await RespireClient.ConnectAsync(new RespireOptions
{
Endpoints = { new RespireEndpoint("localhost") },
ClientSideCache = new() { CoalesceConcurrentMisses = true },
});
User? user = await redis.GetOrSetAsync<User>(
"user:42", LoadUserAsync, TimeSpan.FromMinutes(5));
Replies come back as C# types
Pick a command to see the frame Respire writes, the frame Redis sends back, and the value your code receives. Errors become exceptions. Pushes become cache evictions.
Values and serializationYou write
string? name = await redis.GetStringAsync("user:42:name");
Respire sends
*2\r\n$3\r\nGET\r\n$12\r\nuser:42:name\r\nRedis replies
$3\r\nAda\r\nYou get
name is "Ada"string?
- +Simple string
- -Error
- :Integer
- $Bulk string
- * %Array, map
- >Push
Concurrent callers share one socket write
Each await looks like its own round trip. Requests that arrive together are written to the socket together, and replies are matched back in order. There is no batching switch to remember.
- A
await redis.GetStringAsync("user:1")waiting - B
await redis.IncrementAsync("visits")waiting - C
await redis.Hashes.GetAllAsync("cart:9")waiting
ABCHot reads never leave the process
Turn on client-side caching and eligible reads stay in bounded process memory. Redis sends a > push frame when a key you read changes, and Respire evicts the stale copy for you.
var options = new RespireOptions
{
Endpoints = { new("localhost") },
ClientSideCache = new(),
};
Measured with BenchmarkDotNet on .NET 10: 186.5 μs for a StackExchange.Redis GET against a local server, 151.5 ns and 0 B allocated for a Respire cache hit. See the benchmark run.
Blocking reads wait on their own connections
BLPOP and blocking stream reads hold a connection until data arrives. Respire sends them over a dedicated pool, so a worker waiting 30 seconds for a job never delays the GETs behind it.
string? job = await redis.Lists.LeftPopAsync(
"jobs",
waitFor: TimeSpan.FromSeconds(30));
BLPOP jobs waiting, up to 30sWhat the docs cover
$10 Data types
*7
$12 Coordination
*6
$10 Production
*6
Try Respire in your next .NET application.
dotnet add package Respire --prerelease