public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) { RateLimitCacheEntry cacheRec = null; string path = context.ActionDescriptor.ViewEnginePath; string method = context.HttpContext.Request.Method; // string method = context.HandlerMethod.HttpMethod; string key = RateLimitRule.MakeRuleKey(path, method); if (rateLimits.RateLimitRuleMatches(key)) { logger?.LogDebug($"Limit rule for page: {path} and method: {method} matched"); string filterKey = rateLimits.FilterKey.BuildKey(context); string filterKeyName = rateLimits.FilterKey.GetFilterKeyName(); if (rateLimits.IsWhitelisted(filterKey)) { logger?.LogDebug($"Rate ignored for whitelisted {filterKeyName}: {filterKey}"); } else { var rule = rateLimits.GetRateLimitByKey(key); var ruleLimits = rule.RequestLimits; var cacheKeys = GetTimeUnitCacheKeys(path, method, filterKey, filterKeyName).ToArray(); DateTime now = DateTime.UtcNow; // limit for this page and method is in effect, check cache entries for each time unit (minute, hour, day) for (int z = 0; z < 3; z++) { if (ruleLimits[z] == 0) { // zero means don't do limiting for this time unit logger?.LogDebug($"Rate-limiting disabled for per-{rule.GetTimeUnitName(z)}"); } else { bool exists = memoryCache.TryGetValue <RateLimitCacheEntry>(cacheKeys[z], out cacheRec); if (exists) { if (cacheRec.IsExpired(now)) { // there's no guarantee the system will auto-expire entries on time, so we have to manually check them logger?.LogDebug($"Manual delete of expired cache key for {rule.GetRuleDesc(z)} for {filterKeyName} {filterKey}"); memoryCache.Remove(cacheKeys[z]); // dump the old cache entry // create a new cache entry cacheRec = new RateLimitCacheEntry(spans[z]); CreateOrUpdateCacheEntry(cacheKeys[z], cacheRec, spans[z]); } else { TimeSpan timeRemaining = cacheRec.CalcRemainingTime(now); int totSecs = Convert.ToInt32(timeRemaining.TotalSeconds) + 1; string timeRemainingStr = cacheRec.BuildTimeCountdownStringFromSpan(timeRemaining); if (cacheRec.ReqCnt + 1 > ruleLimits[z]) { // cache limit has been exceeded - return 429 logger?.LogDebug($"Rule limit ({cacheRec.ReqCnt}) exceeded for {rule.GetRuleDesc(z)} for Filter: {filterKeyName}, Key: {filterKey}"); context.HttpContext.Response.Headers.Add("Retry-After", totSecs.ToString()); context.Result = new ContentResult { // StatusCode = (int) HttpStatusCode.TooManyRequests, StatusCode = 429, ContentType = "text/html", Content = $"Request quota ({rule.GetRuleDesc(z)}) exceeded. Try again in {timeRemainingStr}." }; return; } else { logger?.LogDebug($"Time remaining on cache entry per {rule.GetTimeUnitName(z)}: {timeRemainingStr} for Filter: {filterKeyName}, Key: {filterKey}"); logger?.LogDebug($"Adding 1 to count ({cacheRec.ReqCnt}) per {rule.GetTimeUnitName(z)} for Filter: {filterKeyName}, Key: {filterKey}"); cacheRec.ReqCnt += 1; // pass in new time left based on original starting time and span CreateOrUpdateCacheEntry(cacheKeys[z], cacheRec, cacheRec.CalcRemainingTime(now)); } } } else { // doesn't exist in cache, so we'll create it fresh cacheRec = new RateLimitCacheEntry(spans[z]); CreateOrUpdateCacheEntry(cacheKeys[z], cacheRec, spans[z]); } } } } } else { logger?.LogDebug($"No limit rule for page: {path} and method: {method}"); } await next.Invoke(); }