using Marco.Pms.Helpers; using Marco.Pms.Helpers.CacheHelper; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels.Masters; using Marco.Pms.Model.MongoDBModels.Project; using Marco.Pms.Model.MongoDBModels.Utility; using Marco.Pms.Model.Projects; using MarcoBMS.Services.Service; using Microsoft.EntityFrameworkCore; using Project = Marco.Pms.Model.Projects.Project; namespace Marco.Pms.Services.Helpers { public class CacheUpdateHelper { private readonly ProjectCache _projectCache; private readonly EmployeeCache _employeeCache; private readonly ReportCache _reportCache; private readonly ILoggingService _logger; private readonly IDbContextFactory _dbContextFactory; private readonly ApplicationDbContext _context; private readonly GeneralHelper _generalHelper; public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ILoggingService logger, IDbContextFactory dbContextFactory, ApplicationDbContext context, GeneralHelper generalHelper) { _projectCache = projectCache; _employeeCache = employeeCache; _reportCache = reportCache; _logger = logger; _dbContextFactory = dbContextFactory; _context = context; _generalHelper = generalHelper; } // ------------------------------------ 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 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); // Get the results from the completed tasks. var status = await statusTask; var teamSize = await teamSizeTask; var (allBuildings, allFloors, allWorkAreas, workSummariesByWorkAreaId) = await infrastructureTask; // --- 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; 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(); // --- 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 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); // --- Step 2: Process the fetched data and build the MongoDB models --- var allStatuses = await statusTask; var teamSizesByProjectId = await teamSizeTask; var allWorkAreas = await workAreasTask; var workSummariesByWorkAreaId = await workSummaryTask; // 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; 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) { StatusMaster projectStatus = await _context.StatusMasters .FirstOrDefaultAsync(s => s.Id == project.ProjectStatusId) ?? new StatusMaster(); try { bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project, projectStatus); 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"); } } // ------------------------------------ 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; } } // ------------------------------------------------------- WorkItem ------------------------------------------------------- public async Task?> GetWorkItemsByWorkAreaIds(List workAreaIds) { try { var response = await _projectCache.GetWorkItemsByWorkAreaIdsFromCache(workAreaIds); 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) { try { var workItems = await _projectCache.GetWorkItemDetailsByWorkAreaFromCache(workAreaId); 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); } } // ------------------------------------ Employee Profile Cache --------------------------------------- public async Task AddApplicationRole(Guid employeeId, List roleIds) { // 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); } 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) { try { var response = await _employeeCache.AddProjectsToCache(employeeId, projectIds); 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) { try { var response = await _employeeCache.GetProjectsFromCache(employeeId); 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) { try { var response = await _employeeCache.GetPermissionsFromCache(employeeId); 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) { try { var response = await _employeeCache.ClearAllProjectIdsFromCache(employeeId); } 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) { try { await _employeeCache.ClearAllProjectIdsByPermissionIdFromCache(permissionId); } 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) { try { var response = await _employeeCache.ClearAllPermissionIdsByEmployeeIDFromCache(employeeId); } 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) { try { var response = await _employeeCache.RemoveRoleIdFromCache(employeeId, roleId); } 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 ClearAllEmployees() { try { var response = await _employeeCache.ClearAllEmployeesFromCache(); } catch (Exception ex) { _logger.LogError(ex, "Error occured while deleting all employees from Cache"); } } // ------------------------------------ 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"); } } } }