Optimized the Update project API
This commit is contained in:
parent
c5d9beec04
commit
e769c161f4
@ -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
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
Loading…
x
Reference in New Issue
Block a user