Wiaoj.DistributedCounter.Redis 0.2.0-alpha.1

This is a prerelease version of Wiaoj.DistributedCounter.Redis.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Wiaoj.DistributedCounter.Redis --version 0.2.0-alpha.1
                    
NuGet\Install-Package Wiaoj.DistributedCounter.Redis -Version 0.2.0-alpha.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Wiaoj.DistributedCounter.Redis" Version="0.2.0-alpha.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Wiaoj.DistributedCounter.Redis" Version="0.2.0-alpha.1" />
                    
Directory.Packages.props
<PackageReference Include="Wiaoj.DistributedCounter.Redis" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Wiaoj.DistributedCounter.Redis --version 0.2.0-alpha.1
                    
#r "nuget: Wiaoj.DistributedCounter.Redis, 0.2.0-alpha.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Wiaoj.DistributedCounter.Redis@0.2.0-alpha.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Wiaoj.DistributedCounter.Redis&version=0.2.0-alpha.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Wiaoj.DistributedCounter.Redis&version=0.2.0-alpha.1&prerelease
                    
Install as a Cake Tool

Wiaoj.DistributedCounter.Redis

Redis storage provider implementation for the Wiaoj.DistributedCounter library, backed by StackExchange.Redis and server-side Lua scripts.

This package implements the ICounterStorage contract for distributed Redis environments, providing atomic limit checks, sliding window TTL handling, and pipelined batch operations across standalone, cluster, and sentinel deployments.


Installation

dotnet add package Wiaoj.DistributedCounter.Redis

What This Package Contains

  • RedisCounterStorage: Concrete implementation of ICounterStorage delegating atomic operations to Redis via IDatabase.
  • Atomic Lua Scripts: Server-side scripts executed during limit evaluations and increments with expiration to eliminate race conditions without distributed locks:
    • IncrementWithExpire: Performs atomic INCRBY and sets key expiration (PEXPIRE) when a TTL is provided.
    • IncrementIfLessThan: Checks current + amount <= limit before applying INCRBY, returning {isAllowed, currentValue, pttl} in a single execution.
    • DecrementIfGreaterThan: Checks current - amount >= minLimit before applying DECRBY, returning remaining threshold capacity.
  • Fluent DI Builder Extensions: Extension methods on IDistributedCounterBuilder and CounterConfiguration for configuring Redis backends.

Dependency Injection Setup

1. Global Default Configuration

Configure Redis as the global storage backend for all distributed counters:

Using a Connection String
using Wiaoj.DistributedCounter;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDistributedCounter(counter => {
    counter.UseRedis("localhost:6379,abortConnect=false");
    counter.AddAutoFlush();

    counter.AddImmediateCounter<RateLimitTag>();
    counter.AddBufferedCounter<PageViewsTag>();
});
Using ConfigurationOptions
using StackExchange.Redis;
using Wiaoj.DistributedCounter;

builder.Services.AddDistributedCounter(counter => {
    ConfigurationOptions redisOptions = new() {
        EndPoints = { "redis-node1:6379", "redis-node2:6379" },
        Password = "SecretPassword",
        ConnectTimeout = 5000,
        SyncTimeout = 3000
    };

    counter.UseRedis(redisOptions);
    counter.AddAutoFlush();
});
Using an Existing DI-Registered IConnectionMultiplexer
builder.Services.AddSingleton<IConnectionMultiplexer>(
    _ => ConnectionMultiplexer.Connect("localhost:6379"));

builder.Services.AddDistributedCounter(counter => {
    counter.UseRedis();
    counter.AddAutoFlush();
});

2. Multi-Cluster and Keyed Service Routing

Route specific counter categories to independent Redis instances or clusters using .NET Keyed Services:

using StackExchange.Redis;
using Wiaoj.DistributedCounter;

// Register multiple keyed multiplexers in DI
builder.Services.AddKeyedSingleton<IConnectionMultiplexer>(
    "security-cluster",
    (_, _) => ConnectionMultiplexer.Connect("security-redis:6379"));

builder.Services.AddKeyedSingleton<IConnectionMultiplexer>(
    "analytics-cluster",
    (_, _) => ConnectionMultiplexer.Connect("analytics-redis:6379"));

