Added an API to get basic list of organizations

This commit is contained in:
ashutosh.nehete 2025-11-26 12:47:50 +05:30
parent bbe36ed535
commit b6baff7d00
3 changed files with 96 additions and 0 deletions

View File

@ -45,6 +45,14 @@ namespace Marco.Pms.Services.Controllers
return StatusCode(response.StatusCode, response);
}
[HttpGet("list/basic")]
public async Task<IActionResult> 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<IActionResult> GetOrganizationDetails(Guid id)
{

View File

@ -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<object>.SuccessResponse(response, "Successfully fetched the organization list", 200);
}
/// <summary>
/// Retrieves a paginated, searchable list of organizations.
/// Optimized for performance using DB Projections and AsNoTracking.
/// </summary>
/// <param name="searchString">Optional keyword to filter organizations by name.</param>
/// <param name="pageNumber">The requested page number (1-based).</param>
/// <param name="pageSize">The number of records per page (Max 50).</param>
/// <param name="loggedInEmployee">The current user context for security filtering.</param>
/// <param name="ct">Cancellation token to cancel operations if the client disconnects.</param>
/// <returns>A paginated list of BasicOrganizationVm.</returns>
public async Task<ApiResponse<object>> 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 ?? "<empty>", 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<BasicOrganizationVm>(_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<object>.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<object>.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<object>.ErrorResponse("Data Fetch Failed", "An unexpected error occurred while retrieving the organization list.", 500);
}
}
public async Task<ApiResponse<object>> GetOrganizationDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId)
{
_logger.LogDebug("Started fetching details for OrganizationId: {OrganizationId}", id);

View File

@ -8,6 +8,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
{
#region =================================================================== Get Functions ===================================================================
Task<ApiResponse<object>> GetOrganizarionListAsync(string? searchString, long? sprid, bool active, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId);
Task<ApiResponse<object>> GetOrganizationBasicListAsync(string? searchString, int pageNumber, int pageSize, Employee loggedInEmployee, CancellationToken ct);
Task<ApiResponse<object>> GetOrganizationDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId);
Task<ApiResponse<object>> GetOrganizationHierarchyListAsync(Guid employeeId, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId);
#endregion