Refactor dashboard queries to push filtering and aggregation into the database
DashboardReadService no longer loads full entity lists and filters in memory. All job panels (today/overdue/in-progress) now execute targeted COUNT + capped SELECT queries in SQL. AR aging buckets, powder order lines, bill totals, and active-customer counts are all aggregated at the DB level. The SuperAdmin action previously loaded every company row to compute plan distribution and alert lists; it now delegates to a new GetSuperAdminDashboardDataAsync() that uses SQL GROUP BY and projections instead. DashboardIndexData record updated to carry pre-sliced counts and capped lists so the controller only does lightweight DTO projection. DashboardPowderOrderLineData replaces the deep Job→JobItem→Coat Include chains with a single flat coat query projected in SQL. OnlineUserMiddleware switches its per-user throttle from a static ConcurrentDictionary (grows forever) to IMemoryCache with a 60-second sliding expiry. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using PowderCoating.Web.Services;
|
||||
|
||||
namespace PowderCoating.Web.Middleware;
|
||||
@@ -10,22 +11,18 @@ namespace PowderCoating.Web.Middleware;
|
||||
public class OnlineUserMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
|
||||
/// <summary>
|
||||
/// Maps each user ID to the UTC time the tracker was last updated.
|
||||
/// A <see cref="System.Collections.Concurrent.ConcurrentDictionary{TKey,TValue}"/>
|
||||
/// is used because multiple concurrent requests from the same user (e.g. polling
|
||||
/// and a page navigation) may race to update the entry. The dictionary is
|
||||
/// static so the throttle window persists across DI scope lifetimes — the
|
||||
/// middleware instance itself may be recreated but the throttle state must not.
|
||||
/// </summary>
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, DateTime> _throttle = new();
|
||||
private readonly IMemoryCache _cache;
|
||||
|
||||
/// <summary>
|
||||
/// Initialises the middleware with the next request delegate in the pipeline.
|
||||
/// </summary>
|
||||
/// <param name="next">The next middleware component.</param>
|
||||
public OnlineUserMiddleware(RequestDelegate next) => _next = next;
|
||||
/// <param name="cache">Shared app cache used for expiring per-user throttle entries.</param>
|
||||
public OnlineUserMiddleware(RequestDelegate next, IMemoryCache cache)
|
||||
{
|
||||
_next = next;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls the downstream pipeline first, then — after the response is
|
||||
@@ -63,12 +60,15 @@ public class OnlineUserMiddleware
|
||||
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (string.IsNullOrEmpty(userId)) return;
|
||||
|
||||
// Throttle: only update the tracker once per 60 seconds per user
|
||||
// Throttle: only update the tracker once per 60 seconds per user.
|
||||
// IMemoryCache automatically expires old entries so the throttle state
|
||||
// does not grow without bound across the lifetime of the process.
|
||||
var now = DateTime.UtcNow;
|
||||
if (_throttle.TryGetValue(userId, out var lastWrite) && (now - lastWrite).TotalSeconds < 60)
|
||||
var throttleKey = $"online-user-touch:{userId}";
|
||||
if (_cache.TryGetValue(throttleKey, out _))
|
||||
return;
|
||||
|
||||
_throttle[userId] = now;
|
||||
_cache.Set(throttleKey, true, TimeSpan.FromSeconds(60));
|
||||
|
||||
var email = context.User.FindFirstValue(ClaimTypes.Email) ?? string.Empty;
|
||||
var firstName = context.User.FindFirstValue(ClaimTypes.GivenName) ?? string.Empty;
|
||||
|
||||
Reference in New Issue
Block a user