Optimization of WorkItem Delete API in Project Controller

This commit is contained in:
ashutosh.nehete 2025-07-16 18:39:29 +05:30
parent 3f7925aa72
commit 089ae7e9e5
3 changed files with 90 additions and 363 deletions

View File

@ -1,7 +1,6 @@
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Project;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Service;
@ -11,7 +10,6 @@ using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using MongoDB.Driver;
namespace MarcoBMS.Services.Controllers
@ -410,55 +408,24 @@ namespace MarcoBMS.Services.Controllers
[HttpDelete("task/{id}")]
public async Task<IActionResult> DeleteProjectTask(Guid id)
{
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
List<Guid> workAreaIds = new List<Guid>();
WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId);
if (task != null)
// --- Step 1: Input Validation ---
if (!ModelState.IsValid)
{
if (task.CompletedWork == 0)
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
_logger.LogWarning("project Alocation called with invalid model state for list of projects. Errors: {Errors}", string.Join(", ", errors));
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request data provided.", errors, 400));
}
// --- Step 2: Prepare data without I/O ---
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var serviceResponse = await _projectServices.DeleteProjectTaskAsync(id, tenantId, loggedInEmployee);
var response = serviceResponse.Response;
var notification = serviceResponse.Notification;
if (notification != null)
{
var assignedTask = await _context.TaskAllocations.Where(t => t.WorkItemId == id).ToListAsync();
if (assignedTask.Count == 0)
{
_context.WorkItems.Remove(task);
await _context.SaveChangesAsync();
_logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id);
var floorId = task.WorkArea?.FloorId;
var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId);
workAreaIds.Add(task.WorkAreaId);
var projectId = floor?.Building?.ProjectId;
var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}" };
await _signalR.SendNotificationAsync(notification);
await _cache.DeleteWorkItemByIdAsync(task.Id);
if (projectId != null)
{
await _cache.DeleteProjectByIdAsync(projectId.Value);
}
}
else
{
_logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Task is currently assigned and cannot be deleted.", "Task is currently assigned and cannot be deleted.", 400));
}
}
else
{
double percentage = (task.CompletedWork / task.PlannedWork) * 100;
percentage = Math.Round(percentage, 2);
_logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted", task.Id, percentage);
return BadRequest(ApiResponse<object>.ErrorResponse(System.String.Format("Task is {0}% complete and cannot be deleted", percentage), System.String.Format("Task is {0}% complete and cannot be deleted", percentage), 400));
}
}
else
{
_logger.LogWarning("Task with ID {WorkItemId} not found ID database", id);
}
return Ok(ApiResponse<object>.SuccessResponse(new { }, "Task deleted successfully", 200));
return StatusCode(response.StatusCode, response);
}
#endregion

View File

