1234 lines
64 KiB
C#

using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Helpers.Utility;
using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Dtos.DocumentManager;
using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Filters;
using Marco.Pms.Model.MongoDBModels.Utility;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.DocumentManager;
using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using System.Text.RegularExpressions;
using Document = Marco.Pms.Model.DocumentManager.Document;
namespace Marco.Pms.Services.Controllers
{
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class DocumentController : ControllerBase
{
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly IServiceScopeFactory _serviceScope;
private readonly UserHelper _userHelper;
private readonly ILoggingService _logger;
private readonly IMapper _mapper;
private readonly Guid tenantId;
private static readonly Guid ProjectEntity = Guid.Parse("c8fe7115-aa27-43bc-99f4-7b05fabe436e");
private static readonly Guid EmployeeEntity = Guid.Parse("dbb9555a-7a0c-40f2-a9ed-f0463f1ceed7");
private static readonly string Collection = "DocumentModificationLog";
public DocumentController(IDbContextFactory<ApplicationDbContext> dbContextFactory,
IServiceScopeFactory serviceScope,
UserHelper userHelper,
ILoggingService logger,
IMapper mapper)
{
_dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory));
_serviceScope = serviceScope ?? throw new ArgumentNullException(nameof(serviceScope));
_userHelper = userHelper ?? throw new ArgumentNullException(nameof(userHelper));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
tenantId = userHelper.GetTenantId();
}
[HttpGet("list/{entityTypeId}/entity/{entityId}")]
public async Task<IActionResult> GetDocumentListAsync(Guid entityTypeId, Guid entityId, [FromQuery] string? filter, [FromQuery] string? searchString,
[FromQuery] bool isActive = true, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
{
using var scope = _serviceScope.CreateScope();
await using var _context = await _dbContextFactory.CreateDbContextAsync();
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
try
{
_logger.LogInfo("Fetching documents for EntityTypeId: {EntityTypeId}, EntityId: {EntityId}", entityTypeId, entityId);
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Check global permission
var hasViewPermission = await _permission.HasPermission(PermissionsMaster.ViewDocument, loggedInEmployee.Id);
if (!hasViewPermission && loggedInEmployee.Id != entityId)
{
_logger.LogWarning("Access Denied for Employee {EmployeeId} on EntityId {EntityId}", loggedInEmployee.Id, entityId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to view documents", 403));
}
// Validate entity type
if (ProjectEntity != entityTypeId && EmployeeEntity != entityTypeId)
{
_logger.LogWarning("Invalid EntityTypeId: {EntityTypeId}", entityTypeId);
return NotFound(ApiResponse<object>.ErrorResponse("Entity type not found", "Entity Type not found in database", 404));
}
// Project permission check
if (ProjectEntity == entityTypeId)
{
var hasProjectPermission = await _permission.HasProjectPermission(loggedInEmployee, entityId);
if (!hasProjectPermission)
{
_logger.LogWarning("Employee {EmployeeId} does not have project access for ProjectId {ProjectId}", loggedInEmployee.Id, entityId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to access project documents", 403));
}
}
// Employee validation
else if (EmployeeEntity == entityTypeId)
{
var isEmployeeExists = await _context.Employees
.AnyAsync(e => e.Id == entityId && e.TenantId == tenantId);
if (!isEmployeeExists)
{
_logger.LogWarning("Employee {EmployeeId} not found for Tenant {TenantId}", entityId, tenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Employee not found", "Employee not found in database", 404));
}
}
// Base Query (with includes to avoid lazy loading)
IQueryable<DocumentAttachment> documentQuery = _context.DocumentAttachments
.AsNoTracking() // Optimization: Read-only query
.Include(da => da.UploadedBy)
.ThenInclude(e => e!.JobRole)
.Include(da => da.DocumentType!)
.ThenInclude(dt => dt.DocumentCategory)
.Where(da => da.EntityId == entityId &&
da.DocumentType != null &&
da.DocumentType.DocumentCategory != null &&
da.DocumentType.DocumentCategory.EntityTypeId == entityTypeId &&
da.IsActive == isActive &&
da.TenantId == tenantId);
// Apply filter if provided
var documentFilter = TryDeserializeFilter(filter);
if (documentFilter != null)
{
_logger.LogInfo("Applying document filters for EntityId {EntityId}", entityId);
if (documentFilter.IsVerified != null)
documentQuery = documentQuery.Where(da => da.IsVerified == documentFilter.IsVerified);
if (documentFilter.DocumentCategoryIds?.Any() ?? false)
documentQuery = documentQuery.Where(da => documentFilter.DocumentCategoryIds.Contains(da.DocumentType!.DocumentCategoryId));
if (documentFilter.DocumentTypeIds?.Any() ?? false)
documentQuery = documentQuery.Where(da => documentFilter.DocumentTypeIds.Contains(da.DocumentTypeId));
if (documentFilter.DocumentTagIds?.Any() ?? false)
{
var filteredIds = await _context.AttachmentTagMappings
.AsNoTracking()
.Where(at => at.DocumentTag != null && documentFilter.DocumentTagIds.Contains(at.DocumentTag.Id) && at.TenantId == tenantId)
.Select(at => at.AttachmentId)
.ToListAsync();
documentQuery = documentQuery.Where(da => filteredIds.Contains(da.Id));
}
if (documentFilter.UploadedByIds?.Any() ?? false)
documentQuery = documentQuery.Where(da => documentFilter.UploadedByIds.Contains(da.UploadedById));
// Date Range Filtering (Uploaded vs Verified date)
if (documentFilter.StartDate != null && documentFilter.EndDate != null)
{
if (documentFilter.IsUploadedAt)
{
documentQuery = documentQuery.Where(da =>
da.UpdatedAt.HasValue &&
da.UpdatedAt.Value.Date >= documentFilter.StartDate.Value.Date &&
da.UpdatedAt.Value.Date <= documentFilter.EndDate.Value.Date);
}
else
{
documentQuery = documentQuery.Where(da =>
da.VerifiedAt.HasValue &&
da.VerifiedAt.Value.Date >= documentFilter.StartDate.Value.Date &&
da.VerifiedAt.Value.Date <= documentFilter.EndDate.Value.Date);
}
}
else
{
documentQuery = documentQuery.Where(da => da.IsCurrentVersion);
}
}
else
{
// Default: only latest version
documentQuery = documentQuery.Where(da => da.IsCurrentVersion);
}
// Apply search filter
if (!string.IsNullOrWhiteSpace(searchString))
{
documentQuery = documentQuery.Where(da =>
da.Name.Contains(searchString) ||
(da.DocumentId != null && da.DocumentId.Contains(searchString))
);
}
// Apply pagination & ordering
var documents = await documentQuery
.OrderByDescending(t => t.UploadedAt)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
var documentIds = documents.Select(da => da.Id).ToList();
// Get versions for the selected documents
var versions = await _context.AttachmentVersionMappings
.AsNoTracking()
.Where(av => documentIds.Contains(av.ChildAttachmentId) && av.TenantId == tenantId)
.ToListAsync();
// Map to ViewModel
var response = documents.Select(doc =>
{
var version = versions.FirstOrDefault(v => v.ChildAttachmentId == doc.Id);
var vm = _mapper.Map<DocumentListVM>(doc);
vm.ParentAttachmentId = version?.ParentAttachmentId;
vm.Version = version?.Version ?? 1;
return vm;
}).ToList();
_logger.LogInfo("Fetched {Count} documents for EntityId {EntityId}", response.Count, entityId);
return Ok(ApiResponse<object>.SuccessResponse(response, "Document list fetched successfully", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "Error fetching documents for EntityId {EntityId}", entityId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", ex.Message, 500));
}
}
[HttpGet("get/details/{id}")]
public async Task<IActionResult> GetDetailsAsync(Guid id)
{
_logger.LogInfo("Starting GetDetails API for AttachmentId: {AttachmentId}", id);
// Create a new DbContext instance to fetch data
await using var _context = await _dbContextFactory.CreateDbContextAsync();
// Create a new scoped service provider to resolve scoped dependencies
using var scope = _serviceScope.CreateScope();
// Resolve the permission service from the scoped service provider
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
// Get the currently logged-in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogDebug("Logged in employee id: {EmployeeId}", loggedInEmployee.Id);
// Fetch the AttachmentVersionMapping with all necessary related data loaded eagerly
var versionMapping = await _context.AttachmentVersionMappings
.Include(av => av.ChildAttachment)
.ThenInclude(da => da!.UploadedBy)
.ThenInclude(e => e!.JobRole)
.Include(av => av.ChildAttachment)
.ThenInclude(da => da!.UpdatedBy)
.ThenInclude(e => e!.JobRole)
.Include(av => av.ChildAttachment)
.ThenInclude(da => da!.VerifiedBy)
.ThenInclude(e => e!.JobRole)
.Include(av => av.ChildAttachment)
.ThenInclude(da => da!.DocumentType)
.FirstOrDefaultAsync(av => av.ChildAttachmentId == id && av.TenantId == tenantId);
// If no mapping found, return 404
if (versionMapping == null || versionMapping.ChildAttachment == null)
{
_logger.LogWarning("AttachmentVersionMapping not found for AttachmentId: {AttachmentId}, TenantId: {TenantId}",
id, tenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Document Attachment not found", "Document Attachment not found in database", 404));
}
// Check if the logged in employee has permission to view the document OR is the owner of the attachment entity
var hasViewPermission = await _permission.HasPermission(PermissionsMaster.ViewDocument, loggedInEmployee.Id);
if (!hasViewPermission && loggedInEmployee.Id != versionMapping.ChildAttachment.EntityId)
{
_logger.LogWarning("Access Denied for Employee {EmployeeId} on EntityId {EntityId}",
loggedInEmployee.Id, versionMapping.ChildAttachment.EntityId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to view documents", 403));
}
// Map the domain entity to the view model
var documentAttachmentVM = _mapper.Map<DocumentAttachmentDetailsVM>(versionMapping.ChildAttachment);
documentAttachmentVM.Version = versionMapping.Version;
documentAttachmentVM.ParentAttachmentId = versionMapping.ParentAttachmentId;
_logger.LogInfo("Document details fetched successfully for AttachmentId: {AttachmentId}", id);
// Return success response with document details
return Ok(ApiResponse<object>.SuccessResponse(documentAttachmentVM, "Document details fetched successfully", 200));
}
[HttpGet("get/filter/{entityTypeId}")]
public async Task<IActionResult> GetFilterObjectAsync(Guid entityTypeId)
{
// Log: Starting filter fetch process
_logger.LogInfo("Initiating GetFilterObjectAsync to retrieve document filter data.");
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScope.CreateScope();
// Get current logged-in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogDebug("Fetched current employee: {EmployeeId}", loggedInEmployee.Id);
// Fetch all relevant document attachments for the tenant with related data
var documentList = await _context.DocumentAttachments
.Include(da => da.UploadedBy)
.Include(da => da.DocumentType)
.ThenInclude(dt => dt!.DocumentCategory)
.Where(da => da.DocumentType != null &&
da.DocumentType.DocumentCategory != null &&
da.DocumentType.DocumentCategory.EntityTypeId == entityTypeId &&
da.TenantId == tenantId)
.ToListAsync();
_logger.LogInfo("Fetched {Count} document attachments for tenant {TenantId}", documentList.Count, tenantId);
// Select IDs for attachments present in documentList
var documentIds = documentList.Select(da => da.Id).ToList();
// Preload tags for given ids
var documentTags = await _context.AttachmentTagMappings
.Where(at => documentIds.Contains(at.AttachmentId) && at.DocumentTag != null)
.Select(at => new
{
Id = at.DocumentTag!.Id,
Name = at.DocumentTag.Name
})
.Distinct()
.ToListAsync();
_logger.LogInfo("Loaded {Count} document tags", documentTags.Count);
// Gather unique UploadedBy users
var uploadedBy = documentList
.Where(da => da.UploadedBy != null)
.Select(da => new
{
Id = da.UploadedBy!.Id,
Name = $"{da.UploadedBy.FirstName} {da.UploadedBy.LastName}"
})
.Distinct()
.ToList();
_logger.LogInfo("Collected {Count} unique uploaders", uploadedBy.Count);
// Gather unique DocumentCategories
var documentCategories = documentList
.Where(da => da.DocumentType?.DocumentCategory != null)
.Select(da => new
{
Id = da.DocumentType!.DocumentCategory!.Id,
Name = da.DocumentType.DocumentCategory.Name
})
.Distinct()
.ToList();
_logger.LogInfo("Collected {Count} unique document categories", documentCategories.Count);
// Gather unique DocumentTypes
var documentTypes = documentList
.Where(da => da.DocumentType != null)
.Select(da => new
{
Id = da.DocumentType!.Id,
Name = da.DocumentType.Name
})
.Distinct()
.ToList();
_logger.LogInfo("Collected {Count} unique document types", documentTypes.Count);
// Compose response
var response = new
{
UploadedBy = uploadedBy,
DocumentCategory = documentCategories,
DocumentType = documentTypes,
DocumentTag = documentTags
};
_logger.LogInfo("Returning filter response successfully.");
return Ok(ApiResponse<object>.SuccessResponse(response, "Filters for documents fetched successfully", 200));
}
[HttpGet("list/versions/{parentAttachmentId}")]
public async Task<IActionResult> GetListAllVersionsAsync(Guid parentAttachmentId)
{
_logger.LogInfo("Start fetching document versions for ParentAttachmentId: {ParentAttachmentId}", parentAttachmentId);
// Create a new DbContext instance asynchronously
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScope.CreateScope();
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
try
{
// Retrieve currently logged in employee details for potential security or filtering checks
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (loggedInEmployee == null)
{
_logger.LogWarning("No logged in employee found while fetching versions for ParentAttachmentId: {ParentAttachmentId}", parentAttachmentId);
return Unauthorized(ApiResponse<object>.ErrorResponse("Unauthorized access", 401));
}
// Retrieve all version mappings linked to the parent attachment and tenant
var versionMappings = await _context.AttachmentVersionMappings
.Include(av => av.ChildAttachment)
.ThenInclude(da => da!.UploadedBy)
.Include(av => av.ChildAttachment)
.ThenInclude(da => da!.UpdatedBy)
.Where(av => av.ParentAttachmentId == parentAttachmentId && av.TenantId == tenantId)
.ToListAsync();
var entityId = versionMappings.Select(av => av.ChildAttachment?.EntityId).FirstOrDefault();
// Check global permission
var hasViewPermission = await _permission.HasPermission(PermissionsMaster.ViewDocument, loggedInEmployee.Id);
if (!hasViewPermission && loggedInEmployee.Id != entityId)
{
_logger.LogWarning("Access Denied for Employee {EmployeeId} on EntityId {EntityId}", loggedInEmployee.Id, entityId ?? Guid.Empty);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to view documents", 403));
}
_logger.LogInfo("Found {Count} versions for ParentAttachmentId: {ParentAttachmentId}", versionMappings.Count, parentAttachmentId);
// Map the retrieved child attachments to view models with version info
var response = versionMappings.Select(versionMapping =>
{
var documentVM = _mapper.Map<AttachmentVersionVM>(versionMapping.ChildAttachment);
documentVM.Version = versionMapping.Version;
return documentVM;
}).ToList();
_logger.LogInfo("Successfully mapped version data for ParentAttachmentId: {ParentAttachmentId}", parentAttachmentId);
return Ok(ApiResponse<object>.SuccessResponse(response, "Document versions fetched successfully", 200));
}
catch (Exception ex)
{
// Log the exception and return an internal server error response
_logger.LogError(ex, "Error occurred while fetching document versions for ParentAttachmentId: {ParentAttachmentId}", parentAttachmentId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An error occurred while fetching document versions", 500));
}
finally
{
_logger.LogInfo("End processing GetAllVersions for ParentAttachmentId: {ParentAttachmentId}", parentAttachmentId);
}
}
[HttpGet("get/version/{id}")]
public async Task<IActionResult> GetAllVersionsAsync(Guid id)
{
// API endpoint to get all versions of an attachment by its ID
_logger.LogInfo($"Fetching versions for attachment with ID: {id}");
await using var _context = await _dbContextFactory.CreateDbContextAsync();
// Create a new DbContext from the factory asynchronously
using var scope = _serviceScope.CreateScope();
// Create a new service scope for scoped services
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Get the currently logged in employee asynchronously
var versionMapping = await _context.AttachmentVersionMappings
// Retrieve version mapping including the child attachment and its document
.Include(av => av.ChildAttachment)
.ThenInclude(da => da!.Document)
.FirstOrDefaultAsync(av => av.ChildAttachmentId == id);
if (versionMapping == null || versionMapping.ChildAttachment == null || versionMapping.ChildAttachment.Document == null)
// Return 404 if no version mapping or related entities found
{
_logger.LogWarning($"Version mapping not found for attachment ID: {id}");
return NotFound(ApiResponse<object>.ErrorResponse("Version not found", "Version not found in database", 404));
}
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var hasDownloadPermission = await _permission.HasPermission(PermissionsMaster.DownloadDocument, loggedInEmployee.Id);
if (!hasDownloadPermission && loggedInEmployee.Id != versionMapping.ChildAttachment.EntityId)
{
_logger.LogWarning("Access Denied for Employee {EmployeeId} on EntityId {EntityId} for downloading",
loggedInEmployee.Id, versionMapping.ChildAttachment.EntityId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to download documents", 403));
}
var s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
// Resolve S3UploadService from the scope to generate pre-signed URL
var preSignedUrl = s3Service.GeneratePreSignedUrl(versionMapping.ChildAttachment.Document.S3Key);
// Generate pre-signed URL for the document stored in S3
_logger.LogInfo($"Generated pre-signed URL for attachment ID: {id}");
return Ok(ApiResponse<object>.SuccessResponse(preSignedUrl, "Pre-Signed Url for old version fetched successfully", 200));
// Return the pre-signed URL with a success response
}
/// <summary>
/// Uploads a document attachment for an Employee or Project.
/// Validates permissions, document type, entity existence, tags, and uploads to S3.
/// </summary>
[HttpPost("upload")]
public async Task<IActionResult> UploadDocumentAsync([FromBody] DocumentAttachmentDto model)
{
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScope.CreateScope();
_logger.LogInfo("Document upload initiated for EntityId: {EntityId}, DocumentTypeId: {DocumentTypeId}", model.EntityId, model.DocumentTypeId);
try
{
// Get logged in user
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Permission check
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var hasUploadPermission = await permissionService.HasPermission(PermissionsMaster.UploadDocument, loggedInEmployee.Id);
if (!hasUploadPermission && loggedInEmployee.Id != model.EntityId)
{
_logger.LogWarning("Access Denied. User {UserId} tried to upload document for {EntityId}", loggedInEmployee.Id, model.EntityId);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to upload this document", 403));
}
// Validate Document Type
var documentType = await _context.DocumentTypeMasters
.Include(dt => dt.DocumentCategory)
.FirstOrDefaultAsync(dt => dt.Id == model.DocumentTypeId && dt.TenantId == tenantId && dt.DocumentCategory != null);
if (documentType == null)
{
_logger.LogWarning("DocumentTypeId {DocumentTypeId} not found for Tenant {TenantId}", model.DocumentTypeId, tenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Document Type not found", "Document Type not found in database", 404));
}
// Document ID validation
if (documentType.IsMandatory && string.IsNullOrWhiteSpace(model.DocumentId))
{
_logger.LogWarning("Mandatory DocumentId missing for DocumentTypeId: {DocumentTypeId}", documentType.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Document ID missing", "User must provide the document ID for this document", 400));
}
if (documentType.IsValidationRequired && !string.IsNullOrWhiteSpace(model.DocumentId) && !string.IsNullOrWhiteSpace(documentType.RegexExpression))
{
if (!Regex.IsMatch(model.DocumentId, documentType.RegexExpression))
{
_logger.LogWarning("Invalid DocumentId format for DocumentTypeId: {DocumentTypeId}, Provided: {DocumentId}", documentType.Id, model.DocumentId);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Document ID", "Provided document ID is not valid", 400));
}
}
// Verify if Employee/Project exists
var entityType = documentType.DocumentCategory!.EntityTypeId;
bool entityExists = false;
if (entityType.Equals(EmployeeEntity))
{
entityExists = await _context.Employees.AnyAsync(e => e.Id == model.EntityId && e.TenantId == tenantId);
}
else if (entityType.Equals(ProjectEntity))
{
entityExists = await _context.Projects.AnyAsync(p => p.Id == model.EntityId && p.TenantId == tenantId);
}
if (!entityExists)
{
_logger.LogWarning("Entity not found. EntityType: {EntityType}, EntityId: {EntityId}", entityType, model.EntityId);
return NotFound(ApiResponse<object>.ErrorResponse($"{(entityType == EmployeeEntity ? "Employee" : "Project")} Not Found", "Entity not found in database", 404));
}
// Map DTO to DB entity
var attachment = _mapper.Map<DocumentAttachment>(model);
attachment.UploadedAt = DateTime.UtcNow;
attachment.UploadedById = loggedInEmployee.Id;
attachment.IsCurrentVersion = true;
attachment.TenantId = tenantId;
// Validate Attachment
var allowedSize = documentType.MaxSizeAllowedInMB * 1024;
if (model.Attachment.FileSize > allowedSize)
{
_logger.LogWarning("File size {FileSize} exceeded max allowed {MaxSize}MB", model.Attachment.FileSize, documentType.MaxSizeAllowedInMB);
return BadRequest(ApiResponse<object>.ErrorResponse("File size limit exceeded", $"Max allowed {documentType.MaxSizeAllowedInMB} MB.", 400));
}
string base64 = model.Attachment.Base64Data?.Split(',').LastOrDefault() ?? "";
if (string.IsNullOrWhiteSpace(base64))
{
_logger.LogWarning("Missing Base64 data for attachment.");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data missing", "File data required", 400));
}
var s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
var fileType = s3Service.GetContentTypeFromBase64(base64);
var validContentTypes = documentType.AllowedContentType.Split(',').ToList();
if (!validContentTypes.Contains(fileType))
{
_logger.LogWarning("Unsupported file type {FileType} for DocumentType {DocumentTypeId}", fileType, documentType.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Unsupported file type", $"Unsupported file type: {fileType}", 400));
}
// Generate S3 ObjectKey/FileName
string folderName = entityType == EmployeeEntity ? "EmployeeDocuments" : "ProjectDocuments";
string fileName = s3Service.GenerateFileName(fileType, tenantId, folderName);
string objectKey = entityType == EmployeeEntity
? $"tenant-{tenantId}/Employee/{model.EntityId}/{folderName}/{fileName}"
: $"tenant-{tenantId}/project-{model.EntityId}/{folderName}/{fileName}";
// Fire-and-forget upload
_ = Task.Run(async () =>
{
var logger = scope.ServiceProvider.GetRequiredService<ILoggingService>();
try
{
await s3Service.UploadFileAsync(base64, fileType, objectKey);
logger.LogInfo("File uploaded successfully to S3: {ObjectKey}", objectKey);
}
catch (Exception ex)
{
logger.LogError(ex, "S3 upload failed for {ObjectKey}.", objectKey);
}
});
// Create Document record
var document = new Document
{
BatchId = Guid.NewGuid(),
UploadedById = loggedInEmployee.Id,
FileName = model.Attachment.FileName ?? fileName,
ContentType = model.Attachment.ContentType ?? fileType,
S3Key = objectKey,
FileSize = model.Attachment.FileSize,
UploadedAt = DateTime.UtcNow,
TenantId = tenantId
};
_context.Documents.Add(document);
attachment.DocumentDataId = document.Id;
_context.DocumentAttachments.Add(attachment);
//Process Versioning
var versionMapping = new AttachmentVersionMapping
{
ParentAttachmentId = attachment.Id,
ChildAttachmentId = attachment.Id,
Version = 1,
TenantId = tenantId
};
_context.AttachmentVersionMappings.Add(versionMapping);
// Process Tags
if (model.Tags?.Any() == true)
{
var names = model.Tags.Select(t => t.Name).ToList();
var existingTags = await _context.DocumentTagMasters
.Where(t => names.Contains(t.Name) && t.TenantId == tenantId)
.ToListAsync();
var attachmentTagMappings = new List<AttachmentTagMapping>();
foreach (var tag in model.Tags.Where(t => t.IsActive))
{
var existingTag = existingTags.FirstOrDefault(t => t.Name == tag.Name);
var tagEntity = existingTag ?? new DocumentTagMaster
{
Id = Guid.NewGuid(),
Name = tag.Name,
Description = tag.Name,
TenantId = tenantId
};
if (existingTag == null)
{
_context.DocumentTagMasters.Add(tagEntity);
}
attachmentTagMappings.Add(new AttachmentTagMapping
{
DocumentTagId = tagEntity.Id,
AttachmentId = attachment.Id,
TenantId = tenantId
});
}
_context.AttachmentTagMappings.AddRange(attachmentTagMappings);
}
await _context.SaveChangesAsync();
_logger.LogInfo("Document uploaded successfully. AttachmentId: {AttachmentId}, DocumentId: {DocumentId}", attachment.Id, document.Id);
var response = _mapper.Map<DocumentListVM>(attachment);
response.UploadedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
response.ParentAttachmentId = versionMapping.ParentAttachmentId;
response.Version = versionMapping.Version;
return Ok(ApiResponse<object>.SuccessResponse(response, "Document added successfully", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error during document upload.");
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An error occurred while uploading the document", 500));
}
}
/// <summary>
/// Verifies a document attachment by its ID. Checks permissions, logs the operation, and updates verification fields.
/// </summary>
/// <param name="id">Document Attachment ID (Guid)</param>
/// <param name="isVerify">Flag to verify or unverify the document (default: true)</param>
[HttpPost("verify/{id}")]
public async Task<IActionResult> VerifyDocumentAsync(Guid id, [FromQuery] bool isVerify = true)
{
// Begin: Create DbContext and DI scope
await using var _context = await _dbContextFactory.CreateDbContextAsync();
using var scope = _serviceScope.CreateScope();
try
{
// Get current logged-in employee for authentication/auditing
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var tenantId = loggedInEmployee.TenantId;
_logger.LogInfo("Attempting to verify document. EmployeeId: {EmployeeId}, DocumentId: {DocumentId}, IsVerify: {IsVerify}",
loggedInEmployee.Id, id, isVerify);
// Fetch active/current document by Id, TenantId, and relevant conditions
var documentAttachment = await _context.DocumentAttachments
.FirstOrDefaultAsync(da => da.Id == id && da.IsActive && da.IsCurrentVersion && da.TenantId == tenantId);
if (documentAttachment == null)
{
_logger.LogWarning("Document attachment not found. Requested Id: {DocumentId}, TenantId: {TenantId}", id, tenantId);
return NotFound(ApiResponse<object>.ErrorResponse("Attachment not found", "Attachment not found in database", 404));
}
// Permission service: check if employee is authorized to verify documents
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var hasVerifyPermission = await permissionService.HasPermission(PermissionsMaster.VerifyDocument, loggedInEmployee.Id);
if (!hasVerifyPermission)
{
_logger.LogWarning("Access denied for document verification. EmployeeId: {EmployeeId}, DocumentId: {DocumentId}", loggedInEmployee.Id, id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to verify this document", 403));
}
// Log existing entity state before update (for audit trail)
var updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
var existingEntityBson = updateLogHelper.EntityToBsonDocument(documentAttachment);
// Update document verification status and audit fields
documentAttachment.IsVerified = isVerify;
documentAttachment.VerifiedAt = DateTime.UtcNow;
documentAttachment.VerifiedById = loggedInEmployee.Id;
// Commit changes
await _context.SaveChangesAsync();
// Log the update to MongoDB for change tracking
await updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
{
EntityId = documentAttachment.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = existingEntityBson,
UpdatedAt = DateTime.UtcNow
}, Collection);
var versionMapping = await _context.AttachmentVersionMappings.FirstOrDefaultAsync(av => av.ChildAttachmentId == documentAttachment.Id);
var response = _mapper.Map<DocumentListVM>(documentAttachment);
if (versionMapping != null)
{
response.ParentAttachmentId = versionMapping.ParentAttachmentId;
response.Version = versionMapping.Version;
}
_logger.LogInfo("Document verified successfully. DocumentId: {DocumentId}, VerifiedBy: {EmployeeId}", documentAttachment.Id, loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(new { }, "Document is verified successfully", 200));
}
catch (Exception ex)
{
// Handle unexpected errors gracefully
_logger.LogError(ex, "Error occurred during document verification. DocumentId: {DocumentId}", id);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Server Error", "An error occurred while verifying the document", 500));
}
}
[HttpPut("edit/{id}")]
public async Task<IActionResult> UpdateDocumentAsync(Guid id, [FromBody] UpdateDocumentAttachmentDto model)
{
// Logger initialization at the start for consistent logging
using var scope = _serviceScope.CreateScope();
_logger.LogInfo("Start UpdateDocument API for AttachmentId: {AttachmentId}", id);
try
{
await using var _context = await _dbContextFactory.CreateDbContextAsync();
// Get logged-in employee details
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var permissionService = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var hasUploadPermission = await permissionService.HasPermission(PermissionsMaster.UploadDocument, loggedInEmployee.Id);
// Fetch the existing attachment
var oldAttachment = await _context.DocumentAttachments
.Include(da => da.DocumentType)
.ThenInclude(dt => dt!.DocumentCategory)
.FirstOrDefaultAsync(da => da.Id == id && da.IsActive && da.IsCurrentVersion && da.TenantId == tenantId);
if (oldAttachment == null)
{
_logger.LogWarning("Attachment not found for Id: {AttachmentId}", id);
return NotFound(ApiResponse<object>.ErrorResponse("Attachment not found", "Attachment not found in database", 404));
}
// Permission check: ensure uploader is authorized
if (!hasUploadPermission && loggedInEmployee.Id != oldAttachment.EntityId)
{
_logger.LogWarning("Access denied for EmployeeId: {EmployeeId}", loggedInEmployee.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to upload this document", 403));
}
// Validate the document type
var documentType = oldAttachment.DocumentType;
if (documentType == null)
{
_logger.LogWarning("Document type not found for AttachmentId: {AttachmentId}", id);
return NotFound(ApiResponse<object>.ErrorResponse("Document Type not found", "Document Type not found in database", 404));
}
// Mandatory DocumentID check
if (documentType.IsMandatory && string.IsNullOrWhiteSpace(model.DocumentId))
{
_logger.LogWarning("Document ID missing for mandatory DocumentTypeId: {DocumentTypeId}", documentType.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Document ID missing", "User must provide the document ID for this document", 400));
}
// DocumentID Regex validation
if (documentType.IsValidationRequired && !string.IsNullOrWhiteSpace(model.DocumentId) && !string.IsNullOrWhiteSpace(documentType.RegexExpression))
{
if (!Regex.IsMatch(model.DocumentId, documentType.RegexExpression))
{
_logger.LogWarning("Provided document ID does not match regex for DocumentTypeId: {DocumentTypeId}", documentType.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Document ID", "Provided document ID is not valid", 400));
}
}
// Validate entity existence and project-level permission
var entityType = documentType.DocumentCategory!.EntityTypeId;
bool entityExists;
if (entityType.Equals(EmployeeEntity))
{
entityExists = await _context.Employees.AnyAsync(e => e.Id == oldAttachment.EntityId && e.TenantId == tenantId);
}
else if (entityType.Equals(ProjectEntity))
{
entityExists = await _context.Projects.AnyAsync(p => p.Id == oldAttachment.EntityId && p.TenantId == tenantId);
if (entityExists)
{
entityExists = await permissionService.HasProjectPermission(loggedInEmployee, oldAttachment.EntityId);
}
}
else
{
entityExists = false;
}
if (!entityExists)
{
_logger.LogWarning("Entity not found (Employee/Project) for AttachmentId: {AttachmentId}", id);
return NotFound(ApiResponse<object>.ErrorResponse($"{(entityType == EmployeeEntity ? "Employee" : "Project")} Not Found", "Entity not found in database", 404));
}
// Prepare for versioning
var oldVersionMapping = await _context.AttachmentVersionMappings
.FirstOrDefaultAsync(av => av.ChildAttachmentId == oldAttachment.Id && av.TenantId == tenantId);
var updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
var existingEntityBson = updateLogHelper.EntityToBsonDocument(oldAttachment);
DocumentAttachment newAttachment;
AttachmentVersionMapping newVersionMapping;
if (model.Attachment != null)
{
// File size check
var allowedSize = documentType.MaxSizeAllowedInMB * 1024;
if (model.Attachment.FileSize > allowedSize)
{
_logger.LogWarning("Attachment exceeded max file size for DocumentTypeId: {DocumentTypeId}", documentType.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("File size limit exceeded", $"Max allowed {documentType.MaxSizeAllowedInMB} MB.", 400));
}
// Base64 validation
string base64 = model.Attachment.Base64Data?.Split(',').LastOrDefault() ?? "";
if (string.IsNullOrWhiteSpace(base64))
{
_logger.LogWarning("Base64 data missing for new attachment");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data missing", "File data required", 400));
}
// Content type verification
var s3Service = scope.ServiceProvider.GetRequiredService<S3UploadService>();
var fileType = s3Service.GetContentTypeFromBase64(base64);
var validContentTypes = documentType.AllowedContentType.Split(',').ToList();
if (!validContentTypes.Contains(fileType))
{
_logger.LogWarning("Unsupported file type: {FileType}", fileType);
return BadRequest(ApiResponse<object>.ErrorResponse("Unsupported file type", $"Unsupported file type: {fileType}", 400));
}
// S3 keys and folder structure
string folderName = entityType == EmployeeEntity ? "EmployeeDocuments" : "ProjectDocuments";
string fileName = s3Service.GenerateFileName(fileType, tenantId, folderName);
string objectKey = entityType == EmployeeEntity
? $"tenant-{tenantId}/Employee/{oldAttachment.EntityId}/{folderName}/{fileName}"
: $"tenant-{tenantId}/project-{oldAttachment.EntityId}/{folderName}/{fileName}";
// Asynchronous S3 upload with logging
_ = Task.Run(async () =>
{
var logger = scope.ServiceProvider.GetRequiredService<ILoggingService>();
try
{
await s3Service.UploadFileAsync(base64, fileType, objectKey);
logger.LogInfo("File uploaded successfully to S3: {ObjectKey}", objectKey);
}
catch (Exception ex)
{
logger.LogError(ex, "S3 upload failed for {ObjectKey}.", objectKey);
}
});
// Create Document record
var document = new Document
{
BatchId = Guid.NewGuid(),
UploadedById = loggedInEmployee.Id,
FileName = model.Attachment.FileName ?? fileName,
ContentType = model.Attachment.ContentType ?? fileType,
S3Key = objectKey,
FileSize = model.Attachment.FileSize,
UploadedAt = DateTime.UtcNow,
TenantId = tenantId
};
_context.Documents.Add(document);
if (oldAttachment.IsVerified == true)
{
// Record new document attachment as the current version
var attachment = new DocumentAttachment
{
Name = model.Name,
DocumentId = model.DocumentId,
Description = model.Description,
IsCurrentVersion = true,
EntityId = oldAttachment.EntityId,
DocumentDataId = document.Id,
UploadedAt = DateTime.UtcNow,
UploadedById = loggedInEmployee.Id,
DocumentTypeId = oldAttachment.DocumentTypeId,
TenantId = oldAttachment.TenantId
};
_context.DocumentAttachments.Add(attachment);
// Mark old version as not current
oldAttachment.IsCurrentVersion = false;
// Version mapping
AttachmentVersionMapping versionMapping;
if (oldVersionMapping != null)
{
versionMapping = new AttachmentVersionMapping
{
ParentAttachmentId = oldVersionMapping.ParentAttachmentId,
ChildAttachmentId = attachment.Id,
Version = (oldVersionMapping.Version + 1),
TenantId = tenantId
};
}
else
{
versionMapping = new AttachmentVersionMapping
{
ParentAttachmentId = attachment.Id,
ChildAttachmentId = attachment.Id,
Version = 1,
TenantId = tenantId
};
}
_context.AttachmentVersionMappings.Add(versionMapping);
newAttachment = attachment;
newVersionMapping = versionMapping;
_logger.LogInfo("Created new current version for AttachmentId: {AttachmentId}", attachment.Id);
}
else
{
oldAttachment.Name = model.Name;
oldAttachment.DocumentId = model.DocumentId;
oldAttachment.Description = model.Description;
oldAttachment.DocumentDataId = document.Id;
if (oldAttachment.IsVerified == true)
{
oldAttachment.IsVerified = null;
_logger.LogInfo("Reset verification flag for AttachmentId: {AttachmentId}", oldAttachment.Id);
}
oldAttachment.UpdatedAt = DateTime.UtcNow;
oldAttachment.UpdatedById = loggedInEmployee.Id;
newAttachment = oldAttachment;
newVersionMapping = oldVersionMapping ?? new AttachmentVersionMapping();
_logger.LogInfo("Attachment metadata updated for AttachmentId: {AttachmentId}", oldAttachment.Id);
}
}
else
{
// Update attachment metadata only (no file upload)
oldAttachment.Name = model.Name;
oldAttachment.DocumentId = model.DocumentId;
oldAttachment.Description = model.Description;
if (oldAttachment.IsVerified == true)
{
oldAttachment.IsVerified = null;
_logger.LogInfo("Reset verification flag for AttachmentId: {AttachmentId}", oldAttachment.Id);
}
oldAttachment.UpdatedAt = DateTime.UtcNow;
oldAttachment.UpdatedById = loggedInEmployee.Id;
newAttachment = oldAttachment;
newVersionMapping = oldVersionMapping ?? new AttachmentVersionMapping();
_logger.LogInfo("Attachment metadata updated for AttachmentId: {AttachmentId}", oldAttachment.Id);
}
// Tag management
if (model.Tags?.Any() == true)
{
var names = model.Tags.Select(t => t.Name).ToList();
var existingTags = await _context.DocumentTagMasters
.Where(t => names.Contains(t.Name) && t.TenantId == tenantId)
.ToListAsync();
var attachmentTagMappings = new List<AttachmentTagMapping>();
var oldTags = await _context.AttachmentTagMappings
.Include(dt => dt.DocumentTag)
.Where(dt => dt.DocumentTag != null && dt.AttachmentId == newAttachment.Id && dt.TenantId == tenantId)
.ToListAsync();
var oldTagNames = oldTags.Select(dt => dt.DocumentTag!.Name).ToList();
foreach (var tag in model.Tags.Where(t => t.IsActive && !oldTagNames.Contains(t.Name)))
{
var existingTag = existingTags.FirstOrDefault(t => t.Name == tag.Name);
var tagEntity = existingTag ?? new DocumentTagMaster
{
Id = Guid.NewGuid(),
Name = tag.Name,
Description = tag.Name,
TenantId = tenantId
};
if (existingTag == null)
{
_context.DocumentTagMasters.Add(tagEntity);
}
attachmentTagMappings.Add(new AttachmentTagMapping
{
DocumentTagId = tagEntity.Id,
AttachmentId = newAttachment.Id,
TenantId = tenantId
});
}
_context.AttachmentTagMappings.AddRange(attachmentTagMappings);
var deletedTagMappings = new List<AttachmentTagMapping>();
foreach (var tag in model.Tags.Where(t => !t.IsActive && oldTagNames.Contains(t.Name)))
{
var deletedTagMapping = oldTags.FirstOrDefault(at => at.DocumentTag!.Name == tag.Name);
if (deletedTagMapping != null)
{
deletedTagMappings.Add(deletedTagMapping);
}
}
_context.AttachmentTagMappings.RemoveRange(deletedTagMappings);
_logger.LogInfo("Tags processed for AttachmentId: {AttachmentId}", newAttachment.Id);
}
// Persist changes to database
await _context.SaveChangesAsync();
_logger.LogInfo("Database changes committed for AttachmentId: {AttachmentId}", newAttachment.Id);
// Update logs
await updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
{
EntityId = oldAttachment.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = existingEntityBson,
UpdatedAt = DateTime.UtcNow
}, Collection);
_logger.LogInfo("Update logs pushed for AttachmentId: {AttachmentId}", oldAttachment.Id);
// Prepare response
var response = _mapper.Map<DocumentListVM>(newAttachment);
response.UploadedBy = _mapper.Map<BasicEmployeeVM>(loggedInEmployee);
response.ParentAttachmentId = newVersionMapping.ParentAttachmentId;
response.Version = newVersionMapping.Version;
_logger.LogInfo("API completed successfully for AttachmentId: {AttachmentId}", newAttachment.Id);
return Ok(ApiResponse<object>.SuccessResponse(response, "Document Updated successfully", 200));
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occurred while updating document for AttachmentId: {AttachmentId}", id);
return StatusCode(500, ApiResponse<object>.ErrorResponse("Exception occured", "Exception occured while the updating document", 500));
}
}
[HttpDelete("delete/{id}")]
public async Task<IActionResult> DeleteDocumentAsync(Guid id, [FromQuery] bool isActive = false)
{
// Create a new DbContext instance asynchronously
await using var _context = await _dbContextFactory.CreateDbContextAsync();
// Create a new service scope to resolve scoped services like permission and logging helpers
using var scope = _serviceScope.CreateScope();
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
var updateLogHelper = scope.ServiceProvider.GetRequiredService<UtilityMongoDBHelper>();
// Message to indicate whether the document is being activated or deactivated
var message = isActive ? "activated" : "deactivated";
// Get the currently logged-in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Log the start of the delete operation for traceability
_logger.LogInfo("DeleteDocument started for document ID: {DocumentId} by employee ID: {EmployeeId}", id, loggedInEmployee.Id);
// Retrieve the document attachment matching the criteria from the database
var documentAttachment = await _context.DocumentAttachments
.FirstOrDefaultAsync(da => da.Id == id && da.IsCurrentVersion && da.TenantId == tenantId && da.IsActive != isActive);
// If the document attachment is not found, log a warning and return 404 Not Found
if (documentAttachment == null)
{
_logger.LogWarning("DocumentAttachment not found for ID: {DocumentId}", id);
return NotFound(ApiResponse<object>.ErrorResponse("Document Attachment not found", "Document Attachment not found in database", 404));
}
// Check if the logged in employee has permission to delete OR is the owner of the document attachment
var hasDeletePermission = await _permission.HasPermission(PermissionsMaster.DeleteDocument, loggedInEmployee.Id);
if (!hasDeletePermission && loggedInEmployee.Id != documentAttachment.EntityId)
{
_logger.LogWarning("Access denied for employee ID: {EmployeeId} when attempting to delete document ID: {DocumentId}", loggedInEmployee.Id, id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("Access Denied.", "You do not have permission to delete documents", 403));
}
// Log the current state of the document attachment before updating for audit trail
var existingEntityBson = updateLogHelper.EntityToBsonDocument(documentAttachment);
// Update document attachment status and metadata
documentAttachment.IsActive = isActive;
documentAttachment.IsVerified = null;
documentAttachment.UpdatedAt = DateTime.UtcNow;
documentAttachment.UpdatedById = loggedInEmployee.Id;
// Persist changes to the database
await _context.SaveChangesAsync();
// Log the update operation to MongoDB for inspection and history
await updateLogHelper.PushToUpdateLogsAsync(new UpdateLogsObject
{
EntityId = documentAttachment.Id.ToString(),
UpdatedById = loggedInEmployee.Id.ToString(),
OldObject = existingEntityBson,
UpdatedAt = DateTime.UtcNow
}, Collection);
// Log the successful completion of the operation
_logger.LogInfo("DocumentAttachment ID: {DocumentId} has been {Message} by employee ID: {EmployeeId}", id, message, loggedInEmployee.Id);
// Return success response
return Ok(ApiResponse<object>.SuccessResponse(new { }, $"Document attachment is {message}", 200));
}
#region =================================================================== Helper Functions ===================================================================
private DocumentFilter? TryDeserializeFilter(string? filter)
{
if (string.IsNullOrWhiteSpace(filter))
{
return null;
}
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
DocumentFilter? documentFilter = null;
try
{
// First, try to deserialize directly. This is the expected case (e.g., from a web client).
documentFilter = JsonSerializer.Deserialize<DocumentFilter>(filter, options);
}
catch (JsonException ex)
{
_logger.LogError(ex, "[{MethodName}] Failed to directly deserialize filter. Attempting to unescape and re-parse. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
// If direct deserialization fails, it might be an escaped string (common with tools like Postman or some mobile clients).
try
{
// Unescape the string first, then deserialize the result.
string unescapedJsonString = JsonSerializer.Deserialize<string>(filter, options) ?? "";
if (!string.IsNullOrWhiteSpace(unescapedJsonString))
{
documentFilter = JsonSerializer.Deserialize<DocumentFilter>(unescapedJsonString, options);
}
}
catch (JsonException ex1)
{
// If both attempts fail, log the final error and return null.
_logger.LogError(ex1, "[{MethodName}] All attempts to deserialize the filter failed. Filter will be ignored. Filter: {Filter}", nameof(TryDeserializeFilter), filter);
return null;
}
}
return documentFilter;
}
#endregion
}
}