A .NET 10 implementation of the Redis Serialization Protocol using DotNetty and C# with a storage abstraction.
Further reading:
This repository includes a GitHub Actions workflow at .github/workflows/publish-nuget.yml that publishes all packable projects:
DredisDredis.Abstractions.AuthDredis.Abstractions.CommandDredis.Abstractions.Storage
The workflow uses nuget.org Trusted Publishing (OIDC) and does not require storing a long-lived NuGet API key in GitHub secrets.
One-time setup:
- On nuget.org, go to Trusted Publishing and add a GitHub policy with:
- Repository Owner: your GitHub org/user
- Repository:
Dredis - Workflow File:
publish-nuget.yml
- Add a GitHub repository secret named
NUGET_USERNAMEwith your nuget.org profile name. - Ensure each package ID (
Dredis,Dredis.Abstractions.Auth,Dredis.Abstractions.Command,Dredis.Abstractions.Storage) is owned by the same nuget.org owner used for the trusted policy.
Publishing:
- Push a tag like
v1.0.1, or run thePublish NuGet packagesworkflow manually from Actions. - The workflow builds, packs, and pushes all packages from
artifacts/*.nupkgwith duplicate pushes skipped.
Currently implemented RESP commands and behavior:
- Connection:
PING,ECHO - Strings:
GET,SET(supportsEX,PX,NX,XX),MGET,MSET - Keys:
DEL,EXISTS - Counters:
INCR,INCRBY,DECR,DECRBY - Bitmaps:
GETBIT,SETBIT,BITCOUNT,BITOP,BITPOS,BITFIELD(GET,SET,INCRBY,OVERFLOW) - Expiration:
EXPIRE,PEXPIRE,TTL,PTTL - Hashes:
HSET,HGET,HDEL,HGETALL - Lists:
LPUSH,RPUSH,LPOP,RPOP,LRANGE,LLEN,LINDEX,LSET,LTRIM - Sets:
SADD,SREM,SMEMBERS,SCARD - Sorted sets:
ZADD,ZREM,ZRANGE,ZCARD,ZSCORE,ZRANGEBYSCORE,ZINCRBY,ZCOUNT,ZRANK,ZREVRANK,ZREMRANGEBYSCORE - Streams:
XADD,XDEL,XLEN,XTRIM,XRANGE,XREVRANGE,XREAD,XINFO,XSETID,XGROUP CREATE,XGROUP DESTROY,XGROUP SETID,XGROUP DELCONSUMER,XREADGROUP,XACK,XPENDING,XCLAIM - Probabilistic:
PFADD,PFCOUNT,PFMERGE(HyperLogLog) - Bloom filter:
BF.RESERVE,BF.ADD,BF.MADD,BF.EXISTS,BF.MEXISTS,BF.INFO - Cuckoo filter:
CF.RESERVE,CF.ADD,CF.ADDNX,CF.INSERT,CF.INSERTNX,CF.EXISTS,CF.DEL,CF.COUNT,CF.INFO - t-digest:
TDIGEST.CREATE,TDIGEST.RESET,TDIGEST.ADD,TDIGEST.QUANTILE,TDIGEST.CDF,TDIGEST.RANK,TDIGEST.REVRANK,TDIGEST.BYRANK,TDIGEST.BYREVRANK,TDIGEST.TRIMMED_MEAN,TDIGEST.MIN,TDIGEST.MAX,TDIGEST.INFO - Top-K:
TOPK.RESERVE,TOPK.ADD,TOPK.INCRBY,TOPK.QUERY,TOPK.COUNT,TOPK.LIST,TOPK.INFO - Time series:
TS.CREATE,TS.ADD,TS.INCRBY,TS.DECRBY,TS.GET,TS.RANGE,TS.REVRANGE,TS.MRANGE,TS.MREVRANGE,TS.DEL,TS.INFO - Pub/Sub:
PUBLISH,SUBSCRIBE,UNSUBSCRIBE,PSUBSCRIBE,PUNSUBSCRIBE - Transactions:
MULTI,EXEC,DISCARD,WATCH,UNWATCH - JSON:
JSON.SET,JSON.GET,JSON.DEL,JSON.TYPE,JSON.STRLEN,JSON.ARRLEN,JSON.ARRAPPEND,JSON.ARRINDEX,JSON.ARRINSERT,JSON.ARRREM,JSON.ARRTRIM,JSON.MGET - Vectors:
VSET,VGET,VDIM,VDEL,VSIM,VSEARCH(metrics:COSINE,DOT,L2)
Notes:
XREADsupportsCOUNTandBLOCK.XREADGROUPsupportsCOUNTandBLOCK.XPENDINGsupports both summary and extended forms with filtering (IDLE, consumer, range).XCLAIMsupports all options:IDLE,TIME,RETRYCOUNT,FORCE,JUSTID.XINFOsupportsSTREAM,GROUPS, andCONSUMERS.ZRANGEandZRANGEBYSCOREboth supportWITHSCORESoption.ZINCRBYincrements member scores and creates members if they don't exist.ZRANKandZREVRANKreturn 0-based ranks in ascending and descending order respectively.- Consumer groups track pending entries with delivery count, idle time, and consumer ownership.
PUBLISHreturns the number of clients that received the message.SUBSCRIBEsends subscription confirmations and receives published messages via push messages.UNSUBSCRIBEwith no arguments unsubscribes from all channels.PSUBSCRIBEsupports glob-style patterns (*,?,[abc]) for channel matching.PUNSUBSCRIBEwith no arguments unsubscribes from all patterns.- Pattern subscriptions receive
pmessageresponses with pattern, channel, and message. MULTIbegins a transaction, queueing subsequent commands.EXECexecutes all queued commands atomically, returning an array of results.DISCARDcancels a transaction and discards all queued commands.WATCHprovides optimistic locking by monitoring keys for modifications.UNWATCHclears all watched keys (automatically cleared byEXECandDISCARD).- Transactions support optimistic locking via
WATCH: if a watched key is modified beforeEXEC, the transaction is aborted and returns null. JSON.SETstores JSON values at specified JSONPath locations.JSON.GETretrieves JSON values from one or multiple paths (defaults to root path$).JSON.DELdeletes JSON values at specified paths and returns the count of deleted paths.JSON.TYPEreturns the JSON type(s) at the specified path(s) (object, array, string, number, boolean, null).JSON.STRLENreturns the string length at the specified path(s).JSON.ARRLENreturns the array length at the specified path(s).JSON.ARRAPPENDappends one or more JSON values to the array at the specified path.JSON.ARRINDEXreturns the index of a JSON value in the array at the specified path, or -1 if not found.JSON.ARRINSERTinserts one or more JSON values at the specified index in the array.JSON.ARRREMremoves an element at the specified index from the array (or last element if no index provided).JSON.ARRTRIMtrims the array at the specified path to the specified range.JSON.MGETretrieves JSON values from multiple keys at the specified path.VSEARCHsupports paging/options viaLIMIT <n>and optionalOFFSET <n>before query vector components.VSEARCHis backward-compatible with positional top-k form:VSEARCH prefix topK metric ....VSEARCHstrict option order: metric form must beVSEARCH prefix metric LIMIT n [OFFSET n] ...; positional form supports only optionalOFFSET.TS.CREATEsupports optionalRETENTION <milliseconds>,ON_DUPLICATE <LAST|FIRST|MIN|MAX|SUM|BLOCK>, andLABELS <name value ...>.TS.ADDsupports optionalON_DUPLICATE <LAST|FIRST|MIN|MAX|SUM|BLOCK>.TS.RANGEandTS.REVRANGEsupport optionalCOUNT <n>andAGGREGATION <AVG|SUM|MIN|MAX|COUNT> <bucketMs>.TS.MRANGEandTS.MREVRANGEsupportFILTER <label=value ...>with optionalCOUNTandAGGREGATION.
| Area | Supported | Notes |
|---|---|---|
| RESP parsing/encoding | Yes | DotNetty Redis codec |
| Strings | Yes | GET, SET, MGET, MSET |
| Keys | Yes | DEL, EXISTS |
| Counters | Yes | INCR, INCRBY, DECR, DECRBY |
| Bitmaps | Yes | GETBIT, SETBIT, BITCOUNT, BITOP, BITPOS, BITFIELD (GET, SET, INCRBY, OVERFLOW) |
| Expiration | Yes | EXPIRE, PEXPIRE, TTL, PTTL |
| Hashes | Yes | HSET, HGET, HDEL, HGETALL |
| Lists | Yes | LPUSH, RPUSH, LPOP, RPOP, LRANGE, LLEN, LINDEX, LSET, LTRIM |
| Sets | Yes | SADD, SREM, SMEMBERS, SCARD |
| Sorted sets | Yes | ZADD, ZREM, ZRANGE, ZCARD, ZSCORE, ZRANGEBYSCORE, ZINCRBY, ZCOUNT, ZRANK, ZREVRANK, ZREMRANGEBYSCORE |
| Streams | Yes | XADD, XDEL, XLEN, XTRIM, XRANGE, XREVRANGE, XREAD, XINFO, XSETID |
| Probabilistic | Yes | PFADD, PFCOUNT, PFMERGE (HyperLogLog) |
| Bloom filter | Yes | BF.RESERVE, BF.ADD, BF.MADD, BF.EXISTS, BF.MEXISTS, BF.INFO |
| Cuckoo filter | Yes | CF.RESERVE, CF.ADD, CF.ADDNX, CF.INSERT, CF.INSERTNX, CF.EXISTS, CF.DEL, CF.COUNT, CF.INFO |
| t-digest | Yes | TDIGEST.CREATE, TDIGEST.RESET, TDIGEST.ADD, TDIGEST.QUANTILE, TDIGEST.CDF, TDIGEST.RANK, TDIGEST.REVRANK, TDIGEST.BYRANK, TDIGEST.BYREVRANK, TDIGEST.TRIMMED_MEAN, TDIGEST.MIN, TDIGEST.MAX, TDIGEST.INFO |
| Top-K | Yes | TOPK.RESERVE, TOPK.ADD, TOPK.INCRBY, TOPK.QUERY, TOPK.COUNT, TOPK.LIST, TOPK.INFO |
| Time series | Yes | TS.CREATE, TS.ADD, TS.INCRBY, TS.DECRBY, TS.GET, TS.RANGE, TS.REVRANGE, TS.MRANGE, TS.MREVRANGE, TS.DEL, TS.INFO |
| Consumer groups | Yes | XGROUP CREATE/DESTROY/SETID/DELCONSUMER, XREADGROUP, XACK, XPENDING, XCLAIM |
| Pub/Sub | Yes | PUBLISH, SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE |
| Transactions | Yes | MULTI, EXEC, DISCARD, WATCH, UNWATCH with optimistic locking |
| JSON | Yes | JSON.SET, JSON.GET, JSON.DEL, JSON.TYPE, JSON.STRLEN, JSON.ARRLEN, JSON.ARRAPPEND, JSON.ARRINDEX, JSON.ARRINSERT, JSON.ARRREM, JSON.ARRTRIM, JSON.MGET |
| Vectors | Yes | VSET, VGET, VDIM, VDEL, VSIM, VSEARCH |
Dredis includes handshake compatibility for common .NET Redis clients, including StackExchange.Redis and NRedis-family clients (NRedisStack).
- Handshake/probe commands supported:
CLIENT(SETNAME,SETINFO,ID,GETNAME),COMMAND,CONFIG GET,INFO,SELECT,READONLY,READWRITE. - Inline command parsing is supported in addition to array-style RESP command frames.
- Missing tie-break key (
__Booksleeve_TieBreak) is handled with a valid bulk-string response shape expected by StackExchange.Redis. - End-to-end compatibility is validated by
Dredis.Tests/DredisNRedisCompatibilityTests.cs.
Dredis is built on a modular architecture with the following components:
- DredisServer: Network server using DotNetty for high-performance async I/O
- DredisCommandHandler: RESP command processor with support for Redis commands and transactions
- IKeyValueStore: Storage abstraction interface for implementing custom storage backends
- JSON Support: Dedicated JSON command handlers in
DredisCommandHandler.Json.cssupporting JSONPath operations
The JSON implementation uses System.Text.Json for parsing and manipulation, and supports JSONPath syntax for navigating JSON documents. All JSON commands support both single and multi-path operations where applicable.
Dredis.Abstractions.Auth provides a standalone contract package for authentication and authorization integration.
IAuthenticationProvidervalidates incoming credentials and returns anAuthenticatedIdentitywhen successful.IAuthorizationProviderevaluates whether an authenticated identity can perform a requested action/resource operation.AuthenticationRequestandAuthorizationRequestdefine the request payloads for provider implementations.AuthorizationDecisionprovides a simpleAllow/Denydecision model.
This abstraction is intentionally implementation-agnostic so hosts can plug in custom identity providers, API key validation, JWT processing, role/claim checks, or policy engines without coupling to server internals.
Example skeleton:
using Dredis.Abstractions.Auth;
public sealed class MyAuthenticationProvider : IAuthenticationProvider
{
public Task<AuthenticatedIdentity?> AuthenticateAsync(AuthenticationRequest request, CancellationToken cancellationToken = default)
{
if (request.Username == "admin" && request.Password == "secret")
{
var identity = new AuthenticatedIdentity(
SubjectId: "user:admin",
DisplayName: "Administrator",
Claims: new Dictionary<string, string> { ["role"] = "admin" });
return Task.FromResult<AuthenticatedIdentity?>(identity);
}
return Task.FromResult<AuthenticatedIdentity?>(null);
}
}
public sealed class MyAuthorizationProvider : IAuthorizationProvider
{
public Task<AuthorizationDecision> AuthorizeAsync(AuthorizationRequest request, CancellationToken cancellationToken = default)
{
var isAdmin = request.Identity.Claims?.TryGetValue("role", out var role) == true && role == "admin";
return Task.FromResult(isAdmin ? AuthorizationDecision.Allow : AuthorizationDecision.Deny);
}
}You can register custom RESP commands on DredisServer before calling StartAsync or RunAsync. Registered commands are automatically applied to each connection's DredisCommandHandler.
using Dredis;
using Dredis.Abstractions.Command;
using Dredis.Abstractions.Storage;
public sealed class HelloCommand : ICommand
{
public string Name => "HELLO";
public Task<string> ExecuteAsync(params string[] parameters)
{
var payload = parameters.Length == 0 ? "world" : string.Join(',', parameters);
return Task.FromResult($"hello:{payload}");
}
}
var store = new MyKeyValueStore(); // your IKeyValueStore implementation
var server = new DredisServer(store);
server.Register(new HelloCommand());
await server.RunAsync(6379);From a Redis client, this command can be called as HELLO or HELLO one two, and returns a bulk-string reply.
DredisServer now supports an options model (DredisServerOptions) and configuration binding via Microsoft.Extensions.Configuration.
BindAddress(default:127.0.0.1)Port(default:6379)BossGroupThreadCount(default:1)WorkerGroupThreadCount(optional; when omitted, DotNetty default sizing is used)
Validation note:
- Invalid values fail fast during options binding/server construction.
BindAddressmust be a valid IP address.Portmust be between1and65535.BossGroupThreadCountandWorkerGroupThreadCount(when provided) must be greater than0.
{
"DredisServer": {
"BindAddress": "127.0.0.1",
"Port": 6379,
"BossGroupThreadCount": 1,
"WorkerGroupThreadCount": 4
}
}using Dredis;
using Dredis.Abstractions.Storage;
using Microsoft.Extensions.Configuration;
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.Build();
var store = new MyKeyValueStore();
var server = new DredisServer(store, configuration);
await server.RunAsync(); // Uses DredisServer section valuesYou can still override just the port at call-time:
await server.RunAsync(6380);Use ICommand when:
- You want to add application-level custom commands with simple string in/out behavior.
- You prefer explicit registration on
DredisServerviaserver.Register(...). - You do not need to extend your storage abstraction.
Use ICustomDataTypeStore when:
- You are implementing storage-backed custom data type commands.
- You want unknown commands to flow through the storage layer without changing
IKeyValueStore. - You need richer RESP response shapes (integer, error, bulk, null bulk, array).
Compatibility note:
- Existing
IKeyValueStoreimplementations remain fully compatible with no code changes. ICustomDataTypeStoreis optional and only needed when you want unknown-command flow-through for custom data types.- If
ICustomDataTypeStoreis not implemented, unknown commands continue through registeredICommandhandlers and then standard unknown-command behavior.
Example skeleton:
using Dredis.Abstractions.Storage;
public sealed class MyStore : IKeyValueStore, ICustomDataTypeStore
{
public Task<CustomDataTypeResult> TryExecuteCustomDataTypeAsync(string command, string[] args, CancellationToken token = default)
{
if (string.Equals(command, "CTYPE.ECHO", StringComparison.OrdinalIgnoreCase))
{
var payload = args.Length == 0 ? string.Empty : string.Join(',', args);
return Task.FromResult(CustomDataTypeResult.BulkString(System.Text.Encoding.UTF8.GetBytes($"ctype:{payload}")));
}
return Task.FromResult(CustomDataTypeResult.NotHandled);
}
// Implement IKeyValueStore members...
}Dispatch order for unknown commands is:
ICustomDataTypeStore(if implemented by the active store)- Registered
ICommandhandlers - Standard unknown-command error
- Added optional
ICustomDataTypeStoreextension to support storage-backed custom data type commands without introducing breaking changes toIKeyValueStore. - Unknown command resolution now flows through
ICustomDataTypeStore(when implemented), then registeredICommandhandlers, then standard unknown-command behavior.
- Redis® is a registered trademark of Redis Ltd.; all rights in the Redis name and related marks are reserved to Redis Ltd.
- Copyright in Redis software, source code, and official documentation remains with their respective owners.
- Dredis is an independent community project and is not affiliated with, endorsed by, or sponsored by Redis Ltd.
- For current trademark usage requirements, see the official policy: https://redis.io/legal/trademark-guidelines/.
- Additional Redis commands as needed