@ -1033,130 +1033,6 @@ namespace Marco.Pms.Services.Service
#region =================================================================== Project Infrastructre Manage APIs ===================================================================
public async Task<ApiResponse<object>> ManageProjectInfra(List<InfraDto> infraDots, Guid tenantId, Employee loggedInEmployee)
{
var responseData = new InfraVM { };
string responseMessage = "";
string message = "";
List<Guid> projectIds = new List<Guid>();
if (infraDots != null)
{
foreach (var item in infraDots)
{
if (item.Building != null)
{
Building building = _mapper.Map<Building>(item.Building);
building.TenantId = tenantId;
if (item.Building.Id == null)
{
//create
_context.Buildings.Add(building);
await _context.SaveChangesAsync();
responseData.building = building;
responseMessage = "Buliding Added Successfully";
message = "Building Added";
await _cache.AddBuildngInfra(building.ProjectId, building);
}
else
{
//update
_context.Buildings.Update(building);
await _context.SaveChangesAsync();
responseData.building = building;
responseMessage = "Buliding Updated Successfully";
message = "Building Updated";
await _cache.UpdateBuildngInfra(building.ProjectId, building);
}
projectIds.Add(building.ProjectId);
}
if (item.Floor != null)
{
Floor floor = _mapper.Map<Floor>(item.Floor);
floor.TenantId = tenantId;
bool isCreated = false;
if (item.Floor.Id == null)
{
//create
_context.Floor.Add(floor);
await _context.SaveChangesAsync();
responseData.floor = floor;
responseMessage = "Floor Added Successfully";
message = "Floor Added";
isCreated = true;
}
else
{
//update
_context.Floor.Update(floor);
await _context.SaveChangesAsync();
responseData.floor = floor;
responseMessage = "Floor Updated Successfully";
message = "Floor Updated";
}
Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId);
var projectId = building?.ProjectId ?? Guid.Empty;
projectIds.Add(projectId);
message = $"{message} in Building: {building?.Name}";
if (isCreated)
{
await _cache.AddBuildngInfra(projectId, floor: floor);
}
else
{
await _cache.UpdateBuildngInfra(projectId, floor: floor);
}
}
if (item.WorkArea != null)
{
WorkArea workArea = _mapper.Map<WorkArea>(item.WorkArea);
workArea.TenantId = tenantId;
bool isCreated = false;
if (item.WorkArea.Id == null)
{
//create
_context.WorkAreas.Add(workArea);
await _context.SaveChangesAsync();
responseData.workArea = workArea;
responseMessage = "Work Area Added Successfully";
message = "Work Area Added";
isCreated = true;
}
else
{
//update
_context.WorkAreas.Update(workArea);
await _context.SaveChangesAsync();
responseData.workArea = workArea;
responseMessage = "Work Area Updated Successfully";
message = "Work Area Updated";
}
Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId);
var projectId = floor?.Building?.ProjectId ?? Guid.Empty;
projectIds.Add(projectId);
message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}";
if (isCreated)
{
await _cache.AddBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId);
}
else
{
await _cache.UpdateBuildngInfra(projectId, workArea: workArea, buildingId: floor?.BuildingId);
}
}
}
message = $"{message} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}";
var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message };
return ApiResponse<object>.SuccessResponse(responseData, responseMessage, 200);
}
return ApiResponse<object>.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400);
}
public async Task<ServiceResponse> ManageProjectInfraAsync(List<InfraDto> infraDtos, Guid tenantId, Employee loggedInEmployee)
{
// 1. Guard Clause: Handle null or empty input gracefully.
@ -1244,151 +1120,6 @@ namespace Marco.Pms.Services.Service
};
}
/// <summary>
/// Manages a batch of infrastructure changes (creates/updates for Buildings, Floors, and WorkAreas).
/// This method is optimized to perform all database operations in a single, atomic transaction.
/// </summary>
public async Task<ApiResponse<object>> ManageProjectInfraAsync1(List<InfraDto> infraDtos, Guid tenantId, Employee loggedInEmployee)
{
// --- Step 1: Input Validation ---
if (infraDtos == null || !infraDtos.Any())
{
_logger.LogWarning("ManageProjectInfraAsync called with null or empty DTO list.");
return ApiResponse<object>.ErrorResponse("Invalid details.", "Infrastructure data cannot be empty.", 400);
}
_logger.LogInfo("Begin ManageProjectInfraAsync for {DtoCount} items, TenantId: {TenantId}, User: {UserId}", infraDtos.Count, tenantId, loggedInEmployee.Id);
// --- Step 2: Categorize DTOs by Type and Action ---
var buildingsToCreateDto = infraDtos.Where(i => i.Building != null && i.Building.Id == null).Select(i => i.Building!).ToList();
var buildingsToUpdateDto = infraDtos.Where(i => i.Building != null && i.Building.Id != null).Select(i => i.Building!).ToList();
var floorsToCreateDto = infraDtos.Where(i => i.Floor != null && i.Floor.Id == null).Select(i => i.Floor!).ToList();
var floorsToUpdateDto = infraDtos.Where(i => i.Floor != null && i.Floor.Id != null).Select(i => i.Floor!).ToList();
var workAreasToCreateDto = infraDtos.Where(i => i.WorkArea != null && i.WorkArea.Id == null).Select(i => i.WorkArea!).ToList();
var workAreasToUpdateDto = infraDtos.Where(i => i.WorkArea != null && i.WorkArea.Id != null).Select(i => i.WorkArea!).ToList();
_logger.LogDebug("Categorized DTOs...");
try
{
// --- Step 3: Fetch all required existing data in bulk ---
// Fetch existing entities to be updated
var buildingIdsToUpdate = buildingsToUpdateDto.Select(d => d.Id!.Value).ToList();
var existingBuildings = await _context.Buildings.Where(b => buildingIdsToUpdate.Contains(b.Id) && b.TenantId == tenantId).ToDictionaryAsync(b => b.Id);
var floorIdsToUpdate = floorsToUpdateDto.Select(d => d.Id!.Value).ToList();
var existingFloors = await _context.Floor.Include(f => f.Building).Where(f => floorIdsToUpdate.Contains(f.Id) && f.TenantId == tenantId).ToDictionaryAsync(f => f.Id);
var workAreaIdsToUpdate = workAreasToUpdateDto.Select(d => d.Id!.Value).ToList();
var existingWorkAreas = await _context.WorkAreas.Include(wa => wa.Floor!.Building).Where(wa => workAreaIdsToUpdate.Contains(wa.Id) && wa.TenantId == tenantId).ToDictionaryAsync(wa => wa.Id);
// Fetch parent entities for items being created to get their ProjectIds
var buildingIdsForNewFloors = floorsToCreateDto.Select(f => f.BuildingId).ToList();
var parentBuildingsForNewFloors = await _context.Buildings.Where(b => buildingIdsForNewFloors.Contains(b.Id)).ToDictionaryAsync(b => b.Id);
var floorIdsForNewWorkAreas = workAreasToCreateDto.Select(wa => wa.FloorId).ToList();
var parentFloorsForNewWorkAreas = await _context.Floor.Include(f => f.Building).Where(f => floorIdsForNewWorkAreas.Contains(f.Id)).ToDictionaryAsync(f => f.Id);
_logger.LogInfo("Fetched existing entities and parents for new items.");
// --- Step 4: Aggregate all affected ProjectIds for Security Check ---
var affectedProjectIds = new HashSet<Guid>();
// From buildings being created/updated
buildingsToCreateDto.ForEach(b => affectedProjectIds.Add(b.ProjectId));
foreach (var b in existingBuildings.Values) { affectedProjectIds.Add(b.ProjectId); }
// From floors being created/updated
foreach (var f in floorsToCreateDto) { if (parentBuildingsForNewFloors.TryGetValue(f.BuildingId, out var b)) affectedProjectIds.Add(b.ProjectId); }
foreach (var f in existingFloors.Values) { if (f.Building != null) affectedProjectIds.Add(f.Building.ProjectId); }
// From work areas being created/updated
foreach (var wa in workAreasToCreateDto) { if (parentFloorsForNewWorkAreas.TryGetValue(wa.FloorId, out var f) && f.Building != null) affectedProjectIds.Add(f.Building.ProjectId); }
foreach (var wa in existingWorkAreas.Values) { if (wa.Floor?.Building != null) affectedProjectIds.Add(wa.Floor.Building.ProjectId); }
// Security Check against the complete list of affected projects
var hasPermission = await _permission.HasPermission(PermissionsMaster.ManageProjectInfra, loggedInEmployee.Id);
if (!hasPermission)
{
_logger.LogWarning("Access DENIED for user {UserId} trying to manage infrastructure for projects.", loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to manage infrastructure for one or more of the specified projects.", 403);
}
// --- Step 5: Process all logic IN MEMORY, tracking changes ---
// Process Buildings
var createdBuildings = new List<Building>();
foreach (var dto in buildingsToCreateDto)
{
var newBuilding = _mapper.Map<Building>(dto);
newBuilding.TenantId = tenantId;
createdBuildings.Add(newBuilding);
}
foreach (var dto in buildingsToUpdateDto) { if (existingBuildings.TryGetValue(dto.Id!.Value, out var b)) _mapper.Map(dto, b); }
// Process Floors
var createdFloors = new List<Floor>();
foreach (var dto in floorsToCreateDto)
{
var newFloor = _mapper.Map<Floor>(dto);
newFloor.TenantId = tenantId;
createdFloors.Add(newFloor);
}
foreach (var dto in floorsToUpdateDto) { if (existingFloors.TryGetValue(dto.Id!.Value, out var f)) _mapper.Map(dto, f); }
// Process WorkAreas
var createdWorkAreas = new List<WorkArea>();
foreach (var dto in workAreasToCreateDto)
{
var newWorkArea = _mapper.Map<WorkArea>(dto);
newWorkArea.TenantId = tenantId;
createdWorkAreas.Add(newWorkArea);
}
foreach (var dto in workAreasToUpdateDto) { if (existingWorkAreas.TryGetValue(dto.Id!.Value, out var wa)) _mapper.Map(dto, wa); }
// --- Step 6: Save all database changes in a SINGLE TRANSACTION ---
if (createdBuildings.Any()) _context.Buildings.AddRange(createdBuildings);
if (createdFloors.Any()) _context.Floor.AddRange(createdFloors);
if (createdWorkAreas.Any()) _context.WorkAreas.AddRange(createdWorkAreas);
if (_context.ChangeTracker.HasChanges())
{
await _context.SaveChangesAsync();
_logger.LogInfo("Database save successful.");
}
// --- Step 7: Update Cache using the aggregated ProjectIds (Non-blocking) ---
var finalProjectIds = affectedProjectIds.ToList();
if (finalProjectIds.Any())
{
_ = Task.Run(async () =>
{
try
{
_logger.LogInfo("Queuing background cache update for {ProjectCount} projects.", finalProjectIds.Count);
// Assuming your cache service has a method to handle this.
await _cache.RemoveProjectsAsync(finalProjectIds);
_logger.LogInfo("Background cache update task completed for projects: {ProjectIds}", string.Join(", ", finalProjectIds));
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred during the background cache update task for projects: {ProjectIds}", string.Join(", ", finalProjectIds));
}
});
}
// --- Step 8: Prepare and return a clear response ---
var responseVm = new { /* ... as before ... */ };
return ApiResponse<object>.SuccessResponse(responseVm, "Infrastructure changes processed successfully.", 200);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unexpected error occurred in ManageProjectInfraAsync.");
return ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred.", 500);
}
}
/// <summary>
/// Creates or updates a batch of work items.
/// This method is optimized to perform all database operations in a single, atomic transaction.
@ -1512,60 +1243,88 @@ namespace Marco.Pms.Services.Service
return ApiResponse<List<WorkItemVM>>.SuccessResponse(responseList, message, 200);
}
public async Task<ServiceResponse> DeleteProjectTaskAsync(Guid id, Guid tenantId, Employee loggedInEmployee)
{
// 1. Fetch the task and its parent data in a single query.
// This is still a major optimization, avoiding a separate query for the floor/building.
WorkItem? task = await _context.WorkItems
.AsNoTracking() // Use AsNoTracking because we will re-attach for deletion later.
.Include(t => t.WorkArea)
.ThenInclude(wa => wa!.Floor)
.ThenInclude(f => f!.Building)
.FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId);
//public async Task<IActionResult> DeleteProjectTask(Guid id)
//{
// var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// List<Guid> workAreaIds = new List<Guid>();
// WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId);
// if (task != null)
// {
// if (task.CompletedWork == 0)
// {
// var assignedTask = await _context.TaskAllocations.Where(t => t.WorkItemId == id).ToListAsync();
// if (assignedTask.Count == 0)
// {
// _context.WorkItems.Remove(task);
// await _context.SaveChangesAsync();
// _logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id);
// 2. Guard Clause: Handle non-existent task.
if (task == null)
{
_logger.LogWarning("Attempted to delete a non-existent task with ID {WorkItemId}", id);
return new ServiceResponse
{
Response = ApiResponse<object>.ErrorResponse("Task not found.", $"A task with ID {id} was not found.", 404)
};
}
// var floorId = task.WorkArea?.FloorId;
// var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId);
// 3. Guard Clause: Prevent deletion if work has started.
if (task.CompletedWork > 0)
{
double percentage = Math.Round((task.CompletedWork / task.PlannedWork) * 100, 2);
_logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted.", task.Id, percentage);
return new ServiceResponse
{
Response = ApiResponse<object>.ErrorResponse($"Task is {percentage}% complete and cannot be deleted.", "Deletion failed because the task has progress.", 400)
};
}
// 4. Guard Clause: Efficiently check if the task is assigned in a separate, optimized query.
// AnyAsync() is highly efficient and translates to a `SELECT TOP 1` or `EXISTS` in SQL.
bool isAssigned = await _context.TaskAllocations.AnyAsync(t => t.WorkItemId == id);
if (isAssigned)
{
_logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id);
return new ServiceResponse
{
Response = ApiResponse<object>.ErrorResponse("Task is currently assigned and cannot be deleted.", "Deletion failed because the task is assigned to an employee.", 400)
};
}
// workAreaIds.Add(task.WorkAreaId);
// var projectId = floor?.Building?.ProjectId;
// --- Success Path: All checks passed, proceed with deletion ---
// var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "WorkItem", WorkAreaIds = workAreaIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}" };
// await _signalR.SendNotificationAsync(notification);
// await _cache.DeleteWorkItemByIdAsync(task.Id);
// if (projectId != null)
// {
// await _cache.DeleteProjectByIdAsync(projectId.Value);
// }
// }
// else
// {
// _logger.LogWarning("Task with ID {WorkItemId} is currently assigned and cannot be deleted.", task.Id);
// return BadRequest(ApiResponse<object>.ErrorResponse("Task is currently assigned and cannot be deleted.", "Task is currently assigned and cannot be deleted.", 400));
// }
// }
// else
// {
// double percentage = (task.CompletedWork / task.PlannedWork) * 100;
// percentage = Math.Round(percentage, 2);
// _logger.LogWarning("Task with ID {WorkItemId} is {CompletionPercentage}% complete and cannot be deleted", task.Id, percentage);
// return BadRequest(ApiResponse<object>.ErrorResponse(System.String.Format("Task is {0}% complete and cannot be deleted", percentage), System.String.Format("Task is {0}% complete and cannot be deleted", percentage), 400));
var building = task.WorkArea?.Floor?.Building;
var notification = new
{
LoggedInUserId = loggedInEmployee.Id,
Keyword = "WorkItem",
WorkAreaIds = new[] { task.WorkAreaId },
Message = $"Task Deleted in Building: {building?.Name ?? "N/A"}, on Floor: {task.WorkArea?.Floor?.FloorName ?? "N/A"}, in Area: {task.WorkArea?.AreaName ?? "N/A"} by {loggedInEmployee.FirstName} {loggedInEmployee.LastName}"
};
// }
// }
// else
// {
// _logger.LogWarning("Task with ID {WorkItemId} not found ID database", id);
// }
// return Ok(ApiResponse<object>.SuccessResponse(new { }, "Task deleted successfully", 200));
//}
// 5. Perform the database deletion.
// We must attach a new instance or the original one without AsNoTracking.
// Since we used AsNoTracking, we create a 'stub' entity for deletion.
// This is more efficient than re-querying.
_context.WorkItems.Remove(new WorkItem { Id = task.Id });
await _context.SaveChangesAsync();
_logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id);
// 6. Perform cache operations concurrently.
var cacheTasks = new List<Task>
{
_cache.DeleteWorkItemByIdAsync(task.Id)
};
if (building?.ProjectId != null)
{
cacheTasks.Add(_cache.DeleteProjectByIdAsync(building.ProjectId));
}
await Task.WhenAll(cacheTasks);
// 7. Return the final success response.
return new ServiceResponse
{
Notification = notification,
Response = ApiResponse<object>.SuccessResponse(new { id = task.Id }, "Task deleted successfully.", 200)
};
}
#endregion
#region =================================================================== Helper Functions ===================================================================

View File

@ -23,6 +23,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> GetWorkItemsAsync(Guid workAreaId, Guid tenantId, Employee loggedInEmployee);
Task<ServiceResponse> ManageProjectInfraAsync(List<InfraDto> infraDtos, Guid tenantId, Employee loggedInEmployee);
Task<ApiResponse<List<WorkItemVM>>> CreateProjectTaskAsync(List<WorkItemDto> workItemDtos, Guid tenantId, Employee loggedInEmployee);
Task<ServiceResponse> DeleteProjectTaskAsync(Guid id, Guid tenantId, Employee loggedInEmployee);
}
}