769 lines
32 KiB
C#

using Marco.Pms.CacheHelper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.MongoDBModels;
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<ApplicationDbContext> _dbContextFactory;
public CacheUpdateHelper(ProjectCache projectCache, EmployeeCache employeeCache, ReportCache reportCache, ILoggingService logger,
IDbContextFactory<ApplicationDbContext> dbContextFactory)
{
_projectCache = projectCache;
_employeeCache = employeeCache;
_reportCache = reportCache;
_logger = logger;
_dbContextFactory = dbContextFactory;
}
// ------------------------------------ Project Details Cache ---------------------------------------
// Assuming you have access to an IDbContextFactory<YourDbContext> as _dbContextFactory
// This is crucial for safe parallel database operations.
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<BuildingMongoDB>();
foreach (var building in allBuildings)
{
double buildingPlanned = 0, buildingCompleted = 0;
var floorMongoList = new List<FloorMongoDB>();
foreach (var floor in floorsByBuildingId[building.Id]) // Fast lookup
{
double floorPlanned = 0, floorCompleted = 0;
var workAreaMongoList = new List<WorkAreaMongoDB>();
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.LogWarning("Error occurred while adding project {ProjectId} to Cache: {Error}", project.Id, ex.Message);
}
}
public async Task AddProjectDetailsList(List<Project> 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<ProjectMongoDB>(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<BuildingMongoDB>();
foreach (var building in buildingsByProjectId[project.Id])
{
double buildingPlanned = 0, buildingCompleted = 0;
var floorMongoList = new List<FloorMongoDB>();
foreach (var floor in floorsByBuildingId[building.Id])
{
double floorPlanned = 0, floorCompleted = 0;
var workAreaMongoList = new List<WorkAreaMongoDB>();
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.LogWarning("Error occurred while adding project list to Cache: {Error}", ex.Message);
}
}
public async Task<bool> UpdateProjectDetailsOnly(Project project)
{
try
{
bool response = await _projectCache.UpdateProjectDetailsOnlyToCache(project);
return response;
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while updating project {ProjectId} to Cache: {Error}", project.Id, ex.Message);
return false;
}
}
public async Task<ProjectMongoDB?> GetProjectDetails(Guid projectId)
{
try
{
var response = await _projectCache.GetProjectDetailsFromCache(projectId);
return response;
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while getting project {ProjectId} to Cache: {Error}", ex.Message);
return null;
}
}
public async Task<List<ProjectMongoDB>?> GetProjectDetailsList(List<Guid> projectIds)
{
try
{
var response = await _projectCache.GetProjectDetailsListFromCache(projectIds);
if (response.Any())
{
return response;
}
else
{
return null;
}
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while getting list od project details from to Cache: {Error}", ex.Message);
return null;
}
}
// ------------------------------------ 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<List<BuildingMongoDB>?> 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<WorkAreaInfoMongoDB?> 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;
}
}
public async Task<List<WorkItemMongoDB>?> GetWorkItemsByWorkAreaIds(List<Guid> 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;
}
}
// ------------------------------------------------------- WorkItem -------------------------------------------------------
public async Task ManageWorkItemDetails(List<WorkItem> workItems)
{
try
{
await _projectCache.ManageWorkItemDetailsToCache(workItems);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while saving workItems form Cache: {Error}", ex.Message);
}
}
public async Task<List<WorkItemMongoDB>?> 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<WorkItemMongoDB?> 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);
}
}
// ------------------------------------ Employee Profile Cache ---------------------------------------
public async Task AddApplicationRole(Guid employeeId, List<Guid> roleIds)
{
try
{
var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds);
}
catch (Exception ex)
{
_logger.LogWarning("Error occured while adding Application roleIds to Cache to employee {Employee}: {Error}", employeeId, ex.Message);
}
}
public async Task<bool> AddProjects(Guid employeeId, List<Guid> 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<List<Guid>?> 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<List<Guid>?> 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);
}
}
// ------------------------------------ Report Cache ---------------------------------------
public async Task<List<ProjectReportEmailMongoDB>?> GetProjectReportMail(bool IsSend)
{
try
{
var response = await _reportCache.GetProjectReportMailFromCache(IsSend);
return response;
}
catch (Exception ex)
{
_logger.LogError("Error occured while fetching project report mail bodys: {Error}", ex.Message);
return null;
}
}
public async Task AddProjectReportMail(ProjectReportEmailMongoDB report)
{
try
{
await _reportCache.AddProjectReportMailToCache(report);
}
catch (Exception ex)
{
_logger.LogError("Error occured while adding project report mail bodys: {Error}", ex.Message);
}
}
}
}