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 class StatusMasterMongoDB
{ {
public string? Id { get; set; } public string Id { get; set; } = string.Empty;
public string? Status { get; set; } public string? Status { get; set; }
} }
} }

View File

@ -40,8 +40,8 @@ namespace MarcoBMS.Services.Controllers
private readonly Guid tenantId; private readonly Guid tenantId;
public ProjectController(IDbContextFactory<ApplicationDbContext> dbContextFactory, ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, public ProjectController(IDbContextFactory<ApplicationDbContext> dbContextFactory, ApplicationDbContext context, UserHelper userHelper, ILoggingService logger,
ProjectsHelper projectHelper, IHubContext<MarcoHub> signalR, PermissionServices permission, CacheUpdateHelper cache, IMapper mapper) ProjectsHelper projectHelper, IHubContext<MarcoHub> signalR, CacheUpdateHelper cache, PermissionServices permission, IMapper mapper)
{ {
_dbContextFactory = dbContextFactory; _dbContextFactory = dbContextFactory;
_context = context; _context = context;
@ -52,7 +52,7 @@ namespace MarcoBMS.Services.Controllers
_cache = cache; _cache = cache;
_permission = permission; _permission = permission;
_mapper = mapper; _mapper = mapper;
tenantId = _userHelper.GetTenantId(); tenantId = userHelper.GetTenantId();
} }
#region =================================================================== Project Get APIs =================================================================== #region =================================================================== Project Get APIs ===================================================================
@ -161,29 +161,74 @@ namespace MarcoBMS.Services.Controllers
catch (Exception ex) catch (Exception ex)
{ {
// --- Step 6: Graceful Error Handling --- // --- 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)); 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}")] [HttpGet("get/{id}")]
public async Task<IActionResult> Get([FromRoute] Guid id) public async Task<IActionResult> Get([FromRoute] Guid id)
{ {
// --- Step 1: Input Validation ---
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
var errors = ModelState.Values var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
.SelectMany(v => v.Errors) _logger.LogWarning("Get project called with invalid model state for ID {ProjectId}. Errors: {Errors}", id, string.Join(", ", errors));
.Select(e => e.ErrorMessage) return BadRequest(ApiResponse<object>.ErrorResponse("Invalid request data provided.", errors, 400));
.ToList();
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
var project = await _context.Projects.Where(c => c.TenantId == _userHelper.GetTenantId() && c.Id == id).SingleOrDefaultAsync(); try
if (project == null) return NotFound(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 404)); {
return Ok(ApiResponse<object>.SuccessResponse(project, "Success.", 200)); 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}")] [HttpGet("details/{id}")]
public async Task<IActionResult> Details([FromRoute] Guid id) public async Task<IActionResult> Details([FromRoute] Guid id)
{ {
@ -1331,7 +1376,6 @@ namespace MarcoBMS.Services.Controllers
return vm; return vm;
} }
/// <summary> /// <summary>
/// Fetches project details from the database for a given list of project IDs and assembles them into MongoDB models. /// 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. /// This method encapsulates the optimized, parallel database queries.
@ -1409,6 +1453,51 @@ namespace MarcoBMS.Services.Controllers
return mongoDetailsList; 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 #endregion
} }
} }

View File

@ -20,7 +20,18 @@ namespace Marco.Pms.Services.MappingProfiles
.ForMember( .ForMember(
dest => dest.Id, dest => dest.Id,
// Explicitly and safely convert string Id to Guid 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>(); CreateMap<StatusMasterMongoDB, StatusMaster>();