Respire 0.6.1
Prefix Reserveddotnet add package Respire --version 0.6.1
NuGet\Install-Package Respire -Version 0.6.1
<PackageReference Include="Respire" Version="0.6.1" />
<PackageVersion Include="Respire" Version="0.6.1" />
<PackageReference Include="Respire" />
paket add Respire --version 0.6.1
#r "nuget: Respire, 0.6.1"
#:package Respire@0.6.1
#addin nuget:?package=Respire&version=0.6.1
#tool nuget:?package=Respire&version=0.6.1
Respire
Respire is a fast, modern RESP client for .NET. It works with Redis, Valkey, KeyDB, and other RESP-compatible servers while keeping the API familiar to C# developers.
await using var redis = await RespireClient.ConnectAsync("redis://localhost");
await redis.SetAsync("greeting", "hello", expiry: TimeSpan.FromMinutes(5));
string? greeting = await redis.GetStringAsync("greeting");
await redis.SetAsync("user:1", new User("Ada", 36));
User? user = await redis.GetAsync<User>("user:1");
Status: Respire is pre-release, so its API may still change. See the roadmap for remaining work.
Why Respire?
- Natural .NET APIs. Get
string?,long,bool,TimeSpan?, orT?directly—no protocol wrapper to unpack. Nullability tells you when a result can be missing. - Fast by default. Respire coalesces commands from concurrent callers into fewer socket writes, parses replies from pooled buffers, and spreads work across multiplexed connections. No batching switch is required.
- Hot reads without a network round trip. Optional RESP3 client-side caching stores eligible reads in bounded process memory while Redis pushes invalidations when keys change. Existing APIs become cache-aware without application-managed keys, subscriptions, or refresh code.
- Blocking commands that do not block everything else. Commands such as
BLPOPuse a dedicated pooled connection, leaving normal traffic free to flow. - An API that is easy to explore. Commands are grouped by data type (
redis.Hashes,redis.Streams,redis.SortedSets, and more), while common string operations remain on the client itself. - Modern async patterns. Pub/sub, stream consumer groups, and the
SCANfamily useIAsyncEnumerable. Expiries useTimeSpanandDateTimeOffset. - Safer failure modes. Early batch awaits fail immediately instead of deadlocking. Cancellation abandons the wait without leaving a partial RESP frame on the connection.
- Production-friendly. Built-in reconnection, resubscribing pub/sub, OpenTelemetry, dependency injection, typed serialization, and testable interfaces.
Everyday patterns
Server-assisted client-side caching
Turn on one option and keep using the same typed APIs:
await using var cachedRedis = await RespireClient.ConnectAsync(new RespireOptions
{
Endpoints = { new RespireEndpoint("localhost") },
ClientSideCache = new(),
});
// First call reads Redis. Repeated calls use the in-process cache.
string? name = await cachedRedis.GetStringAsync("user:42:name");
Redis tracks only reads Respire opts into. When any client changes a tracked key, Redis pushes an
invalidation and Respire evicts the local entry; the next read refreshes it lazily. Missing keys,
MGET, hashes, collections, JSON, vector sets, and other deterministic keyed reads participate.
StackExchange.Redis 3.1.13 does not provide an equivalent built-in server-assisted local cache.
Its keyspace-notification APIs can be used to build application-owned invalidation, but storage,
bounds, command eligibility, and race handling remain application concerns. In an official net10
BenchmarkDotNet short run, a cached Respire GET took 151.5 ns versus 186.5 μs for a
StackExchange.Redis server read. That difference measures removing the network round trip—not a
1,000× difference between the clients' uncached wire paths, which measured the same statistically.
Learn how client-side caching works or inspect the benchmark run.
Blocking list reads
Set waitFor and Respire automatically uses a dedicated connection:
string? job = await redis.Lists.LeftPopAsync(
"jobs",
waitFor: TimeSpan.FromSeconds(30));
Pub/sub
Subscriptions are async streams. Leaving the loop and disposing the subscription handles
cleanup—no delegate bookkeeping required. SubscribeAsync returns once the server has
acknowledged the SUBSCRIBE, so the next publish is guaranteed to reach it.
await using var subscription = await redis.SubscribeAsync("orders", token);
await foreach (var message in subscription.WithCancellation(token))
{
Console.WriteLine($"{message.Channel}: {message.Text}");
}
Redis 7 sharded pub/sub uses SSUBSCRIBE and SPUBLISH. Run this as a separate consumer:
await using var shard = await redis.SubscribeShardedAsync("orders:europe", token);
await using var shardMessages = shard.GetAsyncEnumerator(token);
await redis.PublishShardedAsync("orders:europe", "ready", token);
if (await shardMessages.MoveNextAsync())
{
Console.WriteLine(shardMessages.Current.Text);
}
Batches and transactions
Batch commands share one flush. Transactions use one connection and return typed pending
results. Both carry the same facets as the client — batch.Lists.RightPush mirrors
redis.Lists.RightPushAsync — but return a RespirePending<T> instead of awaiting.
var batch = redis.CreateBatch();
var name = batch.GetString("name");
var visits = batch.Increment("visits");
var profile = batch.Hashes.GetAll("user:1");
RespireBatchResult batchResult = await batch.ExecuteAsync();
batchResult.ThrowIfAnyFailed();
Console.WriteLine($"{name.Result}: {visits.Result} ({profile.Result.Count} fields)");
await using var transaction = redis.CreateTransaction();
var balance = transaction.Increment("balance", -100);
transaction.Lists.RightPush("audit", "withdraw:100");
await transaction.CommitAsync();
Always commit or dispose a transaction so its pooled buffer and any dedicated WATCH connection
are released. await using protects early-return and command-queuing failure paths; disposal is
a no-op after a successful commit.
CreateTransaction() returns RespireTransaction, whose CommitAsync completes without a
result because an unwatched EXEC cannot abort. Use CreateTransactionAsync(["balance"]) for
optimistic concurrency with WATCH; it returns RespireWatchedTransaction, whose commit result
must be checked. Read the current value through the client—not a deferred transaction read—then
queue the conditional update,
then recreate and retry the whole attempt when CommitAsync returns false:
bool applied;
do
{
await using var watched = await redis.CreateTransactionAsync(["balance"]);
long current = long.Parse((await redis.GetStringAsync("balance"))!);
watched.Set("balance", current - 100);
applied = await watched.CommitAsync();
}
while (!applied);
Distributed locks
AcquireAsync generates the owner token and returns a non-null attempt. Check Acquired, then use
the Lock handle; disposing the attempt also releases an acquired lock.
await using var attempt = await redis.Locks.AcquireAsync("locks:report", TimeSpan.FromSeconds(30));
if (!attempt.Acquired)
{
return; // someone else holds it
}
var mutex = attempt.Lock;
await RunReportAsync();
A lock is a lease, not a mutex: it disappears on its own when its Duration elapses, even
mid-work. RemainingEstimate and ExpiresAtEstimate provide local best-effort deadlines. For
longer work, start a keep-alive and pass its cancellation token into the protected operation;
renewal failure cancels the token immediately:
await using var keepAlive = await mutex.KeepAliveAsync(cancellationToken);
await RunReportAsync(keepAlive.CancellationToken);
if (keepAlive.OwnershipLost)
{
// Do not publish protected output; another owner may be active.
}
You can instead call mutex.ResetExpiryAsync(...) directly and stop protected writes when it returns
false. ReleaseAsync returns LockReleaseOutcome, distinguishing Released,
AlreadyReleased, and NotOwned. Every operation compares the token on the server, so an expired
handle never extends or deletes the next owner's lock.
When contention is exceptional, AcquireOrThrowAsync returns the handle directly and throws
RespireLockNotAcquiredException after the optional wait budget:
await using var mutex = await redis.Locks.AcquireOrThrowAsync(
"locks:report",
TimeSpan.FromSeconds(30),
wait: TimeSpan.FromSeconds(5)); // retries every 50 ms by default
TryTakeAsync, ResetExpiryAsync, ReleaseAsync, and GetOwnerTokenAsync are the raw-token APIs for
callers that must share ownership between processes or outlive the acquiring process:
var token = Guid.NewGuid().ToString("N");
if (await redis.Locks.TryTakeAsync("locks:report", token, TimeSpan.FromSeconds(30)))
{
try
{
await RunReportAsync();
}
finally
{
await redis.Locks.ReleaseAsync("locks:report", token);
}
}
Redis Cluster
Enable cluster routing and provide one or more seed nodes. Respire loads CLUSTER SLOTS, follows
MOVED/ASK redirects, and caches learned routes. Batches may span nodes; transactions must keep
all keys in one slot, so use Redis hash tags for related keys. WATCH transactions are not
supported in cluster mode—use a same-slot Lua script instead. Sharded pub/sub is also unavailable
in cluster mode; SSUBSCRIBE subscriptions require a non-cluster client.
await using var cluster = await RespireClient.ConnectAsync(new RespireOptions
{
UseCluster = true,
Endpoints =
{
new("redis-1", 6379),
new("redis-2", 6379),
},
});
await cluster.SetAsync("{account:42}:name", "Ada");
await cluster.SetAsync("{account:42}:balance", 100);
A single seed can also be enabled with redis://redis-1?cluster=true.
Zero-copy reads and custom commands
Normal reads favor convenient .NET values. For large payloads, opt into a disposable lease:
using RespireLease blob = await redis.Strings.GetLeaseAsync("blob:4mb");
Process(blob.Span);
Every command in the Redis 8.10 and Valkey 9.1 references is available through the generated,
discoverable RespireCommands catalog. It also includes Redis's integrated JSON, Search,
probabilistic, time-series, and vector commands, Valkey modules, and documented KeyDB and
Dragonfly extensions. Command words are pre-encoded once; only arguments are written per call:
using var document = await redis.ExecuteAsync(
RespireCommands.Json.JSON_SET, "user:1", "$", payload);
using var encoding = await redis.ExecuteAsync(
RespireCommands.Key.OBJECT_ENCODING, "user:1");
Catalog descriptors do not encode key positions, so catalog execution is rejected on
WithKeyPrefix views; use the typed facets there to preserve key isolation.
Strings convert implicitly to RespireCommand for experimental or server-specific commands.
Interpolated values are encoded as single arguments, so spaces stay safe. Format strings and
alignment are honored with invariant culture; holes use IFormattable or ToString() and do not
pass through a Respire serializer.
App integration
Dependency injection
builder.Services.AddRespire(builder.Configuration.GetConnectionString("redis")!);
// Named clients are supported too.
builder.Services.AddKeyedRespire("sessions", "redis://sessions-host");
public sealed class CartService(
[FromKeyedServices("sessions")] IRespireClient redis);
Registration is lazy, so Redis availability never blocks application startup. ConnectTimeout
bounds socket and TLS setup; the Redis handshake and non-blocking commands use CommandTimeout.
Blocking commands use their explicit wait timeout, and caller cancellation applies throughout.
Standalone clients surface setup exceptions directly, while cluster clients wrap seed failures in
RespireConnectionException. The next command starts a new connection attempt.
NativeAOT and trimming
Typed values use reflection-based System.Text.Json metadata by default. For a trimmed or NativeAOT application, generate metadata for every stored type and pass that context to Respire:
using System.Text.Json.Serialization;
using Respire;
using Respire.Serialization;
var options = new RespireOptions
{
Endpoints = { new RespireEndpoint("localhost") },
Serializer = SystemTextJsonSerializer.FromContext(AppJsonContext.Default),
};
await using var redis = await RespireClient.ConnectAsync(options);
// The generic APIs are conservatively annotated because IRespireSerializer can be
// reflection-based. This configured context makes these two calls AOT-safe.
#pragma warning disable IL2026, IL3050
await redis.SetAsync("user:1", new User("Ada", 36));
User? user = await redis.GetAsync<User>("user:1");
#pragma warning restore IL2026, IL3050
[JsonSerializable(typeof(User))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
Add a [JsonSerializable] entry for each non-primitive type. Strings, byte arrays, Boolean values,
and numeric values use Respire's built-in codecs and do not need generated JSON metadata. Custom
serializers can also override the Type-based IRespireSerializer members for polymorphic adapters.
IDistributedCache and HybridCache
Respire.Extensions.Caching provides IDistributedCache and IBufferDistributedCache.
Respire.Extensions.Caching.Hybrid adds Respire as the L2 backend for HybridCache.
builder.Services.AddRespireDistributedCache(
"redis://localhost",
instanceName: "myapp:");
// L1 memory + L2 Redis
builder.Services.AddRespireHybridCache(
"redis://localhost",
instanceName: "myapp:");
Cache entries use the same layout as Microsoft.Extensions.Caching.StackExchangeRedis, so you
can switch without flushing existing entries. Sliding-expiration reads also refresh their TTL
atomically in the same round trip.
Redis ACL note: Cache users need
EVALSHA,EVAL,SET,UNLINK,HSET,HMGET,PTTL,PEXPIRE,PERSIST, andEXISTS. Timeout- or cancellation-safe calls also requireCLIENT IDandCLIENT KILL.
More capabilities
- Lua scripts with automatic
EVALSHAtoEVALfallback - Streams and consumer groups with per-entry acknowledgement
- Key-prefixed client views for multi-tenant applications
- Redis Sentinel primary discovery when connecting
- Sharded pub/sub for Redis 7
- Automatic reconnect and pub/sub resubscribe
- OpenTelemetry spans and metrics through
ActivitySourceandMeter, both namedRespire - Custom
IRespireSerializersupport with a System.Text.Json default IRespireClientand per-feature interfaces for straightforward testing
Redis telemetry follows OpenTelemetry database semantic conventions. db.namespace reports
the database index configured when the connection was established; raw SELECT commands are
not tracked. Query text is not collected because arbitrary Redis command values cannot be
reliably sanitized. Operation latency uses the stable db.client.operation.duration histogram
in seconds; pipelines and transactions are recorded as single operations.
See command coverage for audited sources and regeneration details,
and API design for design decisions, wire architecture, and roadmap.
Reproducible comparisons with StackExchange.Redis—including server reads versus Respire
client-cache hits—live in benchmarks/.
Documentation
Read the Respire documentation or run it locally:
cd website
npm install
npm start
Build and test
dotnet build Respire.slnx
dotnet test tests/Respire.Tests
dotnet test tests/Respire.IntegrationTests # Requires Docker
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- Microsoft.Extensions.ObjectPool (>= 10.0.11)
- Reservoir (>= 1.4.0)
- System.IO.Hashing (>= 10.0.11)
-
net8.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.11)
- Microsoft.Extensions.ObjectPool (>= 10.0.11)
- Reservoir (>= 1.4.0)
- System.Diagnostics.DiagnosticSource (>= 10.0.11)
- System.IO.Pipelines (>= 10.0.11)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Respire:
| Package | Downloads |
|---|---|
|
Respire.Extensions.Caching
IDistributedCache and IBufferDistributedCache implementations backed by Respire. |
|
|
Respire.Extensions.DependencyInjection
Dependency injection integration for Respire clients in Microsoft.Extensions.DependencyInjection applications. |
GitHub repositories
This package is not used by any popular GitHub repositories.