| Category | Critical | Warning |
|---|---|---|
| 1. Hangfire & Background Jobs | 5 | 2 |
| 2. File I/O & Ephemeral Storage | 3 | 2 |
| 3. Caching & State | — | 1 |
| 4. New Feature: Multi-Tenant Connection Dictionary | — | — |
| TOTAL | 8 | 5 |
Ready = Ready for Production
| # | Service | Repo | Hangfire | File I/O | Cache/State | Overall |
|---|---|---|---|---|---|---|
| 1 | gateway-api | Platforms | — | — | ✅ | ✅ PROD |
| 2 | bo-apis | Platforms | Enqueue | — | ✅ | ❌ |
| 3 | adminportal-apis | Platforms | Recurring+Enqueue | Volume mounts | ✅ | ❌ |
| 4 | appsportal-apis | Platforms | Recurring+Outbox | — | ✅ | ❌ |
| 5 | inventory-apis | Platforms | Dashboard | — | ✅ | ⚠️ |
| 6 | integration-apis | Platforms | Webhooks | — | ✅ | ❌ |
| 7 | attachment-apis | InfraServices | Recurring cleanup | — | ✅ | ❌ |
| 8 | notification-apis | InfraServices | — | — | ✅ | ✅ PROD |
| 9 | workflows-apis | WorkflowDesigner | — | — | ✅ | ✅ PROD |
| 10 | hr-apis | Microtec HR | — | — | ✅ | ✅ PROD |
✅ Ready for Production ⚠️ Minor fix needed 🔴 Critical fix needed — Not applicable
All estimates are for 1 developer. Rows are ordered by recommended execution sequence.
| # | Work Item | Category | Services Affected | Est. Days |
|---|---|---|---|---|
| 1 | Refactor background jobs — system-wide architecture Design & implement custom job entry point, remove Hangfire dependency, wire up ACA Jobs / Service Bus pattern ⚙️ Requires a Software Engineer |
Hangfire | All 6 Hangfire services | 2 days |
| 2 | Apply bg job refactor in bo-apis Replace BackgroundJob.Enqueue calls (backup, bulk) with new entry point |
Hangfire | bo-apis | 2 days |
| 3 | Apply bg job refactor in appsportal + inventory + attachment Migrate recurring outbox, notifications, file cleanup jobs; remove Hangfire dashboard |
Hangfire | appsportal-apis, inventory-apis, attachment-apis | TBD |
| 4 | Fix connection string issue — Multi-Tenant Server Registry Replace encrypted per-tenant connection strings with ServerId + DatabaseName pattern; migrate tenant table |
New Feature | bo-apis, adminportal-apis, appsportal-apis, inventory-apis | 2 days |
| 5 | Fix AdminPortal File I/O issue Mount Azure File Share for translation backups, SQL scripts, and cache directory (FIO-1, FIO-2, FIO-3) |
File I/O | adminportal-apis | 1 day |
| 6 | Integrate adminportal with App Insights Wire up Application Insights SDK, configure telemetry, link to existing workspace |
Observability | adminportal-apis | 1 day |
| 7 | Test & validate App Insights logs Verify traces, exceptions, dependencies, and custom events flow correctly across all services in the portal |
Observability | adminportal-apis | 1 day |
| 8 | Integrate Redis & health checks in adminportal Replace local filesystem cache with Redis distributed cache; add /health endpoint covering DB, Redis, and storage dependencies |
Cache / Health | adminportal-apis | 1 day |
| Total (known estimates) | 10+ days | |||
RecurringJob.AddOrUpdate<IBackupService>(
nameof(BackupService),
job => job.BackupTranslationFileAsync(),
CronKeys.EveryDayAtMidnight);
With 3 replicas: 3 backups created simultaneously, writing to same path → file corruption.
RecurringJob.AddOrUpdate<IOutboxMessageBackGroundJob>(
nameof(OutboxMessageBackGroundJob),
job => job.ExecuteAsync(),
CronKeys.EveryDayAtMidnight);
With 3 replicas: Same domain events published 3 times → downstream systems receive duplicate messages.
RecurringJob.AddOrUpdate<INotificationProcessingJob>(
jobId,
job => job.ExecuteAsync(settingType, tenantContext),
cronExpression);
With 3 replicas: Users receive 3x duplicate notifications. Dynamic registration creates race conditions between replicas calling RemoveIfExists + AddOrUpdate.
Same pattern as HF-1c — race condition between RemoveIfExists and AddOrUpdate across replicas.
RecurringJob.AddOrUpdate<IFileCleanupService>(
nameof(FileCleanupService),
job => job.CleanUpUnusedFiles(),
CronKeys.EveryDayAtMidnight);
With 3 replicas: 3 pods attempt to delete the same files simultaneously → race conditions, exceptions, partial cleanup.
When KEDA scales containers to 0 replicas:
BackgroundJob.Enqueue<DatabaseBackupBackgroundJob>(job =>
job.ExecuteAsync(jobId, request));
BackgroundJob.Enqueue<BulkOperationBackgroundJob>(job =>
job.ExecuteAsync(jobId, request));
BackgroundJob.Enqueue<IWebhookDeliveryService>(service =>
service.ProcessDeliveryAsync(subDomainName, deliveryId));
BackgroundJob.Schedule<IWebhookDeliveryService>(
service => service.ProcessDeliveryAsync(subDomainName, deliveryId),
delay);
public void Enqueue<TJob>(Expression<Func<TJob, Task>> methodCall) =>
BackgroundJob.Enqueue(methodCall);
Generic wrapper — all callers inherit the same vulnerability.
BackgroundJob.Schedule<ZatcaBackgroundJob>(
job => job.RetryFailedInvoicesAsync(subDomainName, tenantContext),
TimeSpan.FromMinutes(5));
Uses IBackgroundJobClient (DI-injected — better pattern but same idempotency concern).
These Hangfire attributes use SQL-based distributed locks. With multiple Hangfire servers (replicas), lock contention causes:
Hangfire should be fully removed. Replace with:
POST /jobs/{jobName}) that accepts job parameters and executes the job logic directly// JobsController.cs — custom entry point replacing Hangfire
[ApiController]
[Route("jobs")]
[ApiKey] // Internal API key auth only
public class JobsController(IServiceProvider services) : ControllerBase
{
[HttpPost("{jobName}")]
public async Task<IActionResult> Execute(string jobName, [FromBody] JsonElement? parameters)
{
var job = services.GetKeyedService<IJob>(jobName);
if (job is null) return NotFound();
await job.ExecuteAsync(parameters);
return Ok();
}
}
This eliminates Hangfire's SQL polling, dashboard exposure, distributed lock issues, and connection pool overhead.
All static BackgroundJob.Enqueue() and RecurringJob.AddOrUpdate() calls need to be refactored to use the custom job entry point (see HF-5) or Service Bus queue dispatch.
Uses in-memory Channel<PerformanceLogEntry> to batch logs every 5 seconds. Scale-to-zero or restart drops all buffered logs. BoundedChannelOptions with DropOldest silently discards data under load.
// Line 14 — hardcoded to wwwroot
Path.Combine("wwwroot", _configuration[ConfigKeys.BackupsUploadPath])
// Line 75-78 — creates directory and writes backup SQL to disk
Directory.CreateDirectory(directoryPath);
using var writer = new StreamWriter(filePath, ...);
Impact: Translation backup files lost on every container restart. Database records reference non-existent files.
wwwroot backup path. No code changes needed.// Line 24-26 — resolves script file path
Path.Combine(AppContext.BaseDirectory, options.Value.ScriptsFilePath)
// Line 75, 90, 103 — writes updated scripts back to disk
WriteScriptsToFileAsync(...)
Impact: If SQL scripts are created/edited at runtime, all changes lost on restart.
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "AdminPortal/Commands/Data/commands.json")
Impact: If commands.json is baked into Docker image and read-only → OK. If modified at runtime → changes lost.
Creates/clears Cache/ directory relative to app root. Acceptable for transient caching — but cache is not shared across replicas.
var tempFilePath = Path.GetTempFileName();
var download = fileClient.Download();
using (var fileStream = File.Create(tempFilePath))
{
download.Value.Content.CopyTo(fileStream);
}
return new FileStream(tempFilePath, FileMode.Open, FileAccess.Read,
FileShare.None, 4096, FileOptions.DeleteOnClose);
Creates temp file for streaming downloads. DeleteOnClose flag handles cleanup, but if exception occurs between creation and FileStream construction, temp file leaks. Minor issue.
Files in AdminPortal/Commands/Handlers/ create/clear Cache/ directory on local filesystem. Cache not shared between replicas. Acceptable for non-critical caching, but should use Redis for consistency.
Each tenant stores a full encrypted connection string in the database — server URL, credentials, and database name all baked together. Changing the SQL server requires re-encrypting every tenant's connection string.
{
"MultiTenantsServer": [
{
"Id": "ServerA",
"Url": "10.100.1.4",
"Port": 1433,
"User": "sa",
"Password": "{{from-key-vault}}"
},
{
"Id": "ServerB",
"Url": "10.200.1.4",
"Port": 1433,
"User": "sa",
"Password": "{{from-key-vault}}"
}
]
}
| SubDomain | ServerId | DatabaseName |
|---|---|---|
| test | ServerA | MicrotecApps.shared |
| testdo | ServerA | MicrotecApps.TestDo |
| client-x | ServerB | MicrotecApps.ClientX |
public class TenantInfo
{
public string SubDomain { get; set; }
public string ServerId { get; set; } // e.g. "ServerA"
public string DatabaseName { get; set; } // e.g. "MicrotecApps.shared"
}
// ConnectionStringBuilder resolves ServerId → server config, appends DatabaseName
var connStr = connectionStringBuilder.Build(tenant.ServerId, tenant.DatabaseName);
// → "Server=10.100.1.4,1433;Database=MicrotecApps.shared;User Id=sa;Password=...;"
MultiTenantsServer config section with server registry arrayServerId column to tenant table (migrate existing tenants — all currently point to same server)ConnectionStringBuilder service that resolves ServerId → server config + appends DatabaseNameDatabaseName columnITenantProvider / ITenantContextManager to use the builderMultiTenantsServer--0--Password, etc.)| Service | AddHangfire | UseHangfireServer | UseHangfireDashboard | Recurring Jobs | Enqueue |
|---|---|---|---|---|---|
| BO APIs | ✅ | ✅ | — | — | ✅ (backup, bulk) |
| AdminPortal | ✅ | ✅ | ✅ | ✅ (translation backup) | ✅ (backup, bulk, data transfer) |
| AppsPortal | ✅ | ✅ | ✅ | ✅ (outbox, notifications) | ✅ (sync, zatca, generic) |
| Inventory | ✅ | ✅ | ✅ | — | — |
| Integration | ✅ | ✅ | ✅ | — | ✅ (webhooks) |
| Attachment | ✅ | ✅ | ✅ | ✅ (file cleanup) | — |
| Notifications | — | — | — | — | — |
| Workflows | — | — | — | — | — |
| HR | — | — | — | — | — |
| Gateway | — | — | — | — | — |
End of Report