Wiaoj.DistributedCounter.Redis
0.1.0-alpha.7
See the version list below for details.
dotnet add package Wiaoj.DistributedCounter.Redis --version 0.1.0-alpha.7
NuGet\Install-Package Wiaoj.DistributedCounter.Redis -Version 0.1.0-alpha.7
<PackageReference Include="Wiaoj.DistributedCounter.Redis" Version="0.1.0-alpha.7" />
<PackageVersion Include="Wiaoj.DistributedCounter.Redis" Version="0.1.0-alpha.7" />
<PackageReference Include="Wiaoj.DistributedCounter.Redis" />
paket add Wiaoj.DistributedCounter.Redis --version 0.1.0-alpha.7
#r "nuget: Wiaoj.DistributedCounter.Redis, 0.1.0-alpha.7"
#:package Wiaoj.DistributedCounter.Redis@0.1.0-alpha.7
#addin nuget:?package=Wiaoj.DistributedCounter.Redis&version=0.1.0-alpha.7&prerelease
#tool nuget:?package=Wiaoj.DistributedCounter.Redis&version=0.1.0-alpha.7&prerelease
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 ofICounterStoragedelegating atomic operations to Redis viaIDatabase.- Atomic Lua Scripts: Server-side scripts executed during limit evaluations and increments with expiration to eliminate race conditions without distributed locks:
IncrementWithExpire: Performs atomicINCRBYand sets key expiration (PEXPIRE) when a TTL is provided.IncrementIfLessThan: Checkscurrent + amount <= limitbefore applyingINCRBY, returning{isAllowed, currentValue, pttl}in a single execution.DecrementIfGreaterThan: Checkscurrent - amount >= minLimitbefore applyingDECRBY, returning remaining threshold capacity.
- Fluent DI Builder Extensions: Extension methods on
IDistributedCounterBuilderandCounterConfigurationfor 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:
RedisCounterStorageevaluates the request via a parameterized Lua script (ScriptEvaluateAsync).- The script checks whether the target key currently exists and parses its numeric value (treating non-existent or expired keys as 0).
- If the resulting value is within the limit:
- The key is updated using
INCRBY/DECRBY. - If the key is new and carries an expiration,
PEXPIREis applied. - The script returns
1(allowed), the updated numeric value, and the current remainingPTTL.
- The key is updated using
- 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 remainingPTTL.
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=falseso 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 | Versions 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. |
-
net10.0
- StackExchange.Redis (>= 2.10.1)
- Wiaoj.DistributedCounter (>= 0.1.0-alpha.7)
- Wiaoj.DistributedCounter.Abstractions (>= 0.1.0-alpha.7)
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 |