From 36eb7aef7fcc6dd6f209f383c330bef1415366b8 Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 14 Jul 2025 17:00:28 +0530 Subject: [PATCH] Optimized the Update project API --- .../Controllers/ProjectController.cs | 168 ++++++++++++++---- Marco.Pms.Services/Helpers/ProjectsHelper.cs | 6 +- .../MappingProfiles/ProjectMappingProfile.cs | 3 + 3 files changed, 142 insertions(+), 35 deletions(-) diff --git a/Marco.Pms.Services/Controllers/ProjectController.cs b/Marco.Pms.Services/Controllers/ProjectController.cs index acc97d2..3d5558f 100644 --- a/Marco.Pms.Services/Controllers/ProjectController.cs +++ b/Marco.Pms.Services/Controllers/ProjectController.cs @@ -70,7 +70,6 @@ namespace MarcoBMS.Services.Controllers _logger.LogInfo("Basic project list requested by EmployeeId {EmployeeId}", loggedInEmployee.Id); // Step 2: Get the list of project IDs the user has access to - Guid tenantId = _userHelper.GetTenantId(); // Assuming this is still needed by the helper List accessibleProjectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee); if (accessibleProjectIds == null || !accessibleProjectIds.Any()) @@ -316,7 +315,7 @@ namespace MarcoBMS.Services.Controllers } - var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); + var project = await _context.Projects.Where(c => c.TenantId == tenantId && c.Id == id).Include(c => c.ProjectStatus).SingleOrDefaultAsync(); // includeProperties: "ProjectStatus,Tenant"); //_context.Stock.FindAsync(id); if (project == null) { @@ -420,7 +419,6 @@ namespace MarcoBMS.Services.Controllers } // 2. Prepare data without I/O - Guid tenantId = _userHelper.GetTenantId(); // Assuming this is fast and from claims Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInUserId = loggedInEmployee.Id; var project = projectDto.ToProjectFromCreateProjectDto(tenantId); @@ -465,7 +463,7 @@ namespace MarcoBMS.Services.Controllers } [HttpPut] - [Route("update/{id}")] + [Route("update1/{id}")] public async Task Update([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto) { var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); @@ -480,9 +478,7 @@ namespace MarcoBMS.Services.Controllers } try { - Guid TenantId = GetTenantId(); - - Project project = updateProjectDto.ToProjectFromUpdateProjectDto(TenantId, id); + Project project = updateProjectDto.ToProjectFromUpdateProjectDto(tenantId, id); _context.Projects.Update(project); await _context.SaveChangesAsync(); @@ -507,6 +503,97 @@ namespace MarcoBMS.Services.Controllers } } + /// + /// Updates an existing project's details. + /// This endpoint is secure, handles concurrency, and performs non-essential tasks in the background. + /// + /// The ID of the project to update. + /// The data to update the project with. + /// An ApiResponse confirming the update or an appropriate error. + + [HttpPut("update/{id}")] + public async Task UpdateProject([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto) + { + // --- Step 1: Input Validation --- + if (!ModelState.IsValid) + { + var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList(); + _logger.LogWarning("Update project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors)); + return BadRequest(ApiResponse.ErrorResponse("Invalid request data provided.", errors, 400)); + } + + try + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + // --- Step 2: Fetch the Existing Entity from the Database --- + // This is crucial to avoid the data loss bug. We only want to modify an existing record. + var existingProject = await _context.Projects + .Where(p => p.Id == id && p.TenantId == tenantId) + .SingleOrDefaultAsync(); + + // 2a. Existence Check + if (existingProject == null) + { + _logger.LogWarning("Attempt to update non-existent project with ID {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); + return NotFound(ApiResponse.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404)); + } + + // 2b. Security Check + var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, id); + if (!hasPermission) + { + _logger.LogWarning("Access DENIED for user {UserId} attempting to update project {ProjectId}.", loggedInEmployee.Id, id); + return StatusCode(403, (ApiResponse.ErrorResponse("Access Denied.", "You do not have permission to modify this project.", 403))); + } + + // --- Step 3: Apply Changes and Save --- + // Map the changes from the DTO onto the entity we just fetched from the database. + // This only modifies the properties defined in the mapping, preventing data loss. + _mapper.Map(updateProjectDto, existingProject); + + // Mark the entity as modified (if your mapping doesn't do it automatically). + _context.Entry(existingProject).State = EntityState.Modified; + + try + { + await _context.SaveChangesAsync(); + _logger.LogInfo("Successfully updated project {ProjectId} by user {UserId}.", id, loggedInEmployee.Id); + } + catch (DbUpdateConcurrencyException ex) + { + // --- Step 4: Handle Concurrency Conflicts --- + // This happens if another user modified the project after we fetched it. + _logger.LogWarning("Concurrency conflict while updating project {ProjectId} \n {Error}", id, ex.Message); + return Conflict(ApiResponse.ErrorResponse("Conflict occurred.", "This project has been modified by someone else. Please refresh and try again.", 409)); + } + + // --- Step 5: Perform Side-Effects in the Background (Fire and Forget) --- + // The core database operation is done. Now, we perform non-blocking cache and notification updates. + _ = Task.Run(async () => + { + // Create a DTO of the updated project to pass to background tasks. + var projectDto = _mapper.Map(existingProject); + + // 5a. Update Cache + await UpdateCacheInBackground(existingProject); + + // 5b. Send Targeted SignalR Notification + var notification = new { LoggedInUserId = loggedInEmployee.Id, Keyword = "Update_Project", Response = projectDto }; + await SendNotificationInBackground(notification, projectDto.Id); + }); + + // --- Step 6: Return Success Response Immediately --- + // The client gets a fast response without waiting for caching or SignalR. + return Ok(ApiResponse.SuccessResponse(_mapper.Map(existingProject), "Project updated successfully.", 200)); + } + catch (Exception ex) + { + // --- Step 7: Graceful Error Handling for Unexpected Errors --- + _logger.LogError("An unexpected error occurred while updating project {ProjectId} \n {Error}", id, ex.Message); + return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", null, 500)); + } + } + #endregion #region =================================================================== Project Allocation APIs =================================================================== @@ -524,7 +611,6 @@ namespace MarcoBMS.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } - Guid TenantId = GetTenantId(); if (projectid != null) { @@ -535,14 +621,14 @@ namespace MarcoBMS.Services.Controllers { result = await (from rpm in _context.Employees.Include(c => c.JobRole) - join fp in _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == projectid) + join fp in _context.ProjectAllocations.Where(c => c.TenantId == tenantId && c.ProjectId == projectid) on rpm.Id equals fp.EmployeeId select rpm).ToListAsync(); } else { result = await (from rpm in _context.Employees.Include(c => c.JobRole) - join fp in _context.ProjectAllocations.Where(c => c.TenantId == TenantId && c.ProjectId == projectid && c.IsActive == true) + join fp in _context.ProjectAllocations.Where(c => c.TenantId == tenantId && c.ProjectId == projectid && c.IsActive) on rpm.Id equals fp.EmployeeId select rpm).ToListAsync(); } @@ -577,11 +663,9 @@ namespace MarcoBMS.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid data", errors, 400)); } - Guid TenantId = GetTenantId(); - var employees = await _context.ProjectAllocations - .Where(c => c.TenantId == TenantId && c.ProjectId == projectId && c.Employee != null) + .Where(c => c.TenantId == tenantId && c.ProjectId == projectId && c.Employee != null) .Include(e => e.Employee) .Select(e => new { @@ -605,7 +689,6 @@ namespace MarcoBMS.Services.Controllers { if (projectAllocationDot != null) { - Guid TenentID = GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); List? result = new List(); @@ -616,11 +699,11 @@ namespace MarcoBMS.Services.Controllers { try { - ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(TenentID); + ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId); ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == projectAllocation.EmployeeId && c.ProjectId == projectAllocation.ProjectId && c.ReAllocationDate == null - && c.TenantId == TenentID).SingleOrDefaultAsync(); + && c.TenantId == tenantId).SingleOrDefaultAsync(); if (projectAllocationFromDb != null) { @@ -688,8 +771,6 @@ namespace MarcoBMS.Services.Controllers [HttpGet("assigned-projects/{employeeId}")] public async Task GetProjectsByEmployee([FromRoute] Guid employeeId) { - - Guid tenantId = _userHelper.GetTenantId(); if (employeeId == Guid.Empty) { return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Employee id not valid.", 400)); @@ -729,7 +810,6 @@ namespace MarcoBMS.Services.Controllers { if (projectAllocationDtos != null && employeeId != Guid.Empty) { - Guid TenentID = GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); List? result = new List(); List projectIds = new List(); @@ -738,8 +818,8 @@ namespace MarcoBMS.Services.Controllers { try { - ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(TenentID, employeeId); - ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == TenentID).SingleOrDefaultAsync(); + ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(tenantId, employeeId); + ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == tenantId).SingleOrDefaultAsync(); if (projectAllocationFromDb != null) { @@ -1017,7 +1097,6 @@ namespace MarcoBMS.Services.Controllers return BadRequest(ApiResponse.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400)); } - Guid tenantId = GetTenantId(); var workItemsToCreate = new List(); var workItemsToUpdate = new List(); var responseList = new List(); @@ -1113,7 +1192,6 @@ namespace MarcoBMS.Services.Controllers [HttpDelete("task/{id}")] public async Task DeleteProjectTask(Guid id) { - Guid tenantId = _userHelper.GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); List workAreaIds = new List(); WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); @@ -1162,7 +1240,6 @@ namespace MarcoBMS.Services.Controllers [HttpPost("manage-infra")] public async Task ManageProjectInfra(List infraDots) { - Guid tenantId = GetTenantId(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var responseData = new InfraVM { }; @@ -1177,7 +1254,7 @@ namespace MarcoBMS.Services.Controllers { Building building = item.Building.ToBuildingFromBuildingDto(tenantId); - building.TenantId = GetTenantId(); + building.TenantId = tenantId; if (item.Building.Id == null) { @@ -1204,7 +1281,7 @@ namespace MarcoBMS.Services.Controllers if (item.Floor != null) { Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); - floor.TenantId = GetTenantId(); + floor.TenantId = tenantId; bool isCreated = false; if (item.Floor.Id == null) @@ -1242,7 +1319,7 @@ namespace MarcoBMS.Services.Controllers if (item.WorkArea != null) { WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); - workArea.TenantId = GetTenantId(); + workArea.TenantId = tenantId; bool isCreated = false; if (item.WorkArea.Id == null) @@ -1343,11 +1420,6 @@ namespace MarcoBMS.Services.Controllers return finalViewModels; } - private Guid GetTenantId() - { - return _userHelper.GetTenantId(); - } - private async Task GetProjectViewModel(Guid? id, Project project) { ProjectDetailsVM vm = new ProjectDetailsVM(); @@ -1498,6 +1570,38 @@ namespace MarcoBMS.Services.Controllers return dbProject; } + // Helper method for background cache update + private async Task UpdateCacheInBackground(Project project) + { + try + { + // This logic can be more complex, but the idea is to update or add. + if (!await _cache.UpdateProjectDetailsOnly(project)) + { + await _cache.AddProjectDetails(project); + } + _logger.LogInfo("Background cache update succeeded for project {ProjectId}.", project.Id); + } + catch (Exception ex) + { + _logger.LogError("Background cache update failed for project {ProjectId} \n {Error}", project.Id, ex.Message); + } + } + + // Helper method for background notification + private async Task SendNotificationInBackground(object notification, Guid projectId) + { + try + { + await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification); + _logger.LogInfo("Background SignalR notification sent for project {ProjectId}.", projectId); + } + catch (Exception ex) + { + _logger.LogError("Background SignalR notification failed for project {ProjectId} \n {Error}", projectId, ex.Message); + } + } + #endregion } } \ No newline at end of file diff --git a/Marco.Pms.Services/Helpers/ProjectsHelper.cs b/Marco.Pms.Services/Helpers/ProjectsHelper.cs index 6c1cab1..fe70a0a 100644 --- a/Marco.Pms.Services/Helpers/ProjectsHelper.cs +++ b/Marco.Pms.Services/Helpers/ProjectsHelper.cs @@ -67,11 +67,11 @@ namespace MarcoBMS.Services.Helpers else { var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id); - if (allocation.Any()) + if (!allocation.Any()) { - projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); + return new List(); } - return new List(); + projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); } await _cache.AddProjects(LoggedInEmployee.Id, projectIds); } diff --git a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs index f527f67..18db7ff 100644 --- a/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs +++ b/Marco.Pms.Services/MappingProfiles/ProjectMappingProfile.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Master; using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.Projects; @@ -14,7 +15,9 @@ namespace Marco.Pms.Services.MappingProfiles CreateMap(); CreateMap(); CreateMap(); + CreateMap(); CreateMap(); + CreateMap(); CreateMap(); CreateMap() .ForMember(