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?