builder.Services.AddDistributedCounter(counter => {
    // Default fallback storage
    counter.UseRedisKeyed("analytics-cluster");

    // Route security quota counter specifically to security-cluster
    counter.AddImmediateCounter<AuthRateLimitTag>(cfg => {
        cfg.UseRedisKeyed("security-cluster");
    });

    // Route telemetry to analytics-cluster
    counter.AddBufferedCounter<TelemetryTag>(cfg => {
        cfg.UseRedisKeyed("analytics-cluster");
    });
});

How Atomic Limit Checks Work

When TryIncrementAsync or TryDecrementAsync is called:

  1. RedisCounterStorage evaluates the request via a parameterized Lua script (ScriptEvaluateAsync).
  2. The script checks whether the target key currently exists and parses its numeric value (treating non-existent or expired keys as 0).
  3. If the resulting value is within the limit:
    • The key is updated using INCRBY / DECRBY.
    • If the key is new and carries an expiration, PEXPIRE is applied.
    • The script returns 1 (allowed), the updated numeric value, and the current remaining PTTL.
  4. If the resulting value exceeds the limit:
    • No mutations are applied to the key.
    • The script returns 0 (denied), the unchanged current value, and the remaining PTTL.

This guarantees that limits are enforced strictly without requiring distributed locks or multiple network round-trips.


Batch Operations and Pipelining

Pipelined Batch Increments (BatchIncrementAsync)

During background flush cycles triggered by CounterAutoFlushService, pending counter updates are compiled into an IBatch pipeline:

// Dispatches multiple ScriptEvaluateAsync calls in a single network round-trip
ReadOnlyMemory<CounterUpdate> updates = new CounterUpdate[] {
    new(new CounterKey("metric:orders"), 10, CounterExpiry.Infinite),
    new(new CounterKey("metric:logins"), 2, CounterExpiry.FromMinutes(5))
};

Memory<long> results = new long[2];
await redisStorage.BatchIncrementAsync(updates, results, cancellationToken);

Multi-Key Reads (GetManyAsync)

Batch key queries convert CounterKey spans to RedisKey arrays using ArrayPool<RedisKey> and execute an MGET (StringGetAsync) call to fetch values concurrently.


When Redis Is Unavailable

While the multiplexer has no connection (IConnectionMultiplexer.IsConnected is false), every storage operation throws RedisConnectionException (ConnectionFailureType.UnableToConnect) at once. It doesn't queue the command and wait out StackExchange.Redis' backlog timeout (5 s by default).

Counters back claims, circuit state, rate limits and frequency caps. Each caller decides what an unavailable store means for it: for example ResilientRateLimiter and ResilientCircuitBreaker fail open. With the wait removed, that decision takes effect immediately instead of after 5 s on every call. Once the multiplexer reconnects, operations succeed again without any action.

  • Detection: a connection closed by the server is noticed at once. A silent network partition is only noticed once StackExchange.Redis detects it. Commands issued before then still end in its timeout.
  • Startup: use abortConnect=false so the application starts while Redis is down. Counters then fail fast until the connection is established.

License

This project is licensed under the MIT License.

Product Compatible and additional computed target framework versions.
.NET 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
0.3.0-alpha.1 50 10/5/2026
0.2.0-alpha.3 69 9/24/2026
0.2.0-alpha.2 56 9/24/2026
0.2.0-alpha.1 63 9/24/2026
0.1.0-alpha.9 165 9/21/2026
0.1.0-alpha.8 66 9/21/2026
0.1.0-alpha.7 67 9/18/2026
0.1.0-alpha.6 62 9/16/2026
0.1.0-alpha.5 64 9/16/2026
0.1.0-alpha.4 68 9/16/2026
0.1.0-alpha.3 59 9/15/2026
0.1.0-alpha.2 95 9/15/2026
0.1.0-alpha.1 62 9/14/2026
0.0.1-alpha.112-preview 58 9/13/2026
0.0.1-alpha.111-preview 55 9/13/2026
0.0.1-alpha.110-preview 68 9/12/2026
0.0.1-alpha.109-preview 68 9/11/2026
0.0.1-alpha.108-preview 84 9/8/2026
0.0.1-alpha.107-preview 80 9/8/2026
0.0.1-alpha.106-preview 79 9/8/2026
Loading failed