Optimized the Get project By ID API

This commit is contained in:
ashutosh.nehete 2025-07-14 15:57:52 +05:30
parent 0c84bb11a3
commit c5d9beec04
3 changed files with 116 additions and 16 deletions

View File

@ -2,7 +2,7 @@
{
public class StatusMasterMongoDB
{
public string? Id { get; set; }
public string Id { get; set; } = string.Empty;
public string? Status { get; set; }
}
}

View File

@ -40,8 +40,8 @@ namespace MarcoBMS.Services.Controllers
private readonly Guid tenantId;
public ProjectController(IDbContextFactory<ApplicationDbContext> dbContextFactory, ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper,
ProjectsHelper projectHelper, IHubContext<MarcoHub> signalR, PermissionServices permission, CacheUpdateHelper cache, IMapper mapper)
public ProjectController(IDbContextFactory<ApplicationDbContext> dbContextFactory, ApplicationDbContext context, UserHelper userHelper, ILoggingService logger,
ProjectsHelper projectHelper, IHubContext<MarcoHub> signalR, CacheUpdateHelper cache, PermissionServices permission, IMapper mapper)
{
_dbContextFactory = dbContextFactory;
_context = context;
@ -52,7 +52,7 @@ namespace MarcoBMS.Services.Controllers
_cache = cache;
_permission = permission;
_mapper = mapper;
tenantId = _userHelper.GetTenantId();
tenantId = userHelper.GetTenantId();
}
#region =================================================================== Project Get APIs ===================================================================
@ -161,29 +161,74 @@ namespace MarcoBMS.Services.Controllers
catch (Exception ex)
{
// --- Step 6: Graceful Error Handling ---
_logger.LogError("An unexpected error occurred in GetAllProjects for tenant {TenantId}. : {Error}", tenantId, ex.Message);
_logger.LogError("An unexpected error occurred in GetAllProjects for tenant {TenantId}. \n {Error}", tenantId, ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred. Please try again later.", null, 500));
}
}
/// <summary>
/// Retrieves details for a specific project by its ID.
/// This endpoint is optimized with a cache-first strategy and parallel permission checks.
/// </summary>
/// <param name="id">The unique identifier of the project.</param>
/// <returns>An ApiResponse containing the project details or an appropriate error.</returns>
[HttpGet("get/{id}")]
public async Task<IActionResult> Get([FromRoute] Guid id)
{
// --- Step 1: Input Validation ---
if (!ModelState.IsValid)
{
var errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
_logger.LogWarning("Get 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));
}
var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).SingleOrDefaultAsync();
if (project == null) return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404));
return Ok(ApiResponse<object>.SuccessResponse(project, "Success.", 200));
try
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// --- Step 2: Run independent operations in PARALLEL ---
// We can check permissions and fetch data at the same time to reduce latency.
var permissionTask = _permission.HasProjectPermission(loggedInEmployee, id);
// This helper method encapsulates the "cache-first, then database" logic.
var projectDataTask = GetProjectDataAsync(id);
// Await both tasks to complete.
await Task.WhenAll(permissionTask, projectDataTask);
var hasPermission = await permissionTask;
var projectVm = await projectDataTask;
// --- Step 3: Process results sequentially ---
// 3a. Check for permission first. Forbid() is the idiomatic way to return 403.
if (!hasPermission)
{
_logger.LogWarning("Access denied for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, id);
return StatusCode(403, (ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to access this project.", 403)));
}
// 3b. Check if the project was found (either in cache or DB).
if (projectVm == null)
{
_logger.LogInfo("Project with ID {ProjectId} not found.", id);
return NotFound(ApiResponse<object>.ErrorResponse("Project not found.", $"No project found with ID {id}.", 404));
}
// 3c. Success. Return the consistent ViewModel.
_logger.LogInfo("Successfully retrieved project {ProjectId}.", id);
return Ok(ApiResponse<Project>.SuccessResponse(projectVm, "Project retrieved successfully.", 200));
}
catch (Exception ex)
{
_logger.LogError("An unexpected error occurred while getting project {ProjectId} : \n {Error}", id, ex.Message);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An internal server error occurred.", null, 500));
}
}
[HttpGet("details/{id}")]
public async Task<IActionResult> Details([FromRoute] Guid id)
{
@ -1331,7 +1376,6 @@ namespace MarcoBMS.Services.Controllers
return vm;
}
/// <summary>
/// Fetches project details from the database for a given list of project IDs and assembles them into MongoDB models.
/// This method encapsulates the optimized, parallel database queries.
@ -1409,6 +1453,51 @@ namespace MarcoBMS.Services.Controllers
return mongoDetailsList;
}
/// <summary>
/// Private helper to encapsulate the cache-first data retrieval logic.
/// </summary>
/// <returns>A ProjectDetailVM if found, otherwise null.</returns>
private async Task<Project?> GetProjectDataAsync(Guid projectId)
{
// --- Cache First ---
_logger.LogDebug("Attempting to fetch project {ProjectId} from cache.", projectId);
var cachedProject = await _cache.GetProjectDetails(projectId);
if (cachedProject != null)
{
_logger.LogInfo("Cache HIT for project {ProjectId}.", projectId);
// Map from the cache model (e.g., ProjectMongoDB) to the response ViewModel.
return _mapper.Map<Project>(cachedProject);
}
// --- Database Second (on Cache Miss) ---
_logger.LogInfo("Cache MISS for project {ProjectId}. Fetching from database.", projectId);
var dbProject = await _context.Projects
.AsNoTracking() // Use AsNoTracking for read-only queries.
.Where(p => p.Id == projectId && p.TenantId == tenantId)
.SingleOrDefaultAsync();
if (dbProject == null)
{
return null; // The project doesn't exist.
}
// --- Proactively Update Cache ---
// The next request for this project will now be a cache hit.
try
{
// Map the DB entity to the cache model (e.g., ProjectMongoDB) before caching.
await _cache.AddProjectDetails(dbProject);
_logger.LogInfo("Updated cache with project {ProjectId}.", projectId);
}
catch (Exception ex)
{
_logger.LogWarning("Failed to update cache for project {ProjectId} : \n {Error}", projectId, ex.Message);
}
// Map from the database entity to the response ViewModel.
return dbProject;
}
#endregion
}
}

View File

@ -20,7 +20,18 @@ namespace Marco.Pms.Services.MappingProfiles
.ForMember(
dest => dest.Id,
// Explicitly and safely convert string Id to Guid Id
opt => opt.MapFrom(src => src.Id == null ? Guid.Empty : new Guid(src.Id))
opt => opt.MapFrom(src => new Guid(src.Id))
);
CreateMap<ProjectMongoDB, Project>()
.ForMember(
dest => dest.Id,
// Explicitly and safely convert string Id to Guid Id
opt => opt.MapFrom(src => new Guid(src.Id))
).ForMember(
dest => dest.ProjectStatusId,
// Explicitly and safely convert string ProjectStatusId to Guid ProjectStatusId
opt => opt.MapFrom(src => src.ProjectStatus == null ? Guid.Empty : new Guid(src.ProjectStatus.Id))
);
CreateMap<StatusMasterMongoDB, StatusMaster>();