diff --git a/src/AspNetCoreRateLimit/AspNetCoreRateLimit.csproj b/src/AspNetCoreRateLimit/AspNetCoreRateLimit.csproj
index e50e8de8..580a5d12 100644
--- a/src/AspNetCoreRateLimit/AspNetCoreRateLimit.csproj
+++ b/src/AspNetCoreRateLimit/AspNetCoreRateLimit.csproj
@@ -11,7 +11,7 @@
http://opensource.org/licenses/MIT
git
https://github.com/stefanprodan/AspNetCoreRateLimit
- 8
+ 9
3.2.3
true
../../sgKey.snk
@@ -27,13 +27,15 @@
+
-
+
-
-
-
-
+
+
+
+
+
@@ -41,6 +43,7 @@
+
@@ -52,11 +55,11 @@
-
+
true
-
+
diff --git a/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs b/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs
index f09472d7..61f4bc43 100644
--- a/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs
+++ b/src/AspNetCoreRateLimit/Core/ClientRateLimitProcessor.cs
@@ -7,17 +7,20 @@ namespace AspNetCoreRateLimit
public class ClientRateLimitProcessor : RateLimitProcessor, IRateLimitProcessor
{
private readonly ClientRateLimitOptions _options;
+ private readonly IProcessingStrategy _processingStrategy;
private readonly IRateLimitStore _policyStore;
public ClientRateLimitProcessor(
- ClientRateLimitOptions options,
- IRateLimitCounterStore counterStore,
- IClientPolicyStore policyStore,
- IRateLimitConfiguration config)
- : base(options, counterStore, new ClientCounterKeyBuilder(options), config)
+ IProcessingStrategyFactory processingStrategyFactory,
+ ClientRateLimitOptions options,
+ IRateLimitCounterStore counterStore,
+ IClientPolicyStore policyStore,
+ IRateLimitConfiguration config)
+ : base(options)
{
_options = options;
_policyStore = policyStore;
+ _processingStrategy = processingStrategyFactory.CreateProcessingStrategy(counterStore, new ClientCounterKeyBuilder(options), config, options);
}
public async Task> GetMatchingRulesAsync(ClientRequestIdentity identity, CancellationToken cancellationToken = default)
@@ -26,5 +29,10 @@ public async Task> GetMatchingRulesAsync(ClientReques
return GetMatchingRules(identity, policy?.Rules);
}
+
+ public async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default)
+ {
+ return await _processingStrategy.ProcessRequestAsync(requestIdentity, rule, cancellationToken);
+ }
}
}
\ No newline at end of file
diff --git a/src/AspNetCoreRateLimit/Core/IpRateLimitProcessor.cs b/src/AspNetCoreRateLimit/Core/IpRateLimitProcessor.cs
index a62258ec..9abbfc95 100644
--- a/src/AspNetCoreRateLimit/Core/IpRateLimitProcessor.cs
+++ b/src/AspNetCoreRateLimit/Core/IpRateLimitProcessor.cs
@@ -9,18 +9,22 @@ public class IpRateLimitProcessor : RateLimitProcessor, IRateLimitProcessor
{
private readonly IpRateLimitOptions _options;
private readonly IRateLimitStore _policyStore;
+ private readonly IProcessingStrategy _processingStrategy;
public IpRateLimitProcessor(
- IpRateLimitOptions options,
- IRateLimitCounterStore counterStore,
- IIpPolicyStore policyStore,
- IRateLimitConfiguration config)
- : base(options, counterStore, new IpCounterKeyBuilder(options), config)
+ IProcessingStrategyFactory processingStrategyFactory,
+ IpRateLimitOptions options,
+ IRateLimitCounterStore counterStore,
+ IIpPolicyStore policyStore,
+ IRateLimitConfiguration config)
+ : base(options)
{
_options = options;
_policyStore = policyStore;
+ _processingStrategy = processingStrategyFactory.CreateProcessingStrategy(counterStore, new IpCounterKeyBuilder(options), config, options);
}
+
public async Task> GetMatchingRulesAsync(ClientRequestIdentity identity, CancellationToken cancellationToken = default)
{
var policies = await _policyStore.GetAsync($"{_options.IpPolicyPrefix}", cancellationToken);
@@ -40,5 +44,10 @@ public async Task> GetMatchingRulesAsync(ClientReques
return GetMatchingRules(identity, rules);
}
+
+ public async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default)
+ {
+ return await _processingStrategy.ProcessRequestAsync(requestIdentity, rule, cancellationToken);
+ }
}
}
\ No newline at end of file
diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/AsyncKeyLockProcessingStrategy.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/AsyncKeyLockProcessingStrategy.cs
new file mode 100644
index 00000000..9cc31465
--- /dev/null
+++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/AsyncKeyLockProcessingStrategy.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AspNetCoreRateLimit
+{
+ public class AsyncKeyLockProcessingStrategy : ProcessingStrategy
+ {
+ private readonly RateLimitOptions _options;
+ private readonly IRateLimitCounterStore _counterStore;
+ private readonly ICounterKeyBuilder _counterKeyBuilder;
+ private readonly IRateLimitConfiguration _config;
+
+ public AsyncKeyLockProcessingStrategy(IRateLimitCounterStore counterStore, ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options)
+ : base(counterKeyBuilder, config, options)
+ {
+ _counterStore = counterStore;
+ _counterKeyBuilder = counterKeyBuilder;
+ _config = config;
+ _options = options;
+ }
+
+
+ /// The key-lock used for limiting requests.
+ private static readonly AsyncKeyLock AsyncLock = new AsyncKeyLock();
+
+ public override async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default)
+ {
+ var counter = new RateLimitCounter
+ {
+ Timestamp = DateTime.UtcNow,
+ Count = 1
+ };
+
+ var counterId = BuildCounterKey(requestIdentity, rule);
+
+ // serial reads and writes on same key
+ using (await AsyncLock.WriterLockAsync(counterId).ConfigureAwait(false))
+ {
+ var entry = await _counterStore.GetAsync(counterId, cancellationToken);
+
+ if (entry.HasValue)
+ {
+ // entry has not expired
+ if (entry.Value.Timestamp + rule.PeriodTimespan.Value >= DateTime.UtcNow)
+ {
+ // increment request count
+ var totalCount = entry.Value.Count + _config.RateIncrementer?.Invoke() ?? 1;
+
+ // deep copy
+ counter = new RateLimitCounter
+ {
+ Timestamp = entry.Value.Timestamp,
+ Count = totalCount
+ };
+ }
+ }
+
+ // stores: id (string) - timestamp (datetime) - total_requests (long)
+ await _counterStore.SetAsync(counterId, counter, rule.PeriodTimespan.Value, cancellationToken);
+ }
+
+ return counter;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategy.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategy.cs
new file mode 100644
index 00000000..e15b3b2f
--- /dev/null
+++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategy.cs
@@ -0,0 +1,10 @@
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AspNetCoreRateLimit
+{
+ public interface IProcessingStrategy
+ {
+ Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default);
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategyFactory.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategyFactory.cs
new file mode 100644
index 00000000..75c506a9
--- /dev/null
+++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/IProcessingStrategyFactory.cs
@@ -0,0 +1,7 @@
+namespace AspNetCoreRateLimit
+{
+ public interface IProcessingStrategyFactory
+ {
+ ProcessingStrategy CreateProcessingStrategy(IRateLimitCounterStore counterStore, ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options);
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategy.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategy.cs
new file mode 100644
index 00000000..2cd32efe
--- /dev/null
+++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategy.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AspNetCoreRateLimit
+{
+ public abstract class ProcessingStrategy : IProcessingStrategy
+ {
+ private readonly RateLimitOptions _options;
+ private readonly ICounterKeyBuilder _counterKeyBuilder;
+ private readonly IRateLimitConfiguration _config;
+
+ public ProcessingStrategy(ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options)
+ : base()
+ {
+ _counterKeyBuilder = counterKeyBuilder;
+ _config = config;
+ _options = options;
+ }
+
+ protected virtual string BuildCounterKey(ClientRequestIdentity requestIdentity, RateLimitRule rule)
+ {
+ var key = _counterKeyBuilder.Build(requestIdentity, rule);
+
+ if (_options.EnableEndpointRateLimiting && _config.EndpointCounterKeyBuilder != null)
+ {
+ key += _config.EndpointCounterKeyBuilder.Build(requestIdentity, rule);
+ }
+
+ var bytes = Encoding.UTF8.GetBytes(key);
+
+ using var algorithm = new SHA1Managed();
+ var hash = algorithm.ComputeHash(bytes);
+
+ return Convert.ToBase64String(hash);
+ }
+
+ public abstract Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default);
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategyFactory.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategyFactory.cs
new file mode 100644
index 00000000..a8b7952d
--- /dev/null
+++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/ProcessingStrategyFactory.cs
@@ -0,0 +1,27 @@
+using System;
+using StackExchange.Redis;
+
+namespace AspNetCoreRateLimit
+{
+
+ public class ProcessingStrategyFactory : IProcessingStrategyFactory
+ {
+ private readonly IConnectionMultiplexer _connectionMultiplexer;
+
+ public ProcessingStrategyFactory(IConnectionMultiplexer connectionMultiplexer = null)
+ {
+ _connectionMultiplexer = connectionMultiplexer;
+ }
+
+ public ProcessingStrategy CreateProcessingStrategy(IRateLimitCounterStore counterStore, ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options)
+ {
+ return counterStore switch
+ {
+ MemoryCacheRateLimitCounterStore => new AsyncKeyLockProcessingStrategy(counterStore, counterKeyBuilder, config, options),
+ DistributedCacheRateLimitCounterStore => new AsyncKeyLockProcessingStrategy(counterStore, counterKeyBuilder, config, options),
+ StackExchangeRedisRateLimitCounterStore => new StackExchangeRedisProcessingStrategy(_connectionMultiplexer, counterStore, counterKeyBuilder, config, options),
+ _ => throw new ArgumentException("Unsupported instance of IRateLimitCounterStore provided")
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCoreRateLimit/Core/ProcessingStrategies/StackExchangeRedisProcessingStrategy.cs b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/StackExchangeRedisProcessingStrategy.cs
new file mode 100644
index 00000000..ecb73215
--- /dev/null
+++ b/src/AspNetCoreRateLimit/Core/ProcessingStrategies/StackExchangeRedisProcessingStrategy.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using StackExchange.Redis;
+
+namespace AspNetCoreRateLimit
+{
+ public class StackExchangeRedisProcessingStrategy : ProcessingStrategy
+ {
+ private readonly IConnectionMultiplexer _connectionMultiplexer;
+ private readonly IRateLimitConfiguration _config;
+
+ public StackExchangeRedisProcessingStrategy(IConnectionMultiplexer connectionMultiplexer, IRateLimitCounterStore counterStore, ICounterKeyBuilder counterKeyBuilder, IRateLimitConfiguration config, RateLimitOptions options)
+ : base(counterKeyBuilder, config, options)
+ {
+ _connectionMultiplexer = connectionMultiplexer ?? throw new ArgumentException("IConnectionMultiplexer was null. Ensure StackExchange.Redis was successfully registered");
+ _config = config;
+ }
+
+
+ static private readonly LuaScript _atomicIncrement = LuaScript.Prepare("local count count = redis.call(\"INCRBYFLOAT\", @key, tonumber(@delta)) if tonumber(count) == @delta then redis.call(\"EXPIRE\", @key, @timeout) end return count");
+
+ public override async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default)
+ {
+ var counterId = BuildCounterKey(requestIdentity, rule);
+ return await IncrementAsync(counterId, rule.PeriodTimespan.Value, _config.RateIncrementer);
+ }
+
+ public async Task IncrementAsync(string counterId, TimeSpan interval, Func RateIncrementer = null)
+ {
+ var now = DateTime.UtcNow;
+ var numberOfIntervals = now.Ticks / interval.Ticks;
+ var intervalStart = new DateTime(numberOfIntervals * interval.Ticks, DateTimeKind.Utc);
+
+ // Call the Lua script
+ var count = await _connectionMultiplexer.GetDatabase().ScriptEvaluateAsync(_atomicIncrement, new { key = counterId, timeout = interval.TotalSeconds, delta = RateIncrementer?.Invoke() ?? 1D });
+ return new RateLimitCounter
+ {
+ Count = (double)count,
+ Timestamp = intervalStart
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCoreRateLimit/Core/RateLimitProcessor.cs b/src/AspNetCoreRateLimit/Core/RateLimitProcessor.cs
index d22fe5d5..9ce4ff1d 100644
--- a/src/AspNetCoreRateLimit/Core/RateLimitProcessor.cs
+++ b/src/AspNetCoreRateLimit/Core/RateLimitProcessor.cs
@@ -12,24 +12,12 @@ namespace AspNetCoreRateLimit
public abstract class RateLimitProcessor
{
private readonly RateLimitOptions _options;
- private readonly IRateLimitCounterStore _counterStore;
- private readonly ICounterKeyBuilder _counterKeyBuilder;
- private readonly IRateLimitConfiguration _config;
-
- protected RateLimitProcessor(
- RateLimitOptions options,
- IRateLimitCounterStore counterStore,
- ICounterKeyBuilder counterKeyBuilder,
- IRateLimitConfiguration config)
+
+ protected RateLimitProcessor(RateLimitOptions options)
{
_options = options;
- _counterStore = counterStore;
- _counterKeyBuilder = counterKeyBuilder;
- _config = config;
}
- /// The key-lock used for limiting requests.
- private static readonly AsyncKeyLock AsyncLock = new AsyncKeyLock();
public virtual bool IsWhitelisted(ClientRequestIdentity requestIdentity)
{
@@ -55,45 +43,6 @@ public virtual bool IsWhitelisted(ClientRequestIdentity requestIdentity)
return false;
}
- public virtual async Task ProcessRequestAsync(ClientRequestIdentity requestIdentity, RateLimitRule rule, CancellationToken cancellationToken = default)
- {
- var counter = new RateLimitCounter
- {
- Timestamp = DateTime.UtcNow,
- Count = 1
- };
-
- var counterId = BuildCounterKey(requestIdentity, rule);
-
- // serial reads and writes on same key
- using (await AsyncLock.WriterLockAsync(counterId).ConfigureAwait(false))
- {
- var entry = await _counterStore.GetAsync(counterId, cancellationToken);
-
- if (entry.HasValue)
- {
- // entry has not expired
- if (entry.Value.Timestamp + rule.PeriodTimespan.Value >= DateTime.UtcNow)
- {
- // increment request count
- var totalCount = entry.Value.Count + _config.RateIncrementer?.Invoke() ?? 1;
-
- // deep copy
- counter = new RateLimitCounter
- {
- Timestamp = entry.Value.Timestamp,
- Count = totalCount
- };
- }
- }
-
- // stores: id (string) - timestamp (datetime) - total_requests (long)
- await _counterStore.SetAsync(counterId, counter, rule.PeriodTimespan.Value, cancellationToken);
- }
-
- return counter;
- }
-
public virtual RateLimitHeaders GetRateLimitHeaders(RateLimitCounter? counter, RateLimitRule rule, CancellationToken cancellationToken = default)
{
var headers = new RateLimitHeaders();
@@ -119,23 +68,6 @@ public virtual RateLimitHeaders GetRateLimitHeaders(RateLimitCounter? counter, R
return headers;
}
- protected virtual string BuildCounterKey(ClientRequestIdentity requestIdentity, RateLimitRule rule)
- {
- var key = _counterKeyBuilder.Build(requestIdentity, rule);
-
- if (_options.EnableEndpointRateLimiting && _config.EndpointCounterKeyBuilder != null)
- {
- key += _config.EndpointCounterKeyBuilder.Build(requestIdentity, rule);
- }
-
- var bytes = Encoding.UTF8.GetBytes(key);
-
- using var algorithm = new SHA1Managed();
- var hash = algorithm.ComputeHash(bytes);
-
- return Convert.ToBase64String(hash);
- }
-
protected virtual List GetMatchingRules(ClientRequestIdentity identity, List rules = null)
{
var limits = new List();
diff --git a/src/AspNetCoreRateLimit/Middleware/ClientRateLimitMiddleware.cs b/src/AspNetCoreRateLimit/Middleware/ClientRateLimitMiddleware.cs
index d8c1050d..f11781c9 100644
--- a/src/AspNetCoreRateLimit/Middleware/ClientRateLimitMiddleware.cs
+++ b/src/AspNetCoreRateLimit/Middleware/ClientRateLimitMiddleware.cs
@@ -10,11 +10,12 @@ public class ClientRateLimitMiddleware : RateLimitMiddleware options,
+ IProcessingStrategyFactory processingStrategyFactory,
IRateLimitCounterStore counterStore,
IClientPolicyStore policyStore,
IRateLimitConfiguration config,
ILogger logger)
- : base(next, options?.Value, new ClientRateLimitProcessor(options?.Value, counterStore, policyStore, config), config)
+ : base(next, options?.Value, new ClientRateLimitProcessor(processingStrategyFactory, options?.Value, counterStore, policyStore, config), config)
{
_logger = logger;
}
diff --git a/src/AspNetCoreRateLimit/Middleware/IpRateLimitMiddleware.cs b/src/AspNetCoreRateLimit/Middleware/IpRateLimitMiddleware.cs
index 4eeced18..7b6a338a 100644
--- a/src/AspNetCoreRateLimit/Middleware/IpRateLimitMiddleware.cs
+++ b/src/AspNetCoreRateLimit/Middleware/IpRateLimitMiddleware.cs
@@ -9,12 +9,13 @@ public class IpRateLimitMiddleware : RateLimitMiddleware
private readonly ILogger _logger;
public IpRateLimitMiddleware(RequestDelegate next,
+ IProcessingStrategyFactory processingStrategyFactory,
IOptions options,
IRateLimitCounterStore counterStore,
IIpPolicyStore policyStore,
IRateLimitConfiguration config,
ILogger logger)
- : base(next, options?.Value, new IpRateLimitProcessor(options?.Value, counterStore, policyStore, config), config)
+ : base(next, options?.Value, new IpRateLimitProcessor(processingStrategyFactory, options?.Value, counterStore, policyStore, config), config)
{
_logger = logger;
diff --git a/src/AspNetCoreRateLimit/Middleware/MiddlewareExtensions.cs b/src/AspNetCoreRateLimit/Middleware/MiddlewareExtensions.cs
deleted file mode 100644
index 63a1300c..00000000
--- a/src/AspNetCoreRateLimit/Middleware/MiddlewareExtensions.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Microsoft.AspNetCore.Builder;
-
-namespace AspNetCoreRateLimit
-{
- public static class MiddlewareExtensions
- {
- public static IApplicationBuilder UseIpRateLimiting(this IApplicationBuilder builder)
- {
- return builder.UseMiddleware();
- }
-
- public static IApplicationBuilder UseClientRateLimiting(this IApplicationBuilder builder)
- {
- return builder.UseMiddleware();
- }
- }
-}
\ No newline at end of file
diff --git a/src/AspNetCoreRateLimit/StartupExtensions.cs b/src/AspNetCoreRateLimit/StartupExtensions.cs
new file mode 100644
index 00000000..1cc9615c
--- /dev/null
+++ b/src/AspNetCoreRateLimit/StartupExtensions.cs
@@ -0,0 +1,44 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace AspNetCoreRateLimit
+{
+ public static class StartupExtensions
+ {
+ public static IServiceCollection AddMemoryCacheStores(this IServiceCollection services)
+ {
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ return services;
+ }
+
+ public static IServiceCollection AddDistributedCacheStores(this IServiceCollection services)
+ {
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ return services;
+ }
+
+ public static IServiceCollection AddStackExchangeRedisStores(this IServiceCollection services)
+ {
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ return services;
+ }
+
+ public static IApplicationBuilder UseIpRateLimiting(this IApplicationBuilder builder)
+ {
+ return builder.UseMiddleware();
+ }
+
+ public static IApplicationBuilder UseClientRateLimiting(this IApplicationBuilder builder)
+ {
+ return builder.UseMiddleware();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCoreRateLimit/Store/DistributedCacheClientPolicyStore.cs b/src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheClientPolicyStore.cs
similarity index 100%
rename from src/AspNetCoreRateLimit/Store/DistributedCacheClientPolicyStore.cs
rename to src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheClientPolicyStore.cs
diff --git a/src/AspNetCoreRateLimit/Store/DistributedCacheIpPolicyStore.cs b/src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheIpPolicyStore.cs
similarity index 100%
rename from src/AspNetCoreRateLimit/Store/DistributedCacheIpPolicyStore.cs
rename to src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheIpPolicyStore.cs
diff --git a/src/AspNetCoreRateLimit/Store/DistributedCacheRateLimitCounterStore.cs b/src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheRateLimitCounterStore.cs
similarity index 100%
rename from src/AspNetCoreRateLimit/Store/DistributedCacheRateLimitCounterStore.cs
rename to src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheRateLimitCounterStore.cs
diff --git a/src/AspNetCoreRateLimit/Store/DistributedCacheRateLimitStore.cs b/src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheRateLimitStore.cs
similarity index 100%
rename from src/AspNetCoreRateLimit/Store/DistributedCacheRateLimitStore.cs
rename to src/AspNetCoreRateLimit/Store/DistributedCache/DistributedCacheRateLimitStore.cs
diff --git a/src/AspNetCoreRateLimit/Store/MemoryCacheClientPolicyStore.cs b/src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheClientPolicyStore.cs
similarity index 100%
rename from src/AspNetCoreRateLimit/Store/MemoryCacheClientPolicyStore.cs
rename to src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheClientPolicyStore.cs
diff --git a/src/AspNetCoreRateLimit/Store/MemoryCacheIpPolicyStore.cs b/src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheIpPolicyStore.cs
similarity index 100%
rename from src/AspNetCoreRateLimit/Store/MemoryCacheIpPolicyStore.cs
rename to src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheIpPolicyStore.cs
diff --git a/src/AspNetCoreRateLimit/Store/MemoryCacheRateLimitCounterStore.cs b/src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheRateLimitCounterStore.cs
similarity index 100%
rename from src/AspNetCoreRateLimit/Store/MemoryCacheRateLimitCounterStore.cs
rename to src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheRateLimitCounterStore.cs
diff --git a/src/AspNetCoreRateLimit/Store/MemoryCacheRateLimitStore.cs b/src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheRateLimitStore.cs
similarity index 100%
rename from src/AspNetCoreRateLimit/Store/MemoryCacheRateLimitStore.cs
rename to src/AspNetCoreRateLimit/Store/MemoryCache/MemoryCacheRateLimitStore.cs
diff --git a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisClientPolicyStore.cs b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisClientPolicyStore.cs
new file mode 100644
index 00000000..992d503e
--- /dev/null
+++ b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisClientPolicyStore.cs
@@ -0,0 +1,34 @@
+using System.Threading.Tasks;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Options;
+using StackExchange.Redis;
+
+namespace AspNetCoreRateLimit
+{
+ public class StackExchangeRedisClientPolicyStore : StackExchangeRedisRateLimitStore, IClientPolicyStore
+ {
+ private readonly ClientRateLimitOptions _options;
+ private readonly ClientRateLimitPolicies _policies;
+
+ public StackExchangeRedisClientPolicyStore(
+ IConnectionMultiplexer redis,
+ IOptions options = null,
+ IOptions policies = null) : base(redis)
+ {
+ _options = options?.Value;
+ _policies = policies?.Value;
+ }
+
+ public async Task SeedAsync()
+ {
+ // on startup, save the IP rules defined in appsettings
+ if (_options != null && _policies?.ClientRules != null)
+ {
+ foreach (var rule in _policies.ClientRules)
+ {
+ await SetAsync($"{_options.ClientPolicyPrefix}_{rule.ClientId}", new ClientRateLimitPolicy { ClientId = rule.ClientId, Rules = rule.Rules }).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisIpPolicyStore.cs b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisIpPolicyStore.cs
new file mode 100644
index 00000000..8fa36a4d
--- /dev/null
+++ b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisIpPolicyStore.cs
@@ -0,0 +1,31 @@
+using System.Threading.Tasks;
+using Microsoft.Extensions.Caching.Distributed;
+using Microsoft.Extensions.Options;
+using StackExchange.Redis;
+
+namespace AspNetCoreRateLimit
+{
+ public class StackExchangeRedisIpPolicyStore : StackExchangeRedisRateLimitStore, IIpPolicyStore
+ {
+ private readonly IpRateLimitOptions _options;
+ private readonly IpRateLimitPolicies _policies;
+
+ public StackExchangeRedisIpPolicyStore(
+ IConnectionMultiplexer redis,
+ IOptions options = null,
+ IOptions policies = null) : base(redis)
+ {
+ _options = options?.Value;
+ _policies = policies?.Value;
+ }
+
+ public async Task SeedAsync()
+ {
+ // on startup, save the IP rules defined in appsettings
+ if (_options != null && _policies != null)
+ {
+ await SetAsync($"{_options.IpPolicyPrefix}", _policies).ConfigureAwait(false);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitCounterStore.cs b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitCounterStore.cs
new file mode 100644
index 00000000..ab32322f
--- /dev/null
+++ b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitCounterStore.cs
@@ -0,0 +1,12 @@
+using StackExchange.Redis;
+
+namespace AspNetCoreRateLimit
+{
+ public class StackExchangeRedisRateLimitCounterStore : StackExchangeRedisRateLimitStore, IRateLimitCounterStore
+ {
+ public StackExchangeRedisRateLimitCounterStore(IConnectionMultiplexer connectionMultiplexer)
+ : base(connectionMultiplexer)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitStore.cs b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitStore.cs
new file mode 100644
index 00000000..569f4a57
--- /dev/null
+++ b/src/AspNetCoreRateLimit/Store/StackExchangeRedis/StackExchangeRedisRateLimitStore.cs
@@ -0,0 +1,50 @@
+using Newtonsoft.Json;
+using StackExchange.Redis;
+using System;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace AspNetCoreRateLimit
+{
+ public class StackExchangeRedisRateLimitStore : IRateLimitStore
+ {
+ private readonly IConnectionMultiplexer _redis;
+
+ public StackExchangeRedisRateLimitStore(IConnectionMultiplexer redis)
+ {
+ _redis = redis ?? throw new ArgumentNullException(nameof(redis));
+ }
+
+ public async Task SetAsync(string id, T entry, TimeSpan? expirationTime = null, CancellationToken cancellationToken = default)
+ {
+ // Throw an exception if the key could not be set
+ if (!await _redis.GetDatabase().StringSetAsync(id, JsonConvert.SerializeObject(entry), expirationTime))
+ {
+ throw new ExternalException($"Failed to set key {id}");
+ }
+ }
+
+ public Task ExistsAsync(string id, CancellationToken cancellationToken = default)
+ {
+ return _redis.GetDatabase().KeyExistsAsync(id);
+ }
+
+ public async Task GetAsync(string id, CancellationToken cancellationToken = default)
+ {
+ var stored = await _redis.GetDatabase().StringGetAsync(id);
+ if (stored.HasValue)
+ {
+ return JsonConvert.DeserializeObject(stored.ToString());
+ }
+
+ return default;
+ }
+
+ public Task RemoveAsync(string id, CancellationToken cancellationToken = default)
+ {
+ // Don't throw an exception if the key doesn't exist
+ return _redis.GetDatabase().KeyDeleteAsync(id);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/AspNetCoreRateLimit.Demo/Startup.cs b/test/AspNetCoreRateLimit.Demo/Startup.cs
index 77635376..a5359140 100644
--- a/test/AspNetCoreRateLimit.Demo/Startup.cs
+++ b/test/AspNetCoreRateLimit.Demo/Startup.cs
@@ -26,19 +26,22 @@ public void ConfigureServices(IServiceCollection services)
// needed to store rate limit counters and ip rules
services.AddMemoryCache();
+ // configure ip rate limiting middleware - version 4.0
+
+ // configure client rate limiting middleware - version 4.0
+
// configure ip rate limiting middleware
services.Configure(Configuration.GetSection("IpRateLimiting"));
services.Configure(Configuration.GetSection("IpRateLimitPolicies"));
- services.AddSingleton();
- services.AddSingleton();
// configure client rate limiting middleware
services.Configure(Configuration.GetSection("ClientRateLimiting"));
services.Configure(Configuration.GetSection("ClientRateLimitPolicies"));
- services.AddSingleton();
- //services.AddSingleton();
- services.AddMvc((options) =>
+ // register stores
+ services.AddMemoryCacheStores();
+
+ services.AddMvc((options) =>
{
options.EnableEndpointRouting = false;