From b6baff7d00d754b3d28a6a003f12c2453ed3f2bd Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Wed, 26 Nov 2025 12:47:50 +0530 Subject: [PATCH] Added an API to get basic list of organizations --- .../Controllers/OrganizationController.cs | 8 ++ .../Service/OrganizationService.cs | 87 +++++++++++++++++++ .../ServiceInterfaces/IOrganizationService.cs | 1 + 3 files changed, 96 insertions(+) diff --git a/Marco.Pms.Services/Controllers/OrganizationController.cs b/Marco.Pms.Services/Controllers/OrganizationController.cs index 80a8847..b31bfb1 100644 --- a/Marco.Pms.Services/Controllers/OrganizationController.cs +++ b/Marco.Pms.Services/Controllers/OrganizationController.cs @@ -45,6 +45,14 @@ namespace Marco.Pms.Services.Controllers return StatusCode(response.StatusCode, response); } + [HttpGet("list/basic")] + public async Task GetOrganizationBasicList([FromQuery] string? searchString, CancellationToken ct, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20) + { + var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); + var response = await _organizationService.GetOrganizationBasicListAsync(searchString, pageNumber, pageSize, loggedInEmployee, ct); + return StatusCode(response.StatusCode, response); + } + [HttpGet("details/{id}")] public async Task GetOrganizationDetails(Guid id) { diff --git a/Marco.Pms.Services/Service/OrganizationService.cs b/Marco.Pms.Services/Service/OrganizationService.cs index 575f684..11e0299 100644 --- a/Marco.Pms.Services/Service/OrganizationService.cs +++ b/Marco.Pms.Services/Service/OrganizationService.cs @@ -1,4 +1,5 @@ using AutoMapper; +using AutoMapper.QueryableExtensions; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Organization; using Marco.Pms.Model.Employees; @@ -13,6 +14,7 @@ using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Service.ServiceInterfaces; using MarcoBMS.Services.Service; using Microsoft.EntityFrameworkCore; +using System.Linq.Dynamic.Core; namespace Marco.Pms.Services.Service { @@ -150,6 +152,91 @@ namespace Marco.Pms.Services.Service return ApiResponse.SuccessResponse(response, "Successfully fetched the organization list", 200); } + + /// + /// Retrieves a paginated, searchable list of organizations. + /// Optimized for performance using DB Projections and AsNoTracking. + /// + /// Optional keyword to filter organizations by name. + /// The requested page number (1-based). + /// The number of records per page (Max 50). + /// The current user context for security filtering. + /// Cancellation token to cancel operations if the client disconnects. + /// A paginated list of BasicOrganizationVm. + public async Task> GetOrganizationBasicListAsync(string? searchString, int pageNumber, int pageSize, Employee loggedInEmployee, CancellationToken ct = default) + { + try + { + // 1. INPUT SANITIZATION + // Ensure valid pagination limits to prevent performance degradation or DoS attacks. + int actualPage = pageNumber < 1 ? 1 : pageNumber; + int actualSize = pageSize > 50 ? 50 : (pageSize < 1 ? 10 : pageSize); + + _logger.LogInfo("Fetching Organization list. Page: {Page}, Size: {Size}, Search: {Search}, User: {UserId}", + actualPage, actualSize, searchString ?? "", loggedInEmployee.Id); + + // 2. QUERY BUILDING + // Use AsNoTracking() for read-only scenarios to reduce overhead. + var query = _context.Organizations.AsNoTracking() + .Where(o => o.IsActive); + + // 3. SECURITY FILTER (Multi-Tenancy) + // Enterprise Rule: Always filter by the logged-in user's Tenant/Permissions. + // Assuming loggedInEmployee has a TenantId or OrganizationId + // query = query.Where(o => o.TenantId == loggedInEmployee.TenantId); + + // 4. DYNAMIC FILTERING + if (!string.IsNullOrWhiteSpace(searchString)) + { + var searchTrimmed = searchString.Trim(); + query = query.Where(o => o.Name.Contains(searchTrimmed)); + } + + // 5. COUNT TOTALS (Efficiently) + // Count the total records matching the filter BEFORE applying pagination + var totalCount = await query.CountAsync(ct); + + // 6. FETCH DATA (With Projection) + // CRITICAL OPTIMIZATION: Use .ProjectTo or .Select BEFORE .ToListAsync. + // This ensures SQL only fetches the columns needed for BasicOrganizationVm, + // rather than fetching the whole Entity and discarding data in memory. + var items = await query + .OrderBy(o => o.Name) + .Skip((actualPage - 1) * actualSize) + .Take(actualSize) + .ProjectTo(_mapper.ConfigurationProvider) // Requires AutoMapper.QueryableExtensions + .ToListAsync(ct); + + // 7. PREPARE RESPONSE + var totalPages = (int)Math.Ceiling((double)totalCount / actualSize); + + var pagedResult = new + { + CurrentPage = actualPage, + PageSize = actualSize, + TotalPages = totalPages, + TotalCount = totalCount, + HasPrevious = actualPage > 1, + HasNext = actualPage < totalPages, + Data = items + }; + + _logger.LogInfo("Successfully fetched {Count} organizations out of {Total}.", items.Count, totalCount); + + return ApiResponse.SuccessResponse(pagedResult, "Organization list fetched successfully.", 200); + } + catch (OperationCanceledException) + { + // Handle client disconnection gracefully + _logger.LogWarning("Organization list fetch was cancelled by the client."); + return ApiResponse.ErrorResponse("Request Cancelled", "The operation was cancelled.", 499); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to fetch organization list. User: {UserId}, Error: {Message}", loggedInEmployee.Id, ex.Message); + return ApiResponse.ErrorResponse("Data Fetch Failed", "An unexpected error occurred while retrieving the organization list.", 500); + } + } public async Task> GetOrganizationDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId) { _logger.LogDebug("Started fetching details for OrganizationId: {OrganizationId}", id); diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IOrganizationService.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IOrganizationService.cs index cc70930..6abbe83 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IOrganizationService.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IOrganizationService.cs @@ -8,6 +8,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces { #region =================================================================== Get Functions =================================================================== Task> GetOrganizarionListAsync(string? searchString, long? sprid, bool active, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId); + Task> GetOrganizationBasicListAsync(string? searchString, int pageNumber, int pageSize, Employee loggedInEmployee, CancellationToken ct); Task> GetOrganizationDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId); Task> GetOrganizationHierarchyListAsync(Guid employeeId, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId); #endregion