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); _logger.LogInfo("Basic project list requested by EmployeeId {EmployeeId}", loggedInEmployee.Id);
// Step 2: Get the list of project IDs the user has access to // 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); List<Guid> accessibleProjectIds = await _projectsHelper.GetMyProjects(tenantId, loggedInEmployee);
if (accessibleProjectIds == null || !accessibleProjectIds.Any()) 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) if (project == null)
{ {
@ -420,7 +419,6 @@ namespace MarcoBMS.Services.Controllers
} }
// 2. Prepare data without I/O // 2. Prepare data without I/O
Guid tenantId = _userHelper.GetTenantId(); // Assuming this is fast and from claims
Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); Employee loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var loggedInUserId = loggedInEmployee.Id; var loggedInUserId = loggedInEmployee.Id;
var project = projectDto.ToProjectFromCreateProjectDto(tenantId); var project = projectDto.ToProjectFromCreateProjectDto(tenantId);
@ -465,7 +463,7 @@ namespace MarcoBMS.Services.Controllers
} }
[HttpPut] [HttpPut]
[Route("update/{id}")] [Route("update1/{id}")]
public async Task<IActionResult> Update([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto) public async Task<IActionResult> Update([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto)
{ {
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
@ -480,9 +478,7 @@ namespace MarcoBMS.Services.Controllers
} }
try try
{ {
Guid TenantId = GetTenantId(); Project project = updateProjectDto.ToProjectFromUpdateProjectDto(tenantId, id);
Project project = updateProjectDto.ToProjectFromUpdateProjectDto(TenantId, id);
_context.Projects.Update(project); _context.Projects.Update(project);
await _context.SaveChangesAsync(); 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 #endregion
#region =================================================================== Project Allocation APIs =================================================================== #region =================================================================== Project Allocation APIs ===================================================================
@ -524,7 +611,6 @@ namespace MarcoBMS.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
Guid TenantId = GetTenantId();
if (projectid != null) if (projectid != null)
{ {
@ -535,14 +621,14 @@ namespace MarcoBMS.Services.Controllers
{ {
result = await (from rpm in _context.Employees.Include(c => c.JobRole) 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 on rpm.Id equals fp.EmployeeId
select rpm).ToListAsync(); select rpm).ToListAsync();
} }
else else
{ {
result = await (from rpm in _context.Employees.Include(c => c.JobRole) 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 on rpm.Id equals fp.EmployeeId
select rpm).ToListAsync(); select rpm).ToListAsync();
} }
@ -577,11 +663,9 @@ namespace MarcoBMS.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
Guid TenantId = GetTenantId();
var employees = await _context.ProjectAllocations 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) .Include(e => e.Employee)
.Select(e => new .Select(e => new
{ {
@ -605,7 +689,6 @@ namespace MarcoBMS.Services.Controllers
{ {
if (projectAllocationDot != null) if (projectAllocationDot != null)
{ {
Guid TenentID = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
List<object>? result = new List<object>(); List<object>? result = new List<object>();
@ -616,11 +699,11 @@ namespace MarcoBMS.Services.Controllers
{ {
try try
{ {
ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(TenentID); ProjectAllocation projectAllocation = item.ToProjectAllocationFromProjectAllocationDto(tenantId);
ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == projectAllocation.EmployeeId ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == projectAllocation.EmployeeId
&& c.ProjectId == projectAllocation.ProjectId && c.ProjectId == projectAllocation.ProjectId
&& c.ReAllocationDate == null && c.ReAllocationDate == null
&& c.TenantId == TenentID).SingleOrDefaultAsync(); && c.TenantId == tenantId).SingleOrDefaultAsync();
if (projectAllocationFromDb != null) if (projectAllocationFromDb != null)
{ {
@ -688,8 +771,6 @@ namespace MarcoBMS.Services.Controllers
[HttpGet("assigned-projects/{employeeId}")] [HttpGet("assigned-projects/{employeeId}")]
public async Task<IActionResult> GetProjectsByEmployee([FromRoute] Guid employeeId) public async Task<IActionResult> GetProjectsByEmployee([FromRoute] Guid employeeId)
{ {
Guid tenantId = _userHelper.GetTenantId();
if (employeeId == Guid.Empty) if (employeeId == Guid.Empty)
{ {
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid details.", "Employee id not valid.", 400)); 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) if (projectAllocationDtos != null && employeeId != Guid.Empty)
{ {
Guid TenentID = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
List<object>? result = new List<object>(); List<object>? result = new List<object>();
List<Guid> projectIds = new List<Guid>(); List<Guid> projectIds = new List<Guid>();
@ -738,8 +818,8 @@ namespace MarcoBMS.Services.Controllers
{ {
try try
{ {
ProjectAllocation projectAllocation = projectAllocationDto.ToProjectAllocationFromProjectsAllocationDto(TenentID, employeeId); 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 == TenentID).SingleOrDefaultAsync(); ProjectAllocation? projectAllocationFromDb = await _context.ProjectAllocations.Where(c => c.EmployeeId == employeeId && c.ProjectId == projectAllocationDto.ProjectId && c.ReAllocationDate == null && c.TenantId == tenantId).SingleOrDefaultAsync();
if (projectAllocationFromDb != null) 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)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400));
} }
Guid tenantId = GetTenantId();
var workItemsToCreate = new List<WorkItem>(); var workItemsToCreate = new List<WorkItem>();
var workItemsToUpdate = new List<WorkItem>(); var workItemsToUpdate = new List<WorkItem>();
var responseList = new List<WorkItemVM>(); var responseList = new List<WorkItemVM>();
@ -1113,7 +1192,6 @@ namespace MarcoBMS.Services.Controllers
[HttpDelete("task/{id}")] [HttpDelete("task/{id}")]
public async Task<IActionResult> DeleteProjectTask(Guid id) public async Task<IActionResult> DeleteProjectTask(Guid id)
{ {
Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
List<Guid> workAreaIds = new List<Guid>(); List<Guid> workAreaIds = new List<Guid>();
WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); 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")] [HttpPost("manage-infra")]
public async Task<IActionResult> ManageProjectInfra(List<InfraDot> infraDots) public async Task<IActionResult> ManageProjectInfra(List<InfraDot> infraDots)
{ {
Guid tenantId = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var responseData = new InfraVM { }; var responseData = new InfraVM { };
@ -1177,7 +1254,7 @@ namespace MarcoBMS.Services.Controllers
{ {
Building building = item.Building.ToBuildingFromBuildingDto(tenantId); Building building = item.Building.ToBuildingFromBuildingDto(tenantId);
building.TenantId = GetTenantId(); building.TenantId = tenantId;
if (item.Building.Id == null) if (item.Building.Id == null)
{ {
@ -1204,7 +1281,7 @@ namespace MarcoBMS.Services.Controllers
if (item.Floor != null) if (item.Floor != null)
{ {
Floor floor = item.Floor.ToFloorFromFloorDto(tenantId); Floor floor = item.Floor.ToFloorFromFloorDto(tenantId);
floor.TenantId = GetTenantId(); floor.TenantId = tenantId;
bool isCreated = false; bool isCreated = false;
if (item.Floor.Id == null) if (item.Floor.Id == null)
@ -1242,7 +1319,7 @@ namespace MarcoBMS.Services.Controllers
if (item.WorkArea != null) if (item.WorkArea != null)
{ {
WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId); WorkArea workArea = item.WorkArea.ToWorkAreaFromWorkAreaDto(tenantId);
workArea.TenantId = GetTenantId(); workArea.TenantId = tenantId;
bool isCreated = false; bool isCreated = false;
if (item.WorkArea.Id == null) if (item.WorkArea.Id == null)
@ -1343,11 +1420,6 @@ namespace MarcoBMS.Services.Controllers
return finalViewModels; return finalViewModels;
} }
private Guid GetTenantId()
{
return _userHelper.GetTenantId();
}
private async Task<ProjectDetailsVM> GetProjectViewModel(Guid? id, Project project) private async Task<ProjectDetailsVM> GetProjectViewModel(Guid? id, Project project)
{ {
ProjectDetailsVM vm = new ProjectDetailsVM(); ProjectDetailsVM vm = new ProjectDetailsVM();
@ -1498,6 +1570,38 @@ namespace MarcoBMS.Services.Controllers
return dbProject; 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 #endregion
} }
} }

View File

@ -67,11 +67,11 @@ namespace MarcoBMS.Services.Helpers
else else
{ {
var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id); var allocation = await GetProjectByEmployeeID(LoggedInEmployee.Id);
if (allocation.Any()) if (!allocation.Any())
{ {
projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList(); return new List<Guid>();
} }
return new List<Guid>(); projectIds = allocation.Select(c => c.ProjectId).Distinct().ToList();
} }
await _cache.AddProjects(LoggedInEmployee.Id, projectIds); await _cache.AddProjects(LoggedInEmployee.Id, projectIds);
} }

View File

@ -1,4 +1,5 @@
using AutoMapper; using AutoMapper;
using Marco.Pms.Model.Dtos.Project;
using Marco.Pms.Model.Master; using Marco.Pms.Model.Master;
using Marco.Pms.Model.MongoDBModels; using Marco.Pms.Model.MongoDBModels;
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Projects;
@ -14,7 +15,9 @@ namespace Marco.Pms.Services.MappingProfiles
CreateMap<Project, ProjectVM>(); CreateMap<Project, ProjectVM>();
CreateMap<Project, ProjectInfoVM>(); CreateMap<Project, ProjectInfoVM>();
CreateMap<ProjectMongoDB, ProjectInfoVM>(); CreateMap<ProjectMongoDB, ProjectInfoVM>();
CreateMap<UpdateProjectDto, Project>();
CreateMap<Project, ProjectListVM>(); CreateMap<Project, ProjectListVM>();
CreateMap<Project, ProjectDto>();
CreateMap<ProjectMongoDB, ProjectListVM>(); CreateMap<ProjectMongoDB, ProjectListVM>();
CreateMap<ProjectMongoDB, ProjectVM>() CreateMap<ProjectMongoDB, ProjectVM>()
.ForMember( .ForMember(