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