Optimized the Update project API

This commit is contained in:
ashutosh.nehete 2025-07-14 17:00:28 +05:30
parent ca34b01ab0
commit 36eb7aef7f
3 changed files with 142 additions and 35 deletions

View File

@ -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<Guid> 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<IActionResult> 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
}
}
/// <summary>
/// Updates an existing project's details.
/// This endpoint is secure, handles concurrency, and performs non-essential tasks in the background.
/// </summary>
/// <param name="id">The ID of the project to update.</param>
/// <param name="updateProjectDto">The data to update the project with.</param>
/// <returns>An ApiResponse confirming the update or an appropriate error.</returns>
[HttpPut("update/{id}")]
public async Task<IActionResult> 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<object>.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<object>.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<object>.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<object>.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<ProjectDto>(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<ProjectDto>.SuccessResponse(_mapper.Map<ProjectDto>(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<object>.ErrorResponse("An internal server error occurred.", null, 500));
}
}
#endregion
#region =================================================================== Project Allocation APIs ===================================================================
@ -524,7 +611,6 @@ namespace MarcoBMS.Services.Controllers
return BadRequest(ApiResponse<object>.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<object>.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<object>? result = new List<object>();
@ -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<IActionResult> GetProjectsByEmployee([FromRoute] Guid employeeId)
{
Guid tenantId = _userHelper.GetTenantId();
if (employeeId == Guid.Empty)
{
return BadRequest(ApiResponse<object>.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<object>? result = new List<object>();
List<Guid> projectIds = new List<Guid>();
@ -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<object>.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400));
}
Guid tenantId = GetTenantId();
var workItemsToCreate = new List<WorkItem>();
var workItemsToUpdate = new List<WorkItem>();
var responseList = new List<WorkItemVM>();
@ -1113,7 +1192,6 @@ namespace MarcoBMS.Services.Controllers
[HttpDelete("task/{id}")]
public async Task<IActionResult> DeleteProjectTask(Guid id)
{
Guid tenantId = _userHelper.GetTenantId();
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);
@ -1162,7 +1240,6 @@ namespace MarcoBMS.Services.Controllers
[HttpPost("manage-infra")]
public async Task<IActionResult> ManageProjectInfra(List<InfraDot> 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<ProjectDetailsVM> 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
}
}

View File

@ -67,12 +67,12 @@ 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<Guid>();
}
projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList();
}
await _cache.AddProjects(LoggedInEmployee.Id, projectIds);
}

View File

@ -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<Project, ProjectVM>();
CreateMap<Project, ProjectInfoVM>();
CreateMap<ProjectMongoDB, ProjectInfoVM>();
CreateMap<UpdateProjectDto, Project>();
CreateMap<Project, ProjectListVM>();
CreateMap<Project, ProjectDto>();
CreateMap<ProjectMongoDB, ProjectListVM>();
CreateMap<ProjectMongoDB, ProjectVM>()
.ForMember(