reduce server load using cache

reduce server load using cache

Now let me compile the full analysis.


How Caching Concepts Apply to ERP Crystal MFG

Current State: No Caching Anywhere

The system has zero caching infrastructure — no IMemoryCache, no Redis, no [ResponseCache] attributes, no output caching, no static file cache headers. The only cache-like mechanism is a ConcurrentDictionary<string, string> in DapperContext.cs:18 for SQL server name lookups.

Every single request hits the database, regenerates PDFs/Excel from scratch, and re-fetches the same reference data (company info, system parameters) dozens of times.


Server-Side Caching: Applicability & Impact

Concept from Article Where It Fits in Our System Impact
In-memory Cache (IMemoryCache) Everywhere. Start with: GetCompanyMstInfo (called ~5-15x per request across controllers), GetSysParameterValue (called dozens of times), GetStdInsNotes, GetPartyContactInfo High — these are read-only reference queries hit on nearly every page/report
Database Query Result Caching Heavy analytic reports: GstReportRepository (CTEs, 1725 lines), ArApReportRepository (GROUP BY aggregates), SalesAnalysisReportRepository, InventoryValuationReportRepository (1421 lines with temp tables) Critical — these are the most expensive queries. Cache with sliding expiration (e.g., 5 min)
Output Caching API GET endpoints returning report data (e.g., InvoiceIndex, all report list endpoints). Add [ResponseCache] + UseOutputCache() middleware High — hundreds of Blazor pages hit these on every load. Reduces DB + serialization
PDF Generation Caching QuestPDF documents: Currently uses random filenames (Random rnd + GetNexInt(1000000000)), ensuring every request generates a new PDF even for identical data. Use content-hash based filenames + File.Exists() check High — for frequently-printed documents (invoices, vouchers), eliminates redundant SkiaSharp rendering
Excel Report Caching ClosedXML exports: 40+ controllers, ~100 Excel files. Same random-filename pattern Medium — Excel exports are typically one-off, but caching with short TTL helps for repeated exports
Duplicate Same-Request Caching GetInvoiceInfo called 24 times in InvoiceController.cs within a single request flow. Cache per-request using IMemoryCache scoped to the request or simple Lazy<T> local variables Medium-High — eliminates redundant DB round-trips within a single operation
Distributed Cache (Redis) Multi-server deploy: session state, API key validation (currently reads env var per request), rate-limit counters Long-term — not needed until horizontal scaling

Priority 1: GetCompanyMstInfo + GetSysParameterValue + GetStdInsNotes

These are called in every single controller that generates any report. They are read-only reference data queried from the same database. A pattern like:

// In a base controller or service
private async Task<CompanyMstInfo> GetCachedCompanyInfo(string dbname)
{
    var cacheKey = $"CompanyMstInfo_{dbname}";
    return await _memoryCache.GetOrCreateAsync(cacheKey, async entry =>
    {
        entry.SlidingExpiration = TimeSpan.FromMinutes(10);
        return await _ICompanyRepository.GetCompanyMstInfo(dbname);
    });
}

This alone would eliminate thousands of redundant DB calls per hour.

Priority 2: Content-Hash PDF Caching

Currently all PDFs use:

Random rnd = new();
var randomNumber = rnd.Next(1000000000).ToString().Trim();
var randomFileName = $"{randomNumber}.pdf";

This guarantees zero reuse. Instead:

// Deterministic filename based on input hash
var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(
    $"{dbname}-{invoiceId}-{date}"  // unique identifying parameters
)));
var pathFileName = Path.Combine(reportsBase, $"{hash}.pdf");
if (!File.Exists(pathFileName))
    document.GeneratePdf(pathFileName);  // Only generate if not cached
return File(System.IO.File.ReadAllBytes(pathFileName), "application/pdf", "invoice.pdf");

Priority 3: Output Caching for API Endpoints

Add UseOutputCache() in Program.cs and tag cacheable GET endpoints:

// Program.cs
builder.Services.AddOutputCache();
app.UseOutputCache();

// Controller
[HttpGet("{yearLabel},{dbname}")]
[OutputCache(Duration = 60, VaryByQueryKeys = ["*"])]  // 60-second cache
public async Task<ActionResult> InvoiceIndex(string yearLabel, string dbname)

This is ideal for the ~150+ GET endpoints that the Blazor front-end calls on every page load.


Client-Side Caching: Applicability & Impact

Concept from Article Where It Fits in Our System Impact
Browser Cache (Cache-Control headers) Static assets in wwwroot (CSS, JS, images). Currently UseStaticFiles() is called with no options — browsers get no caching guidance Medium — add StaticFileOptions with max-age=31536000 + versioned filenames for CSS/JS
Blazored.LocalStorage (already used) Currently only stores session state (userinfo) and grid pagination state. Could be extended to cache API response data for offline-tolerant lookups or to reduce server round-trips Medium — cache reference data (item masters, account lists) client-side with version checks
Service Worker / PWA Blazor Server is not a natural fit for offline — all UI rendering happens on the server. But could cache API responses via service worker for offline resilience Low — Blazor Server circuit requires connection; offline PWA adds complexity for marginal benefit
CDN (CloudFront — already used) Currently used only for user-uploaded documents (S3). Could extend to serve generated reports via CloudFront with long cache TTL + invalidation on data change Medium — reduces server load for document downloads, but requires moving reports to S3

Client-Side Priority: Static File Caching (2-line fix)

Most impactful client-side change with minimal effort:

// Program.cs line 385 — change from
app.UseStaticFiles();

// to:
app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        ctx.Context.Response.Headers.CacheControl = 
            new CacheControlHeaderValue { MaxAge = TimeSpan.FromDays(365), Public = true }.ToString();
    }
});

For Blazor Server specifically, the Blazor framework already handles caching of .dll and .wasm files through ASP.NET Core’s built-in static file versioning (_framework/blazor.server.js is cached by the browser by default). Adding explicit headers ensures this works consistently.

Why Blazor Server Limits Client-Side Caching

Because this is Blazor Server (not WASM), the UI rendering happens on the server. Every user interaction goes through a SignalR circuit. This means:

  • Client-side caching cannot cache rendered HTML — UI is server-generated
  • Client-side caching can only cache API response data to potentially skip some HTTP calls
  • LocalStorage is the primary client-side tool (already used for session/grid state)
  • Service Workers would intercept API calls to the backend, but the Blazor circuit itself must stay connected

Summary: Highest-Impact Caching Plan

Rank Cache Type What to Cache Effort Impact Why
1 In-memory (IMemoryCache) GetCompanyMstInfo, GetSysParameterValue, GetStdInsNotes Low (add service registration + 3 cache wrappers) Very High Called 5-15x per request across dozens of controllers
2 Output Caching GET API endpoints for lists/reports Low (AddService + Use + attribute per endpoint) Very High 150+ endpoints hit by Blazor on every page load
3 Per-Request Caching GetInvoiceInfo (called 24x in InvoiceController) Low (local variable or Lazy) High Eliminates redundant DB round-trips in same request
4 PDF Content-Hash Cache Generated PDF files (QuestPDF) Medium (change filename strategy in 14 files) High Eliminates redundant SkiaSharp rendering for repeated prints
5 Static File Cache wwwroot CSS/JS/images Very Low (2-line config change) Medium Reduces bandwidth, faster page loads
6 Database Query Cache Heavy analytic report queries (Gst, ArAp, SalesAnalysis) Medium (cache per repository method with TTL) High Most expensive queries; AdjustToContents + query = 536x savings
7 CloudFront CDN Generated report files (PDF/Excel) via S3 High (requires S3 upload pipeline) Medium Offloads report downloads to CDN, but complex to implement

Want me to proceed with implementing Priority 1 (in-memory caching for reference data) as a concrete first step?