Skip to content
thomhurstPublic

About

A .NET RESP Client

Topics

Resources

Stars

21 stars

Watchers

2 watching

Forks

Latest commit

 

History

1,568 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Respire

A fast, modern Redis client for .NET. Plain C# types in, plain C# types out, and the hard parts of Redis are one call each

Works with Redis, Valkey, KeyDB, and other RESP-compatible servers.

Documentation · Getting started · Benchmarks · Coming from StackExchange.Redis?

dotnet add package Respire
await 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");

That is the whole setup. No multiplexer to configure, no RedisValue to unwrap.

The public API may change before the first supported major release, 2.0.0. See release candidates, the roadmap, and the experimental API policy.

Why Respire?

  • Real return types. string?, long, bool, TimeSpan, or your own T?.
  • Reads from memory. Server-assisted client-side caching: a cached string GET takes about 150 ns and allocates nothing. Redis tells Respire when to evict.
  • Pipelined by default. Concurrent calls share socket writes. No batching switch.
  • Blocking commands that work. BLPOP and blocking stream reads get their own connections, so they never stall other calls.
  • Coordination built in. Locks, rate limiters, semaphores, leader election, queues, and stream workers. No extra library to glue on.
  • Tests without Docker. An in-memory server with a fake clock runs your real client code.
  • Every command. Typed facets per data type, plus a generated catalog of every Redis 8.10 and Valkey 9.1 command.

The hard parts, one call each

Cache-aside with stampede protection. Memory hit, else Redis, else your factory. Concurrent misses share one factory call.

await using 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));

static ValueTask<User?> LoadUserAsync(CancellationToken cancellationToken) => new(new User("Ada", 36));

A distributed lock that renews itself. Disposing the attempt releases the lock.

await using var attempt = await redis.Locks.AcquireAsync(
    "locks:nightly-report", TimeSpan.FromSeconds(30), keepAlive: true);

if (attempt.Acquired)
{
    await RunReportAsync(attempt.Lock.KeepAliveCancellationToken);
}

A rate limit shared by every server. A standard .NET RateLimiter, so the ASP.NET Core middleware accepts it.

using Respire.Coordination;

await using var limiter = redis.Coordination.RateLimiters.SlidingWindow(
    $"limits:api:{userId}", permitLimit: 100, window: TimeSpan.FromMinutes(1), segments: 6);

using var lease = await limiter.AcquireAsync(1, cancellationToken);
if (!lease.IsAcquired)
{
    return; // Over the limit: reject the request, for example with HTTP 429.
}

Optimistic transactions with retries. Read, decide, write. If another client changes the key first, Respire runs the callback again.

await redis.RunTransactionWithReadsAsync(["stock:42"], async (reads, transaction, token) =>
{
    long stock = long.Parse(await reads.GetStringAsync("stock:42", token) ?? "0");
    if (stock > 0)
        transaction.Set("stock:42", stock - 1);
});

A reliable queue. Deduplicated sends, visibility timeouts, and redelivery when a worker dies.

using System.Text;
using Respire.Streaming;

var queue = new RespireReliableQueue<string>(redis, "emails", Encoding.UTF8.GetBytes, Encoding.UTF8.GetString);
await queue.SendAsync("welcome:ada@example.com", deduplicationId: "welcome:42");

foreach (var message in await queue.ReceiveAsync(visibilityTimeout: TimeSpan.FromSeconds(30)))
{
    await HandleAsync(message.Payload);
    await queue.AckAsync(message);
}

Pub/sub as await foreach.

await using var orders = await redis.SubscribeAsync("orders");

await foreach (var message in orders)
{
    Console.WriteLine(message.Text);
}

Many commands, one round trip. Results are typed.

await using var batch = redis.CreateBatch();
var name = batch.GetString("name");
var visits = batch.Increment("visits");
await batch.ExecuteAsync();

Console.WriteLine($"{name.Result}: {visits.Result}");

Tests without Docker. Expire a key by moving the clock, not by waiting.

using Respire.Testing;

var clock = new RespireFakeClock();
await using var server = new RespireFakeServer(clock);
await using var client = await RespireClient.ConnectAsync(server.CreateOptions());

await client.SetAsync("session", "abc", expiry: TimeSpan.FromMinutes(20));
clock.Advance(TimeSpan.FromMinutes(21));
string? expired = await client.GetStringAsync("session"); // null

More in the box

You need With Respire
ASP.NET Core registration services.AddRespire(connectionString)
IDistributedCache and HybridCache services.AddRespireHybridCache(...)
.NET Aspire builder.AddRespireClient("cache")
Stream consumer workers services.AddRespireStreamWorker<THandler>(stream, group)
Job queues with retries and scheduling services.AddRespireJobQueue<THandler, TJob>(...)
Leader election with fencing tokens new RespireLeaderElection(...)
Semaphores, read-write locks, latches, Redlock redis.Coordination.CreateSemaphore(key, capacity)
A blocking work queue redis.Lists.LeftPopAsync(key, waitFor: timeout)
Cluster and Sentinel ?cluster=true for Cluster, ?serviceName=mymaster for Sentinel
OpenTelemetry Traces and metrics built in
NativeAOT Pass a source-generated JsonSerializerContext

Packages

Package Adds
Respire The client, locks, pub/sub, streams, batches, Cluster, and Sentinel
Respire.Coordination Rate limiters, semaphores, read-write locks, latches, leader election, and Redlock
Respire.Streaming Reliable queues, job queues, and stream workers
Respire.DependencyInjection AddRespire and keyed clients
Respire.Caching, Respire.Caching.Hybrid IDistributedCache and HybridCache
Respire.Json, .Search, .TimeSeries, .Probabilistic Typed module APIs such as redis.Json
Respire.Testing, Respire.Testing.Containers An in-memory server, or Redis in a container

Plus SignalR, DataProtection, output caching, health checks, FusionCache, and more: see all packages.

License

MIT

About

A .NET RESP Client

Topics

Resources

Stars

21 stars

Watchers

2 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages