Equinox.Redis
0.1.0
dotnet add package Equinox.Redis --version 0.1.0
NuGet\Install-Package Equinox.Redis -Version 0.1.0
<PackageReference Include="Equinox.Redis" Version="0.1.0" />
<PackageVersion Include="Equinox.Redis" Version="0.1.0" />
<PackageReference Include="Equinox.Redis" />
paket add Equinox.Redis --version 0.1.0
#r "nuget: Equinox.Redis, 0.1.0"
#:package Equinox.Redis@0.1.0
#addin nuget:?package=Equinox.Redis&version=0.1.0
#tool nuget:?package=Equinox.Redis&version=0.1.0
Equinox.Redis
A high-performance Redis client for .NET built with official Microsoft libraries only. Targets .NET Standard 2.0 (.NET Framework 4.6.1+) and .NET Standard 2.1 (modern .NET).
Features
- High-level abstractions — Cache, Counter, Queue, Stack, Set, SortedSet, HashMap, and distributed Lock with JSON/POCO serialization
- RESP2 / RESP3 — Automatic RESP3 negotiation with transparent RESP2 fallback
- Bitmaps, HyperLogLog, Geo — Full command coverage for bit ops, cardinality, and geospatial queries
- Connection pooling — Hybrid multiplexing for concurrent commands, exclusive sockets for blocking/pipelines/transactions, idle trim, and per-node
clustermaxpoolsize - Allocation-conscious — Pooled command arg arrays on hot APIs, buffered RESP I/O, and firehose multiplex writes
- Standalone, Cluster, and Sentinel — Connection strings for all three topologies
- Pipelines & transactions — Batched commands and MULTI/EXEC with connection affinity
- Pub/Sub — Subscribe, pattern subscribe, and publish
- Async throughout — Cancellation tokens on I/O paths
- SSL/TLS — Secure connections with certificate options
- No third-party Redis SDKs — Only official Microsoft packages (plus
System.Text.Json)
Installation
dotnet add package Equinox.Redis
Quick Start
using Equinox.Redis;
using Equinox.Redis.Abstractions;
await using var redis = await RedisClient.ConnectAsync("host=localhost;port=6379");
// Prefer abstractions for application code
var cache = new RedisCache(redis, keyPrefix: "app");
await cache.SetAsync("user:1", new { Name = "Ada" }, TimeSpan.FromMinutes(10));
var user = await cache.GetAsync<UserDto>("user:1");
Connection options:
// Connection string
await using var redis = new RedisClient("host=localhost;port=6379;password=secret;maxpoolsize=20");
// Settings object
await using var redis2 = new RedisClient(new ConnectionSettings
{
Host = "localhost",
Port = 6379,
Password = "secret",
MaxPoolSize = 20
});
// Cluster / Sentinel
await using var cluster = new RedisClient("cluster://127.0.0.1:17000,127.0.0.1:17001,127.0.0.1:17002");
await using var sentinel = new RedisClient("sentinel://127.0.0.1:26379,127.0.0.1:26380/mymaster");
Abstractions APIs
All abstractions live in Equinox.Redis.Abstractions. Construct them with a RedisClient and a key (or key prefix). Values are serialized with System.Text.Json unless noted.
Cache (RedisCache)
Typed get/set with optional TTL and get-or-set.
var cache = new RedisCache(redis, keyPrefix: "cache");
await cache.SetAsync("session:abc", sessionDto, TimeSpan.FromMinutes(30));
var session = await cache.GetAsync<SessionDto>("session:abc");
var profile = await cache.GetOrSetAsync("profile:42", async () =>
{
return await LoadProfileFromDbAsync(42);
}, TimeSpan.FromHours(1));
await cache.ExistsAsync("session:abc");
await cache.RemoveAsync("session:abc");
await cache.SetExpiryAsync("profile:42", TimeSpan.FromMinutes(5));
var ttl = await cache.GetTimeToLiveAsync("profile:42");
Counter (RedisCounter)
Atomic counters with optional expiry.
var counters = new RedisCounter(redis, keyPrefix: "metrics");
await counters.IncrementAsync("pageviews");
await counters.IncrementByAsync("pageviews", 10);
await counters.DecrementAsync("inventory:sku-1");
long value = await counters.GetAsync("pageviews");
await counters.SetAsync("pageviews", 1000);
await counters.ResetAsync("pageviews");
await counters.SetExpiryAsync("pageviews", TimeSpan.FromDays(1));
Queue (RedisQueue<T>) — FIFO
var jobs = new RedisQueue<JobMessage>(redis, key: "queue:jobs");
await jobs.EnqueueAsync(new JobMessage { Id = "j1", Type = "email" });
JobMessage? next = await jobs.DequeueAsync();
JobMessage? peek = await jobs.PeekAsync();
long length = await jobs.LengthAsync();
JobMessage?[] all = await jobs.GetAllAsync();
await jobs.ClearAsync();
Stack (RedisStack<T>) — LIFO
var undo = new RedisStack<EditOp>(redis, key: "stack:undo:doc-9");
await undo.PushAsync(new EditOp { Action = "insert" });
EditOp? last = await undo.PopAsync();
EditOp? top = await undo.PeekAsync();
long count = await undo.CountAsync();
Set (RedisSet<T>)
var tags = new RedisSet<string>(redis, key: "set:post:12:tags");
await tags.AddAsync("redis");
await tags.AddAsync("dotnet");
bool has = await tags.ContainsAsync("redis");
string? random = await tags.GetRandomAsync();
string? popped = await tags.PopAsync();
string?[] members = await tags.GetAllAsync();
await tags.RemoveAsync("dotnet");
await tags.ClearAsync();
Sorted set (RedisSortedSet<T>)
Leaderboards and ranked collections.
var scores = new RedisSortedSet<string>(redis, key: "zset:leaderboard");
await scores.AddAsync("alice", 1200);
await scores.AddAsync("bob", 950);
await scores.IncrementScoreAsync("bob", 100);
double? aliceScore = await scores.GetScoreAsync("alice");
long? rank = await scores.GetRankAsync("alice"); // low → high
long? revRank = await scores.GetReverseRankAsync("alice"); // high → low
string?[] top3 = await scores.GetTopAsync(3);
string?[] bottom3 = await scores.GetBottomAsync(3);
string?[] byRank = await scores.GetRangeByRankAsync(0, 9);
string?[] byScore = await scores.GetRangeByScoreAsync(1000, 2000);
Hash map (RedisHashMap)
Field access plus POCO mapping (nested properties flatten with :). Use [RedisField("name")] and [RedisIgnore] to control mapping.
var userHash = new RedisHashMap(redis, key: "hash:user:1");
await userHash.SetFieldAsync("email", "ada@example.com");
await userHash.SetFieldsAsync(new Dictionary<string, string>
{
["name"] = "Ada",
["role"] = "admin"
});
string? email = await userHash.GetFieldAsync("email");
await userHash.IncrementFieldAsync("loginCount");
// POCO round-trip
await userHash.SetObjectAsync(new User
{
Name = "Ada",
Email = "ada@example.com",
Address = new Address { City = "London" } // stored as Address:City
});
User? loaded = await userHash.GetObjectAsync<User>();
// Patch only changed fields
var updated = await userHash.GetObjectAsync<User>();
updated!.Email = "new@example.com";
await userHash.UpdateFieldsAsync(loaded, updated);
public sealed class User
{
public string Name { get; set; } = "";
[RedisField("email_addr")]
public string Email { get; set; } = "";
[RedisIgnore]
public string? Scratch { get; set; }
public Address? Address { get; set; }
}
Distributed lock (RedisLock)
SET NX + PX acquire; safe release with an ownership token (Lua).
var locker = new RedisLock(redis, key: "lock:invoice:42", timeout: TimeSpan.FromSeconds(30));
if (await locker.AcquireAsync())
{
try
{
await ProcessInvoiceAsync(42);
await locker.ExtendAsync(TimeSpan.FromSeconds(30));
}
finally
{
await locker.ReleaseAsync();
}
}
// Retry until acquired or wait timeout elapses
if (await locker.TryAcquireAsync(TimeSpan.FromSeconds(5)))
{
await DoWorkAsync();
await locker.ReleaseAsync();
}
bool held = await locker.IsHeldAsync();
Scoped pattern (acquire + auto-release):
var locker = new RedisLock(redis, "lock:resource", TimeSpan.FromSeconds(15));
await using (await locker.AcquireScopedAsync())
{
await DoWorkAsync();
}
Command API (lower level)
When you need raw Redis commands, use the typed command surfaces on RedisClient:
await redis.Strings.SetAsync("key", "value", TimeSpan.FromMinutes(5));
var value = await redis.Strings.GetAsync("key");
await redis.Lists.RPushAsync("list", "a", "b");
await redis.Sets.SAddAsync("set", "x", "y");
await redis.Hashes.HSetAsync("hash", "f", "v");
await redis.SortedSets.ZAddAsync("zset", (1.0, "one"));
await redis.Streams.XAddAsync("stream", new Dictionary<string, string> { ["f"] = "v" });
await redis.Bitmaps.SetBitAsync("flags", 7, true);
await redis.HyperLogLog.AddAsync("visitors", "user:1", "user:2");
await redis.Geo.AddAsync("cities", new GeoEntry(13.36, 38.11, "Palermo"));
await redis.Keys.ExpireAsync("key", TimeSpan.FromHours(1));
await redis.PingAsync();
Pipelines, transactions, pub/sub
By default, non-blocking commands share multiplexed socket(s) with a firehose writer (batch writes + flush) so Task.WhenAll scales without one connection per call.
Raise multiplexconnectioncount under heavy write contention (default is 2).
Blocking and connection-local commands (BLPOP, CLIENT, MULTI, …) use an exclusive pooled connection so they do not stall or desync other traffic.
Pipelines still borrow one exclusive connection and write all commands before reading replies (order preserved).
Prefer CreatePipeline() when you want many commands in a single RTT on one connection.
Pub/sub subscribers use a dedicated connection outside the command pool.
var pipeline = redis.CreatePipeline();
pipeline.Queue("SET", "a", "1");
pipeline.Queue("SET", "b", "2");
pipeline.Queue("GET", "a");
var results = await pipeline.ExecuteAsync();
await using var tx = await redis.BeginTransactionAsync();
tx.Queue("INCR", "counter");
tx.Queue("GET", "counter");
var exec = await tx.ExecAsync();
await redis.PubSub.PublishAsync("channel", "hello");
await using var sub = await redis.PubSub.CreateSubscriberAsync();
sub.MessageReceived += (_, e) => Console.WriteLine(e.Message);
await sub.SubscribeAsync("channel");
var result = await redis.ExecuteRawAsync("CLIENT", new[] { "LIST" });
Connection String Options
| Option | Description | Default |
|---|---|---|
host, server |
Redis server hostname | localhost |
port |
Redis server port | 6379 |
password, pwd |
Authentication password | null |
username, user |
Authentication username (Redis 6+) | null |
database, db |
Database index | 0 |
maxpoolsize, max |
Maximum pool connections (standalone/sentinel) | 10 |
minpoolsize, min |
Minimum pool connections (idle trim floor) | 1 |
clustermaxpoolsize |
Maximum connections per cluster node pool | 4 |
idleconnectiontimeout |
Close idle pooled connections above min (ms); 0 disables |
60000 |
enablemultiplexing, multiplexing |
Share sockets for non-blocking commands | true |
multiplexconnectioncount, multiplexconnections |
Number of shared multiplex sockets | 2 |
connecttimeout, timeout |
Connection timeout (ms) | 5000 |
commandtimeout |
Command timeout (ms) | 30000 |
ssl, tls |
Enable SSL/TLS | false |
sslhost |
SSL certificate host | null |
clientname, name |
Client connection name | null |
autoreconnect |
Auto-reconnect on failure | true |
protocol |
auto (RESP3 then RESP2), resp2, or resp3 |
auto |
URL forms: cluster://host:port,... and sentinel://host:port,.../masterName.
Building
dotnet build
Testing
# Full local E2E (standalone + cluster + sentinel via Podman)
./scripts/run-all-e2e-tests.sh --keep
# Standalone only (defaults to localhost:6380)
./scripts/run-e2e-tests.sh --keep
REDIS_PORT=6380 dotnet test
License
MIT License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. 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 was computed. 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. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 is compatible. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Bcl.AsyncInterfaces (>= 8.0.0)
- System.Memory (>= 4.5.5)
- System.Text.Json (>= 8.0.5)
- System.Threading.Channels (>= 8.0.0)
- System.Threading.Tasks.Extensions (>= 4.5.4)
-
.NETStandard 2.1
- System.Text.Json (>= 8.0.5)
- System.Threading.Channels (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|