using AutoMapper; using Marco.Pms.DataAccess.Data; using Marco.Pms.Helpers; using Marco.Pms.Helpers.CacheHelper; using Marco.Pms.Model.Expenses; using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.MongoDBModels.Employees; using Marco.Pms.Model.MongoDBModels.Expenses; using Marco.Pms.Model.MongoDBModels.Masters; using Marco.Pms.Model.MongoDBModels.Project; using Marco.Pms.Model.MongoDBModels.Utility; using Marco.Pms.Model.OrganizationModel; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Utilities; using MarcoBMS.Services.Service; using Microsoft.EntityFrameworkCore; using MongoDB.Driver; using Project = Marco.Pms.Model.Projects.Project; namespace Marco.Pms.Services.Helpers { public class CacheUpdateHelper { private readonly IDbContextFactory _dbContextFactory; private readonly ApplicationDbContext _context; private readonly IMapper _mapper; private readonly ProjectCache _projectCache; private readonly EmployeeCache _employeeCache; private readonly ReportCache _reportCache; private readonly ExpenseCache _expenseCache; private readonly ILoggingService _logger; private readonly GeneralHelper _generalHelper; private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8"); private static readonly Guid Rejected = Guid.Parse("d1ee5eec-24b6-4364-8673-a8f859c60729"); public CacheUpdateHelper( IMapper mapper, ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ExpenseCache expenseCache, ILoggingService logger, IDbContextFactory dbContextFactory, ApplicationDbContext context, GeneralHelper generalHelper) { _mapper = mapper; _projectCache = projectCache; _employeeCache = employeeCache; _reportCache = reportCache; _expenseCache = expenseCache; _logger = logger; _dbContextFactory = dbContextFactory; _context = context; _generalHelper = generalHelper; } #region ======================================================= Project Details Cache ======================================================= public async Task AddProjectDetails(Project project) { // --- Step 1: Fetch all required data from the database in parallel --- // Each task uses its own DbContext instance to avoid concurrency issues. var statusTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); return await context.StatusMasters .AsNoTracking() .Where(s => s.Id == project.ProjectStatusId) .Select(s => new { s.Id, s.Status }) // Projection .FirstOrDefaultAsync(); }); var promotorTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); return await context.Organizations .AsNoTracking() .Where(o => o.Id == project.PromoterId) .Select(o => new OrganizationMongoDB { Id = o.Id.ToString(), Name = o.Name, ContactPerson = o.ContactPerson, Email = o.Email, Address = o.Address, ContactNumber = o.ContactNumber, SPRID = o.SPRID }) // Projection .FirstOrDefaultAsync(); }); var pmcTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); return await context.Organizations .AsNoTracking() .Where(o => o.Id == project.PMCId) .Select(o => new OrganizationMongoDB { Id = o.Id.ToString(), Name = o.Name, ContactPerson = o.ContactPerson, Email = o.Email, Address = o.Address, ContactNumber = o.ContactNumber, SPRID = o.SPRID }) // Projection .FirstOrDefaultAsync(); }); var teamSizeTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); return await context.ProjectAllocations .AsNoTracking() .CountAsync(pa => pa.ProjectId == project.Id && pa.IsActive); // Server-side count is efficient }); // This task fetches the entire infrastructure hierarchy and performs aggregations in the database. var infrastructureTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); // 1. Fetch all hierarchical data using projections. // This is still a chain, but it's inside one task and much faster due to projections. var buildings = await context.Buildings.AsNoTracking() .Where(b => b.ProjectId == project.Id) .Select(b => new { b.Id, b.ProjectId, b.Name, b.Description }) .ToListAsync(); var buildingIds = buildings.Select(b => b.Id).ToList(); var floors = await context.Floor.AsNoTracking() .Where(f => buildingIds.Contains(f.BuildingId)) .Select(f => new { f.Id, f.BuildingId, f.FloorName }) .ToListAsync(); var floorIds = floors.Select(f => f.Id).ToList(); var workAreas = await context.WorkAreas.AsNoTracking() .Where(wa => floorIds.Contains(wa.FloorId)) .Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }) .ToListAsync(); var workAreaIds = workAreas.Select(wa => wa.Id).ToList(); // 2. THE KEY OPTIMIZATION: Aggregate work items in the database. var workSummaries = await context.WorkItems.AsNoTracking() .Where(wi => workAreaIds.Contains(wi.WorkAreaId)) .GroupBy(wi => wi.WorkAreaId) // Group by parent on the DB server .Select(g => new // Let the DB do the SUM { WorkAreaId = g.Key, PlannedWork = g.Sum(i => i.PlannedWork), CompletedWork = g.Sum(i => i.CompletedWork) }) .ToDictionaryAsync(x => x.WorkAreaId); // Return a ready-to-use dictionary return (buildings, floors, workAreas, workSummaries); }); // Wait for all parallel database operations to complete. await Task.WhenAll(statusTask, teamSizeTask, infrastructureTask, promotorTask, pmcTask); // Get the results from the completed tasks. var status = statusTask.Result; var teamSize = teamSizeTask.Result; var (allBuildings, allFloors, allWorkAreas, workSummariesByWorkAreaId) = infrastructureTask.Result; var promotor = promotorTask.Result; var pmc = pmcTask.Result; // --- Step 2: Process the fetched data and build the MongoDB model --- var projectDetails = new ProjectMongoDB { Id = project.Id.ToString(), Name = project.Name, ShortName = project.ShortName, ProjectAddress = project.ProjectAddress, StartDate = project.StartDate, EndDate = project.EndDate, ContactPerson = project.ContactPerson, TeamSize = teamSize }; projectDetails.ProjectStatus = new StatusMasterMongoDB { Id = status!.Id.ToString(), Status = status.Status }; // Use fast in-memory lookups instead of .Where() in loops. var floorsByBuildingId = allFloors.ToLookup(f => f.BuildingId); var workAreasByFloorId = allWorkAreas.ToLookup(wa => wa.FloorId); double totalPlannedWork = 0, totalCompletedWork = 0; var buildingMongoList = new List(); foreach (var building in allBuildings) { double buildingPlanned = 0, buildingCompleted = 0; var floorMongoList = new List(); foreach (var floor in floorsByBuildingId[building.Id]) // Fast lookup { double floorPlanned = 0, floorCompleted = 0; var workAreaMongoList = new List(); foreach (var wa in workAreasByFloorId[floor.Id]) // Fast lookup { // Get the pre-calculated summary from the dictionary. O(1) operation. workSummariesByWorkAreaId.TryGetValue(wa.Id, out var summary); var waPlanned = summary?.PlannedWork ?? 0; var waCompleted = summary?.CompletedWork ?? 0; workAreaMongoList.Add(new WorkAreaMongoDB { Id = wa.Id.ToString(), FloorId = wa.FloorId.ToString(), AreaName = wa.AreaName, PlannedWork = waPlanned, CompletedWork = waCompleted }); floorPlanned += waPlanned; floorCompleted += waCompleted; } floorMongoList.Add(new FloorMongoDB { Id = floor.Id.ToString(), BuildingId = floor.BuildingId.ToString(), FloorName = floor.FloorName, PlannedWork = floorPlanned, CompletedWork = floorCompleted, WorkAreas = workAreaMongoList }); buildingPlanned += floorPlanned; buildingCompleted += floorCompleted; } buildingMongoList.Add(new BuildingMongoDB { Id = building.Id.ToString(), ProjectId = building.ProjectId.ToString(), BuildingName = building.Name, Description = building.Description, PlannedWork = buildingPlanned, CompletedWork = buildingCompleted, Floors = floorMongoList }); totalPlannedWork += buildingPlanned; totalCompletedWork += buildingCompleted; } projectDetails.Buildings = buildingMongoList; projectDetails.PlannedWork = totalPlannedWork; projectDetails.CompletedWork = totalCompletedWork; projectDetails.Promoter = promotor; projectDetails.PMC = pmc; try { await _projectCache.AddProjectDetailsToCache(projectDetails); } catch (Exception ex) { _logger.LogError(ex, "Error occurred while adding project {ProjectId} to Cache", project.Id); } } public async Task AddProjectDetailsList(List projects) { var projectIds = projects.Select(p => p.Id).ToList(); if (!projectIds.Any()) { return; // Nothing to do } var projectStatusIds = projects.Select(p => p.ProjectStatusId).Distinct().ToList(); var promotorIds = projects.Select(p => p.PromoterId).Distinct().ToList(); var pmcsIds = projects.Select(p => p.PMCId).Distinct().ToList(); // --- Step 1: Fetch all required data in maximum parallel --- // Each task uses its own DbContext and selects only the required columns (projection). var statusTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); return await context.StatusMasters .AsNoTracking() .Where(s => projectStatusIds.Contains(s.Id)) .Select(s => new { s.Id, s.Status }) // Projection .ToDictionaryAsync(s => s.Id); }); var organizationTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); return await context.Organizations .AsNoTracking() .Where(o => promotorIds.Contains(o.Id) || pmcsIds.Contains(o.Id)) .Select(o => new OrganizationMongoDB { Id = o.Id.ToString(), Name = o.Name, ContactPerson = o.ContactPerson, Email = o.Email, Address = o.Address, ContactNumber = o.ContactNumber, SPRID = o.SPRID }) // Projection .ToListAsync(); }); var teamSizeTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); // Server-side aggregation and projection into a dictionary return await context.ProjectAllocations .AsNoTracking() .Where(pa => projectIds.Contains(pa.ProjectId) && pa.IsActive) .GroupBy(pa => pa.ProjectId) .Select(g => new { ProjectId = g.Key, Count = g.Count() }) .ToDictionaryAsync(x => x.ProjectId, x => x.Count); }); var buildingsTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); return await context.Buildings .AsNoTracking() .Where(b => projectIds.Contains(b.ProjectId)) .Select(b => new { b.Id, b.ProjectId, b.Name, b.Description }) // Projection .ToListAsync(); }); // We need the building IDs for the next level, so we must await this one first. var allBuildings = await buildingsTask; var buildingIds = allBuildings.Select(b => b.Id).ToList(); var floorsTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); return await context.Floor .AsNoTracking() .Where(f => buildingIds.Contains(f.BuildingId)) .Select(f => new { f.Id, f.BuildingId, f.FloorName }) // Projection .ToListAsync(); }); // We need floor IDs for the next level. var allFloors = await floorsTask; var floorIds = allFloors.Select(f => f.Id).ToList(); var workAreasTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); return await context.WorkAreas .AsNoTracking() .Where(wa => floorIds.Contains(wa.FloorId)) .Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }) // Projection .ToListAsync(); }); // The most powerful optimization: Aggregate work items in the database. var workSummaryTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); var workAreaIds = await context.WorkAreas .Where(wa => floorIds.Contains(wa.FloorId)) .Select(wa => wa.Id) .ToListAsync(); // Let the DB do the SUM. This is much faster and transfers less data. return await context.WorkItems .AsNoTracking() .Where(wi => workAreaIds.Contains(wi.WorkAreaId)) .GroupBy(wi => wi.WorkAreaId) .Select(g => new { WorkAreaId = g.Key, PlannedWork = g.Sum(wi => wi.PlannedWork), CompletedWork = g.Sum(wi => wi.CompletedWork) }) .ToDictionaryAsync(x => x.WorkAreaId); }); // Await the remaining parallel tasks. await Task.WhenAll(statusTask, teamSizeTask, workAreasTask, workSummaryTask, organizationTask); // --- Step 2: Process the fetched data and build the MongoDB models --- var allStatuses = statusTask.Result; var teamSizesByProjectId = teamSizeTask.Result; var allWorkAreas = workAreasTask.Result; var workSummariesByWorkAreaId = workSummaryTask.Result; var organizations = organizationTask.Result; // Create fast in-memory lookups for hierarchical data var buildingsByProjectId = allBuildings.ToLookup(b => b.ProjectId); var floorsByBuildingId = allFloors.ToLookup(f => f.BuildingId); var workAreasByFloorId = allWorkAreas.ToLookup(wa => wa.FloorId); var projectDetailsList = new List(projects.Count); foreach (var project in projects) { var projectDetails = new ProjectMongoDB { Id = project.Id.ToString(), Name = project.Name, ShortName = project.ShortName, ProjectAddress = project.ProjectAddress, StartDate = project.StartDate, EndDate = project.EndDate, ContactPerson = project.ContactPerson, TeamSize = teamSizesByProjectId.GetValueOrDefault(project.Id, 0) }; if (allStatuses.TryGetValue(project.ProjectStatusId, out var status)) { projectDetails.ProjectStatus = new StatusMasterMongoDB { Id = status.Id.ToString(), Status = status.Status }; } double totalPlannedWork = 0, totalCompletedWork = 0; var buildingMongoList = new List(); foreach (var building in buildingsByProjectId[project.Id]) { double buildingPlanned = 0, buildingCompleted = 0; var floorMongoList = new List(); foreach (var floor in floorsByBuildingId[building.Id]) { double floorPlanned = 0, floorCompleted = 0; var workAreaMongoList = new List(); foreach (var wa in workAreasByFloorId[floor.Id]) { double waPlanned = 0, waCompleted = 0; if (workSummariesByWorkAreaId.TryGetValue(wa.Id, out var summary)) { waPlanned = summary.PlannedWork; waCompleted = summary.CompletedWork; } workAreaMongoList.Add(new WorkAreaMongoDB { Id = wa.Id.ToString(), FloorId = wa.FloorId.ToString(), AreaName = wa.AreaName, PlannedWork = waPlanned, CompletedWork = waCompleted }); floorPlanned += waPlanned; floorCompleted += waCompleted; } floorMongoList.Add(new FloorMongoDB { Id = floor.Id.ToString(), BuildingId = floor.BuildingId.ToString(), FloorName = floor.FloorName, PlannedWork = floorPlanned, CompletedWork = floorCompleted, WorkAreas = workAreaMongoList }); buildingPlanned += floorPlanned; buildingCompleted += floorCompleted; } buildingMongoList.Add(new BuildingMongoDB { Id = building.Id.ToString(), ProjectId = building.ProjectId.ToString(), BuildingName = building.Name, Description = building.Description, PlannedWork = buildingPlanned, CompletedWork = buildingCompleted, Floors = floorMongoList }); totalPlannedWork += buildingPlanned; totalCompletedWork += buildingCompleted; } projectDetails.Buildings = buildingMongoList; projectDetails.PlannedWork = totalPlannedWork; projectDetails.CompletedWork = totalCompletedWork; projectDetails.Promoter = organizations.FirstOrDefault(o => o.Id == project.PromoterId.ToString()); projectDetails.PMC = organizations.FirstOrDefault(o => o.Id == project.PMCId.ToString()); projectDetailsList.Add(projectDetails); } // --- Step 3: Update the cache --- try { await _projectCache.AddProjectDetailsListToCache(projectDetailsList); } catch (Exception ex) { _logger.LogError(ex, "Error occurred while adding project list to Cache"); } } public async Task UpdateProjectDetailsOnly(Project project) { var projectStatusTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.StatusMasters .FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId) ?? new StatusMaster(); }); var promotorTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.Organizations.FirstOrDefaultAsync(o => o.Id == project.PromoterId) ?? new Organization(); }); var pmcTask = Task.Run(async () => { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.Organizations.FirstOrDefaultAsync(o => o.Id == project.PMCId) ?? new Organization(); }); await Task.WhenAll(projectStatusTask, promotorTask, pmcTask); var projectStatus = projectStatusTask.Result; var promotor = promotorTask.Result; var pmc = pmcTask.Result; try { bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project, projectStatus, promotor, pmc); return response; } catch (Exception ex) { _logger.LogError(ex, "Error occured while updating project {ProjectId} to Cache", project.Id); return false; } } public async Task GetProjectDetails(Guid projectId) { try { var response = await _projectCache.GetProjectDetailsFromCache(projectId); return response; } catch (Exception ex) { _logger.LogError(ex, "Error occured while getting project {ProjectId} to Cache"); return null; } } public async Task GetProjectDetailsWithBuildings(Guid projectId) { try { var response = await _projectCache.GetProjectDetailsWithBuildingsFromCache(projectId); return response; } catch (Exception ex) { _logger.LogError(ex, "Error occured while getting project {ProjectId} to Cache"); return null; } } public async Task?> GetProjectDetailsList(List projectIds) { try { var response = await _projectCache.GetProjectDetailsListFromCache(projectIds); if (response.Any()) { return response; } else { return null; } } catch (Exception ex) { _logger.LogError(ex, "Error occured while getting list of project details from to Cache"); return null; } } public async Task DeleteProjectByIdAsync(Guid projectId) { try { var response = await _projectCache.DeleteProjectByIdFromCacheAsync(projectId); } catch (Exception ex) { _logger.LogError(ex, "Error occured while deleting project from to Cache"); } } public async Task RemoveProjectsAsync(List projectIds) { try { var response = await _projectCache.RemoveProjectsFromCacheAsync(projectIds); } catch (Exception ex) { _logger.LogError(ex, "Error occured while deleting project list from to Cache"); } } #endregion #region ======================================================= Project Infrastructure Cache ======================================================= public async Task AddBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) { try { await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); } catch (Exception ex) { _logger.LogWarning("Error occured while adding project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message); } } public async Task UpdateBuildngInfra(Guid projectId, Building? building = null, Floor? floor = null, WorkArea? workArea = null, Guid? buildingId = null) { try { var response = await _projectCache.UpdateBuildngInfraToCache(projectId, building, floor, workArea, buildingId); if (!response) { await _projectCache.AddBuildngInfraToCache(projectId, building, floor, workArea, buildingId); } } catch (Exception ex) { _logger.LogWarning("Error occured while updating project infra for project {ProjectId} to Cache: {Error}", projectId, ex.Message); } } public async Task?> GetBuildingInfra(Guid projectId) { try { var response = await _projectCache.GetBuildingInfraFromCache(projectId); return response; } catch (Exception ex) { _logger.LogWarning("Error occured while getting project infra for project {ProjectId} form Cache: {Error}", projectId, ex.Message); return null; } } public async Task UpdatePlannedAndCompleteWorksInBuilding(Guid workAreaId, double plannedWork = 0, double completedWork = 0) { try { await _projectCache.UpdatePlannedAndCompleteWorksInBuildingFromCache(workAreaId, plannedWork, completedWork); } catch (Exception ex) { _logger.LogWarning("Error occured while updating planned work and completed work in building infra form Cache: {Error}", ex.Message); } } public async Task GetBuildingAndFloorByWorkAreaId(Guid workAreaId) { try { var response = await _projectCache.GetBuildingAndFloorByWorkAreaIdFromCache(workAreaId); return response; } catch (Exception ex) { _logger.LogWarning("Error occured while fetching workArea Details using its ID form Cache: {Error}", ex.Message); return null; } } #endregion #region ======================================================= WorkItem Cache ======================================================= public async Task?> GetWorkItemsByWorkAreaIds(List workAreaIds, List serviceIds) { try { var response = await _projectCache.GetWorkItemsByWorkAreaIdsFromCache(workAreaIds, serviceIds); if (response.Count > 0) { return response; } return null; } catch (Exception ex) { _logger.LogWarning("Error occured while fetching workItems list using workArea IDs list form Cache: {Error}", ex.Message); return null; } } public async Task ManageWorkItemDetails(List workItems) { try { var workAreaId = workItems.First().WorkAreaId; var workItemDB = await _generalHelper.GetWorkItemsListFromDB(workAreaId); await _projectCache.ManageWorkItemDetailsToCache(workItemDB); } catch (Exception ex) { _logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message); } } public async Task ManageWorkItemDetailsByVM(List workItems) { try { await _projectCache.ManageWorkItemDetailsToCache(workItems); } catch (Exception ex) { _logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message); } } public async Task?> GetWorkItemDetailsByWorkArea(Guid workAreaId, List serviceIds) { try { var workItems = await _projectCache.GetWorkItemDetailsByWorkAreaFromCache(workAreaId, serviceIds); if (workItems.Count > 0) { return workItems; } else { return null; } } catch (Exception ex) { _logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message); return null; } } public async Task GetWorkItemDetailsById(Guid id) { try { var workItem = await _projectCache.GetWorkItemDetailsByIdFromCache(id); if (workItem.Id != "") { return workItem; } else { return null; } } catch (Exception ex) { _logger.LogWarning("Error occured while fetching list of workItems form Cache: {Error}", ex.Message); return null; } } public async Task UpdatePlannedAndCompleteWorksInWorkItem(Guid id, double plannedWork = 0, double completedWork = 0, double todaysAssigned = 0) { try { var response = await _projectCache.UpdatePlannedAndCompleteWorksInWorkItemToCache(id, plannedWork, completedWork, todaysAssigned); } catch (Exception ex) { _logger.LogWarning("Error occured while updating planned work, completed work, and today's assigned work in workItems in Cache: {Error}", ex.Message); } } public async Task DeleteWorkItemByIdAsync(Guid workItemId) { try { var response = await _projectCache.DeleteWorkItemByIdFromCacheAsync(workItemId); } catch (Exception ex) { _logger.LogWarning("Error occured while deleting work item from to Cache: {Error}", ex.Message); } } #endregion #region ======================================================= Employee Profile Cache ======================================================= public async Task AddApplicationRole(Guid employeeId, List roleIds, Guid tenantId) { // 1. Guard Clause: Avoid unnecessary database work if there are no roles to add. if (roleIds == null || !roleIds.Any()) { return; // Nothing to add, so the operation did not result in a change. } Task> getPermissionIdsTask = Task.Run(async () => { using var context = _dbContextFactory.CreateDbContext(); return await context.RolePermissionMappings .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) .Select(p => p.FeaturePermissionId.ToString()) .Distinct() .ToListAsync(); }); // 3. Prepare role IDs in parallel with the database query. var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); // 4. Await the database query result. var newPermissionIds = await getPermissionIdsTask; try { var response = await _employeeCache.AddApplicationRoleToCache(employeeId, newRoleIds, newPermissionIds, tenantId); } catch (Exception ex) { _logger.LogWarning("Error occured while adding Application roleIds to Cache to employee {Employee}: {Error}", employeeId, ex.Message); } } public async Task AddProjects(Guid employeeId, List projectIds, Guid tenantId) { try { var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds, tenantId); return response; } catch (Exception ex) { _logger.LogWarning("Error occured while adding projectIds for employee {EmployeeId} to Cache: {Error}", employeeId, ex.Message); return false; } } public async Task?> GetProjects(Guid employeeId, Guid tenantId) { try { var response = await _employeeCache.GetProjectsFromCache(employeeId, tenantId); if (response.Count > 0) { return response; } return null; } catch (Exception ex) { _logger.LogWarning("Error occured while getting projectIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message); return null; } } public async Task?> GetPermissions(Guid employeeId, Guid tenantId) { try { var response = await _employeeCache.GetPermissionsFromCache(employeeId, tenantId); if (response.Count > 0) { return response; } return null; } catch (Exception ex) { _logger.LogWarning("Error occured while getting permissionIds for employee {EmployeeId} from Cache: {Error}", employeeId, ex.Message); return null; } } public async Task ClearAllProjectIds(Guid employeeId, Guid tenantId) { try { var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId, tenantId); } catch (Exception ex) { _logger.LogWarning("Error occured while deleting projectIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); } } public async Task ClearAllProjectIdsByRoleId(Guid roleId) { try { await _employeeCache.ClearAllProjectIdsByRoleIdFromCache(roleId); } catch (Exception ex) { _logger.LogWarning("Error occured while deleting projectIds from Cache for Application Role {RoleId}: {Error}", roleId, ex.Message); } } public async Task ClearAllProjectIdsByPermissionId(Guid permissionId, Guid tenantId) { try { await _employeeCache.ClearAllProjectIdsByPermissionIdFromCache(permissionId, tenantId); } catch (Exception ex) { _logger.LogWarning("Error occured while deleting projectIds from Cache for Permission {PermissionId}: {Error}", permissionId, ex.Message); } } public async Task ClearAllPermissionIdsByEmployeeID(Guid employeeId, Guid tenantId) { try { var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId, tenantId); } catch (Exception ex) { _logger.LogWarning("Error occured while deleting permissionIds from Cache for employee {EmployeeId}: {Error}", employeeId, ex.Message); } } public async Task ClearAllPermissionIdsByRoleId(Guid roleId) { try { var response = await _employeeCache.ClearAllPermissionIdsByRoleIdFromCache(roleId); } catch (Exception ex) { _logger.LogWarning("Error occured while deleting permissionIds from Cache for Application role {RoleId}: {Error}", roleId, ex.Message); } } public async Task RemoveRoleId(Guid employeeId, Guid roleId, Guid tenantId) { try { var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId, tenantId); } catch (Exception ex) { _logger.LogWarning("Error occured while deleting Application role {RoleId} from Cache for employee {EmployeeId}: {Error}", roleId, employeeId, ex.Message); } } public async Task ClearAllEmployeesFromCacheByEmployeeIds(List employeeIds, Guid tenantId) { try { var stringEmployeeIds = employeeIds.Select(e => e.ToString()).ToList(); var response = await _employeeCache.ClearAllEmployeesFromCacheByEmployeeIds(stringEmployeeIds, tenantId); } catch (Exception ex) { _logger.LogError(ex, "Error occured while deleting all employees from Cache"); } } public async Task ClearAllEmployees() { try { var response = await _employeeCache.ClearAllEmployeesFromCache(); } catch (Exception ex) { _logger.LogError(ex, "Error occured while deleting all employees from Cache"); } } #endregion #region ======================================================= Expenses Cache ======================================================= public async Task AddExpenseByObjectAsync(Expenses expense) { var expenseCache = await GetAllExpnesRelatedTablesForSingle(expense, expense.TenantId); try { await _expenseCache.AddExpenseToCacheAsync(expenseCache); } catch (Exception ex) { _logger.LogError(ex, "Error occurd while storing expense related table in cahce"); return; } return; } public async Task AddExpenseByIdAsync(Guid Id, Guid tenantId) { var expense = await _context.Expenses.AsNoTracking().FirstOrDefaultAsync(e => e.Id == Id && e.TenantId == tenantId); if (expense == null) { return null; } var expenseCache = await GetAllExpnesRelatedTablesForSingle(expense, expense.TenantId); try { await _expenseCache.AddExpenseToCacheAsync(expenseCache); } catch (Exception ex) { _logger.LogError(ex, "Error occurd while storing expense related table in cahce"); return null; } return expenseCache; } public async Task AddExpensesListToCache(List expenses, Guid tenantId) { var expensesCache = await GetAllExpnesRelatedTablesForList(expenses, tenantId); try { await _expenseCache.AddExpensesListToCacheAsync(expensesCache); } catch (Exception ex) { _logger.LogError(ex, "Error occured while saving the list of expenses to Cache"); } } public async Task<(int totalPages, long totalCount, List? expenseList)> GetExpenseListAsync(Guid tenantId, Guid loggedInEmployeeId, bool viewAll, bool viewSelf, int pageNumber, int pageSize, ExpensesFilter? filter, string? searchString) { try { var (totalPages, totalCount, expenseList) = await _expenseCache.GetExpenseListFromCacheAsync(tenantId, loggedInEmployeeId, viewAll, viewSelf, pageNumber, pageSize, filter, searchString); if (expenseList.Any()) { return (totalPages, totalCount, expenseList); } } catch (Exception ex) { _logger.LogError(ex, "Error occured while fetching the list of expenses to Cache"); } return (0, 0, null); } public async Task GetExpenseDetailsById(Guid id, Guid tenantId) { try { var response = await _expenseCache.GetExpenseDetailsByIdAsync(id, tenantId); if (response != null && response.Id != string.Empty) { return response; } } catch (Exception ex) { _logger.LogError(ex, "Error occured while fetching expense details from cache"); } return null; } public async Task ReplaceExpenseAsync(Expenses expense) { bool response = false; try { response = await _expenseCache.DeleteExpenseFromCacheAsync(expense.Id, expense.TenantId); } catch (Exception ex) { _logger.LogError(ex, "Error occured while deleting expense from cache"); } if (response) { await AddExpenseByObjectAsync(expense); } } public async Task DeleteExpenseAsync(Guid id, Guid tenantId) { try { var response = await _expenseCache.DeleteExpenseFromCacheAsync(id, tenantId); } catch (Exception ex) { _logger.LogError(ex, "Error occured while deleting expense from cache"); } } #endregion #region ======================================================= Report Cache ======================================================= public async Task?> GetProjectReportMail(bool IsSend) { try { var response = await _reportCache.GetProjectReportMailFromCache(IsSend); return response; } catch (Exception ex) { _logger.LogError(ex, "Error occured while fetching project report mail bodys"); return null; } } public async Task AddProjectReportMail(ProjectReportEmailMongoDB report) { try { await _reportCache.AddProjectReportMailToCache(report); } catch (Exception ex) { _logger.LogError(ex, "Error occured while adding project report mail bodys"); } } #endregion #region ======================================================= Helper Functions ======================================================= private async Task> GetAllExpnesRelatedTablesForList(List model, Guid tenantId) { List expenseList = new List(); var expenseIds = model.Select(m => m.Id).ToList(); var projectIds = model.Select(m => m.ProjectId).ToList(); var statusIds = model.Select(m => m.StatusId).ToList(); var expensesTypeIds = model.Select(m => m.ExpensesTypeId).ToList(); var paymentModeIds = model.Select(m => m.PaymentModeId).ToList(); var createdByIds = model.Select(m => m.CreatedById).ToList(); var reviewedByIds = model.Select(m => m.ReviewedById).ToList(); var approvedByIds = model.Select(m => m.ApprovedById).ToList(); var processedByIds = model.Select(m => m.ProcessedById).ToList(); var paidByIds = model.Select(m => m.PaidById).ToList(); var projectTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Projects.AsNoTracking().Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).ToListAsync(); }); var paidByTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => paidByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync(); }); var createdByTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => createdByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync(); }); var reviewedByTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => reviewedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync(); }); var approvedByTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => approvedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync(); }); var processedByTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().Where(e => processedByIds.Contains(e.Id) && e.TenantId == tenantId).ToListAsync(); }); var expenseTypeTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.ExpensesTypeMaster.AsNoTracking().Where(et => expensesTypeIds.Contains(et.Id) && et.TenantId == tenantId).ToListAsync(); }); var paymentModeTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.PaymentModeMatser.AsNoTracking().Where(pm => paymentModeIds.Contains(pm.Id) && pm.TenantId == tenantId).ToListAsync(); }); var statusMappingTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.ExpensesStatusMapping .Include(s => s.Status) .Include(s => s.NextStatus) .AsNoTracking() .Where(es => statusIds.Contains(es.StatusId) && es.Status != null) .GroupBy(s => s.StatusId) .Select(g => new { StatusId = g.Key, Status = g.Select(s => s.Status).FirstOrDefault(), NextStatus = g.Select(s => s.NextStatus).OrderBy(s => s!.Name).ToList() }).ToListAsync(); }); var statusTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.ExpensesStatusMaster .AsNoTracking() .Where(es => statusIds.Contains(es.Id)) .ToListAsync(); }); var billAttachmentsTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.BillAttachments .Include(ba => ba.Document) .AsNoTracking() .Where(ba => expenseIds.Contains(ba.ExpensesId) && ba.Document != null) .GroupBy(ba => ba.ExpensesId) .Select(g => new { ExpensesId = g.Key, Documents = g.Select(ba => new DocumentMongoDB { DocumentId = ba.Document!.Id.ToString(), FileName = ba.Document.FileName, ContentType = ba.Document.ContentType, S3Key = ba.Document.S3Key, ThumbS3Key = ba.Document.ThumbS3Key ?? ba.Document.S3Key }).ToList() }) .ToListAsync(); }); // Await all prerequisite checks at once. await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask, processedByTask, statusTask, billAttachmentsTask); var projects = projectTask.Result; var expenseTypes = expenseTypeTask.Result; var paymentModes = paymentModeTask.Result; var statusMappings = statusMappingTask.Result; var paidBys = paidByTask.Result; var createdBys = createdByTask.Result; var reviewedBys = reviewedByTask.Result; var approvedBys = approvedByTask.Result; var processedBy = processedByTask.Result; var billAttachments = billAttachmentsTask.Result; expenseList = model.Select(m => { var response = _mapper.Map(m); response.Project = projects.Where(p => p.Id == m.ProjectId).Select(p => _mapper.Map(p)).FirstOrDefault() ?? new ProjectBasicMongoDB(); response.PaidBy = paidBys.Where(p => p.Id == m.PaidById).Select(p => _mapper.Map(p)).FirstOrDefault() ?? new BasicEmployeeMongoDB(); response.CreatedBy = createdBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault() ?? new BasicEmployeeMongoDB(); response.ReviewedBy = reviewedBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault(); response.ApprovedBy = approvedBys.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault(); response.ProcessedBy = processedBy.Where(e => e.Id == m.CreatedById).Select(e => _mapper.Map(e)).FirstOrDefault(); response.Status = statusMappings.Where(s => s.StatusId == m.StatusId).Select(s => _mapper.Map(s.Status)).FirstOrDefault() ?? new ExpensesStatusMasterMongoDB(); if (response.Status.Id == string.Empty) { var status = statusTask.Result; response.Status = status.Where(s => s.Id == m.StatusId).Select(s => _mapper.Map(s)).FirstOrDefault() ?? new ExpensesStatusMasterMongoDB(); } response.NextStatus = statusMappings.Where(s => s.StatusId == m.StatusId).Select(s => _mapper.Map>(s.NextStatus)).FirstOrDefault() ?? new List(); response.PaymentMode = paymentModes.Where(pm => pm.Id == m.PaymentModeId).Select(pm => _mapper.Map(pm)).FirstOrDefault() ?? new PaymentModeMatserMongoDB(); response.ExpensesType = expenseTypes.Where(et => et.Id == m.ExpensesTypeId).Select(et => _mapper.Map(et)).FirstOrDefault() ?? new ExpensesTypeMasterMongoDB(); response.Documents = billAttachments.Where(ba => ba.ExpensesId == m.Id).Select(ba => ba.Documents).FirstOrDefault() ?? new List(); return response; }).ToList(); return expenseList; } private async Task GetAllExpnesRelatedTablesForSingle(Expenses model, Guid tenantId) { var projectTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == model.ProjectId && p.TenantId == tenantId); }); var paidByTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.PaidById && e.TenantId == tenantId); }); var createdByTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.CreatedById && e.TenantId == tenantId); }); var reviewedByTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ReviewedById && e.TenantId == tenantId); }); var approvedByTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ApprovedById && e.TenantId == tenantId); }); var processedByTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.Employees.Include(e => e.JobRole).AsNoTracking().FirstOrDefaultAsync(e => e.Id == model.ProcessedById && e.TenantId == tenantId); }); var expenseTypeTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.ExpensesTypeMaster.AsNoTracking().FirstOrDefaultAsync(et => et.Id == model.ExpensesTypeId && et.TenantId == tenantId); }); var paymentModeTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.PaymentModeMatser.AsNoTracking().FirstOrDefaultAsync(pm => pm.Id == model.PaymentModeId && pm.TenantId == tenantId); }); var statusMappingTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.ExpensesStatusMapping .Include(s => s.Status) .Include(s => s.NextStatus) .AsNoTracking() .Where(es => es.StatusId == model.StatusId && es.Status != null) .GroupBy(s => s.StatusId) .Select(g => new { StatusId = g.Key, Status = g.Select(s => s.Status).FirstOrDefault(), NextStatus = g.Select(s => s.NextStatus).OrderBy(s => s!.Name).ToList() }).FirstOrDefaultAsync(); }); var statusTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.ExpensesStatusMaster .AsNoTracking() .FirstOrDefaultAsync(es => es.Id == model.StatusId); }); var billAttachmentsTask = Task.Run(async () => { await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); return await dbContext.BillAttachments .Include(ba => ba.Document) .AsNoTracking() .Where(ba => ba.ExpensesId == model.Id && ba.Document != null) .GroupBy(ba => ba.ExpensesId) .Select(g => new { ExpensesId = g.Key, Documents = g.Select(ba => new DocumentMongoDB { DocumentId = ba.Document!.Id.ToString(), FileName = ba.Document.FileName, ContentType = ba.Document.ContentType, S3Key = ba.Document.S3Key, ThumbS3Key = ba.Document.ThumbS3Key ?? ba.Document.S3Key }).ToList() }) .FirstOrDefaultAsync(); }); // Await all prerequisite checks at once. await Task.WhenAll(projectTask, expenseTypeTask, paymentModeTask, statusMappingTask, paidByTask, createdByTask, reviewedByTask, approvedByTask, processedByTask, statusTask, billAttachmentsTask); var project = projectTask.Result; var expenseType = expenseTypeTask.Result; var paymentMode = paymentModeTask.Result; var statusMapping = statusMappingTask.Result; var paidBy = paidByTask.Result; var createdBy = createdByTask.Result; var reviewedBy = reviewedByTask.Result; var approvedBy = approvedByTask.Result; var processedBy = processedByTask.Result; var billAttachment = billAttachmentsTask.Result; var response = _mapper.Map(model); response.Project = _mapper.Map(project); response.PaidBy = _mapper.Map(paidBy); response.CreatedBy = _mapper.Map(createdBy); response.ReviewedBy = _mapper.Map(reviewedBy); response.ApprovedBy = _mapper.Map(approvedBy); response.ProcessedBy = _mapper.Map(processedBy); if (statusMapping != null) { response.Status = _mapper.Map(statusMapping.Status); response.NextStatus = _mapper.Map>(statusMapping.NextStatus); } if (response.Status == null) { var status = statusTask.Result; response.Status = _mapper.Map(status); } response.PaymentMode = _mapper.Map(paymentMode); response.ExpensesType = _mapper.Map(expenseType); if (billAttachment != null) response.Documents = billAttachment.Documents; return response; } #endregion } }