using AutoMapper; using Marco.Pms.CacheHelper; using Marco.Pms.DataAccess.Data; using Marco.Pms.Model.Dtos.Tenant; using Marco.Pms.Model.Employees; using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Projects; using Marco.Pms.Model.Roles; using Marco.Pms.Model.TenantModel; using Marco.Pms.Model.TenantModels; using Marco.Pms.Model.TenantModels.MongoDBModel; using Marco.Pms.Model.Utilities; using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Tenant; using Marco.Pms.Services.Service; using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Service; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Net; using System.Text.Json; // For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 namespace Marco.Pms.Services.Controllers { [Route("api/[controller]")] [ApiController] [Authorize] public class TenantController : ControllerBase { private readonly IDbContextFactory _dbContextFactory; private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ILoggingService _logger; private readonly UserManager _userManager; private readonly IMapper _mapper; private readonly UserHelper _userHelper; private readonly FeatureDetailsHelper _featureDetailsHelper; private readonly static Guid activeStatus = Guid.Parse("62b05792-5115-4f99-8ff5-e8374859b191"); private readonly static Guid activePlanStatus = Guid.Parse("cd3a68ea-41fd-42f0-bd0c-c871c7337727"); private readonly static string AdminRoleName = "Admin"; public TenantController(IDbContextFactory dbContextFactory, IServiceScopeFactory serviceScopeFactory, ILoggingService logger, UserManager userManager, IMapper mapper, UserHelper userHelper, FeatureDetailsHelper featureDetailsHelper) { _dbContextFactory = dbContextFactory; _serviceScopeFactory = serviceScopeFactory; _logger = logger; _userManager = userManager; _mapper = mapper; _userHelper = userHelper; _featureDetailsHelper = featureDetailsHelper; } #region =================================================================== Tenant APIs =================================================================== /// /// Retrieves a paginated list of active tenants with optional filtering and searching. /// /// A string to search for across various tenant fields. /// A JSON serialized string containing advanced filter criteria. /// The number of records to return per page. /// The page number to retrieve. /// A paginated list of tenants matching the criteria. [HttpGet("list")] public async Task GetTenantList([FromQuery] string? searchString, string? filter, int pageSize = 20, int pageNumber = 1) { using var scope = _serviceScopeFactory.CreateScope(); var _permissionService = scope.ServiceProvider.GetRequiredService(); _logger.LogInfo("Attempting to fetch tenant list with pageNumber: {PageNumber} and pageSize: {PageSize}", pageNumber, pageSize); try { // --- 1. PERMISSION CHECK --- var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (loggedInEmployee == null) { // This case should be handled by the [Authorize] attribute. // This check is a safeguard. _logger.LogWarning("Authentication failed: No logged-in employee found."); return StatusCode(403, ApiResponse.ErrorResponse("Authentication required", "User is not logged in.", 403)); } // A root user should have access regardless of the specific permission. var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false; var hasPermission = await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id); if (!hasPermission || !isRootUser) { _logger.LogWarning("Permission denied: User {EmployeeId} attempted to list tenants without 'ManageTenants' permission or root access.", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403)); } // --- 2. QUERY CONSTRUCTION --- // Using a DbContext from the factory ensures a fresh context for this request. await using var _context = await _dbContextFactory.CreateDbContextAsync(); // Start with a base IQueryable. Filters will be appended to this. var tenantQuery = _context.Tenants.Where(t => t.IsActive); // Apply advanced filters from the JSON filter object. var tenantFilter = TryDeserializeFilter(filter); if (tenantFilter != null) { // Date range filtering if (tenantFilter.StartDate.HasValue && tenantFilter.EndDate.HasValue) { // OPTIMIZATION: Avoid using .Date on the database column, as it can prevent index usage. // This structure (`>= start` and `< end.AddDays(1)`) is index-friendly and correctly inclusive. var endDateExclusive = tenantFilter.EndDate.Value.AddDays(1); tenantQuery = tenantQuery.Where(t => t.OnBoardingDate >= tenantFilter.StartDate.Value && t.OnBoardingDate < endDateExclusive); } // List-based filtering if (tenantFilter.IndustryIds?.Any() == true) { tenantQuery = tenantQuery.Where(t => t.IndustryId.HasValue && tenantFilter.IndustryIds.Contains(t.IndustryId.Value)); } if (tenantFilter.References?.Any() == true) { tenantQuery = tenantQuery.Where(t => tenantFilter.References.Contains(t.Reference)); } if (tenantFilter.TenantStatusIds?.Any() == true) { tenantQuery = tenantQuery.Where(t => tenantFilter.TenantStatusIds.Contains(t.TenantStatusId)); } if (tenantFilter.CreatedByIds?.Any() == true) { tenantQuery = tenantQuery.Where(t => t.CreatedById.HasValue && tenantFilter.CreatedByIds.Contains(t.CreatedById.Value)); } } // Apply free-text search string. if (!string.IsNullOrWhiteSpace(searchString)) { // OPTIMIZATION: Do not use .ToLower() on the database columns (e.g., `t.Name.ToLower()`). // This makes the query non-SARGable and kills performance by preventing index usage. // This implementation relies on the database collation being case-insensitive (e.g., `SQL_Latin1_General_CP1_CI_AS` in SQL Server). tenantQuery = tenantQuery.Where(t => t.Name.Contains(searchString) || t.ContactName.Contains(searchString) || t.Email.Contains(searchString) || t.ContactNumber.Contains(searchString) || t.BillingAddress.Contains(searchString) || (t.TaxId != null && t.TaxId.Contains(searchString)) || (t.Description != null && t.Description.Contains(searchString)) || (t.DomainName != null && t.DomainName.Contains(searchString)) ); } // --- 3. PAGINATION AND EXECUTION --- // First, get the total count of records for pagination metadata. // This executes a separate, efficient `COUNT(*)` query. int totalRecords = await tenantQuery.CountAsync(); int totalPages = (int)Math.Ceiling((double)totalRecords / pageSize); _logger.LogInfo("Found {TotalRecords} total tenants matching the query.", totalRecords); // Now, apply ordering and pagination to fetch only the data for the current page. // This is efficient server-side pagination. var tenantList = await tenantQuery .Include(t => t.Industry) // Eager load related data to prevent N+1 queries. .Include(t => t.TenantStatus) .OrderByDescending(t => t.OnBoardingDate) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToListAsync(); // Map the entities to a ViewModel/DTO for the response. var vm = _mapper.Map>(tenantList); // --- 4. CONSTRUCT RESPONSE --- var response = new { TotalCount = totalRecords, TotalPages = totalPages, CurrentPage = pageNumber, PageSize = pageSize, Filter = tenantFilter, // Return the applied filter for context on the client-side. Data = vm }; _logger.LogInfo("Successfully fetched {RecordCount} tenant records.", vm.Count); return Ok(ApiResponse.SuccessResponse(response, $"{totalRecords} records of tenants fetched successfully", 200)); } catch (Exception ex) { // CRITICAL SECURITY FIX: Do not expose the exception details to the client. // Log the full exception for debugging purposes. _logger.LogError(ex, "An unhandled exception occurred while fetching the tenant list."); // Return a generic 500 Internal Server Error response. return StatusCode(500, ApiResponse.ErrorResponse("An internal server error occurred.", "An unexpected error prevented the request from completing.", 500)); } } // GET api//5 [HttpGet("details/{id}")] public async Task GetDetails(Guid id) { return Ok(); } // POST api/ [HttpPost("create")] public async Task CreateTenant([FromBody] CreateTenantDto model) { using var scope = _serviceScopeFactory.CreateScope(); await using var _context = await _dbContextFactory.CreateDbContextAsync(); var _configuration = scope.ServiceProvider.GetRequiredService(); var _emailSender = scope.ServiceProvider.GetRequiredService(); var _permissionService = scope.ServiceProvider.GetRequiredService(); _logger.LogInfo("Attempting to create a new tenant with organization name: {OrganizationName}", model.OrganizationName); // 1. --- PERMISSION CHECK --- var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); if (loggedInEmployee == null) { // This case should ideally be handled by an [Authorize] attribute, but it's good practice to double-check. return Unauthorized(ApiResponse.ErrorResponse("Authentication required", "User is not logged in.", 401)); } var hasPermission = await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id); if (!hasPermission || !(loggedInEmployee.ApplicationUser?.IsRootUser ?? false)) { _logger.LogWarning("Permission denied: User {EmployeeId} attempted to create a tenant without sufficient rights.", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403)); } // 2. --- VALIDATION --- // Check if a user with the same email already exists. var existingUser = await _userManager.FindByEmailAsync(model.Email); if (existingUser != null) { _logger.LogWarning("Tenant creation failed for email {Email}: an application user with this email already exists.", model.Email); return StatusCode(409, ApiResponse.ErrorResponse("Tenant cannot be created", "A user with the specified email already exists.", 409)); } // Check if a tenant with the same Tax ID and Domain Name already exists. var taxTask = Task.Run(async () => { if (!string.IsNullOrWhiteSpace(model.TaxId)) { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.Tenants.AnyAsync(t => t.TaxId == model.TaxId); } return false; }); var domainTask = Task.Run(async () => { if (!string.IsNullOrWhiteSpace(model.DomainName)) { await using var context = await _dbContextFactory.CreateDbContextAsync(); return await context.Tenants.AnyAsync(t => t.DomainName == model.DomainName); } return false; }); await Task.WhenAll(taxTask, domainTask); if (taxTask.Result || domainTask.Result) { if (!string.IsNullOrWhiteSpace(model.TaxId)) { _logger.LogWarning("Tenant creation failed for Tax ID {TaxId}: a tenant with this Tax ID already exists.", model.TaxId); } if (!string.IsNullOrWhiteSpace(model.DomainName)) { _logger.LogWarning("Tenant creation failed for Domain Name {DomainName}: a tenant with this Domain Name already exists.", model.DomainName); } return StatusCode(409, ApiResponse.ErrorResponse("Tenant cannot be created", "A tenant already exists.", 409)); } // Check if the provided logo is a valid Base64 string. if (!string.IsNullOrWhiteSpace(model.logoImage) && !IsBase64String(model.logoImage)) { _logger.LogWarning("Tenant creation failed for user {EmployeeId}: The provided logo image was not a valid Base64 string.", loggedInEmployee.Id); return StatusCode(400, ApiResponse.ErrorResponse("Tenant cannot be created", "The provided logo image is invalid.", 400)); } // 3. --- DATABASE TRANSACTION --- // Use a transaction to ensure all related entities are created successfully or none at all. await using var transaction = await _context.Database.BeginTransactionAsync(); try { // Create the primary Tenant entity var tenant = _mapper.Map(model); tenant.TenantStatusId = activeStatus; tenant.CreatedById = loggedInEmployee.Id; tenant.IsSuperTenant = false; _context.Tenants.Add(tenant); // Create the root ApplicationUser for the new tenant var applicationUser = new ApplicationUser { Email = model.Email, UserName = model.Email, // Best practice to use email as username for simplicity IsRootUser = true, EmailConfirmed = true, // Auto-confirming email as it's part of a trusted setup process TenantId = tenant.Id }; // SECURITY WARNING: Hardcoded passwords are a major vulnerability. // Replace "User@123" with a securely generated random password. var initialPassword = "User@123"; // TODO: Replace with password generation service. var result = await _userManager.CreateAsync(applicationUser, initialPassword); if (!result.Succeeded) { // If user creation fails, roll back the transaction immediately and return the errors. await transaction.RollbackAsync(); var errors = result.Errors.Select(e => e.Description).ToList(); _logger.LogWarning("Failed to create ApplicationUser for tenant {TenantName}. Errors: {Errors}", model.OrganizationName, string.Join(", ", errors)); return BadRequest(ApiResponse.ErrorResponse("Failed to create user", errors, 400)); } // Create the default "Admin" Job Role for the tenant var adminJobRole = new JobRole { Name = AdminRoleName, Description = "Default administrator role for the tenant.", TenantId = tenant.Id }; _context.JobRoles.Add(adminJobRole); // Create the primary Employee record and link it to the ApplicationUser and JobRole var employeeUser = new Employee { FirstName = model.FirstName, LastName = model.LastName, Email = model.Email, PhoneNumber = model.ContactNumber, JoiningDate = model.OnBoardingDate, ApplicationUserId = applicationUser.Id, JobRole = adminJobRole, // Link to the newly created role CurrentAddress = model.BillingAddress, TenantId = tenant.Id }; _context.Employees.Add(employeeUser); var applicationRole = new ApplicationRole { Role = "Super User", Description = "Super User", IsSystem = true, TenantId = tenant.Id }; _context.ApplicationRoles.Add(applicationRole); var rolePermissionMappigs = new List { new RolePermissionMappings { ApplicationRoleId = applicationRole.Id, FeaturePermissionId = PermissionsMaster.ModifyTenant }, new RolePermissionMappings { ApplicationRoleId = applicationRole.Id, FeaturePermissionId = PermissionsMaster.ViewTenant }, new RolePermissionMappings { ApplicationRoleId = applicationRole.Id, FeaturePermissionId = PermissionsMaster.ManageMasters }, new RolePermissionMappings { ApplicationRoleId = applicationRole.Id, FeaturePermissionId = PermissionsMaster.ViewMasters } }; _context.RolePermissionMappings.AddRange(rolePermissionMappigs); _context.EmployeeRoleMappings.Add(new EmployeeRoleMapping { EmployeeId = employeeUser.Id, RoleId = applicationRole.Id, IsEnabled = true, TenantId = tenant.Id }); // Create a default project for the new tenant var project = new Project { Name = "Default Project", ProjectStatusId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"), // Consider using a constant for this GUID ProjectAddress = model.BillingAddress, StartDate = model.OnBoardingDate, EndDate = DateTime.MaxValue, ContactPerson = tenant.ContactName, TenantId = tenant.Id }; _context.Projects.Add(project); // All entities are now added to the context. Save them all in a single database operation. await _context.SaveChangesAsync(); // 4. --- POST-CREATION ACTIONS --- // Generate a password reset token so the new user can set their own password. _logger.LogInfo("User {Email} created. Sending password setup email.", applicationUser.Email); var token = await _userManager.GeneratePasswordResetTokenAsync(applicationUser); var resetLink = $"{_configuration["AppSettings:WebFrontendUrl"]}/reset-password?token={WebUtility.UrlEncode(token)}&email={WebUtility.UrlEncode(applicationUser.Email)}"; await _emailSender.SendResetPasswordEmailOnRegister(applicationUser.Email, employeeUser.FirstName, resetLink); // Map the result to a ViewModel for the API response. var tenantVM = _mapper.Map(tenant); tenantVM.CreatedBy = _mapper.Map(loggedInEmployee); // Commit the transaction as all operations were successful. await transaction.CommitAsync(); _logger.LogInfo("Successfully created tenant {TenantId} for organization {OrganizationName}.", tenant.Id, tenant.Name); return StatusCode(201, ApiResponse.SuccessResponse(tenantVM, "Tenant created successfully.", 201)); } catch (DbUpdateException dbEx) { await transaction.RollbackAsync(); // Log the detailed database exception, including the inner exception if available. _logger.LogError(dbEx, "A database update exception occurred while creating tenant for email {Email}. Inner Exception: {InnerException}", model.Email, dbEx.InnerException?.Message ?? string.Empty); return StatusCode(500, ApiResponse.ErrorResponse("An internal database error occurred.", ExceptionMapper(dbEx), 500)); } catch (Exception ex) { await transaction.RollbackAsync(); // Log the general exception. _logger.LogError(ex, "An unexpected exception occurred while creating tenant for email {Email}.", model.Email); return StatusCode(500, ApiResponse.ErrorResponse("An unexpected internal error occurred.", ExceptionMapper(ex), 500)); } } // PUT api//5 [HttpPut("{id}")] public void Put(int id, [FromBody] string value) { } // DELETE api//5 [HttpDelete("{id}")] public void Delete(int id) { } #endregion #region =================================================================== Subscription APIs =================================================================== [HttpPost("add-subscription")] public async Task AddSubscription(AddSubscriptionDto model) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _permissionService = scope.ServiceProvider.GetRequiredService(); // A root user should have access regardless of the specific permission. var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false; var hasPermission = await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id); if (!hasPermission || !isRootUser) { _logger.LogWarning("Permission denied: User {EmployeeId} attempted to list tenants without 'ManageTenants' permission or root access.", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403)); } var tenantSubscription = new TenantSubscriptions { TenantId = model.TenantId, PlanId = model.PlanId, StatusId = activePlanStatus, CreatedAt = DateTime.UtcNow, CreatedById = loggedInEmployee.Id, CurrencyId = model.CurrencyId, IsTrial = model.IsTrial, StartDate = DateTime.UtcNow, AutoRenew = model.AutoRenew }; switch (model.Frequency) { case PLAN_FREQUENCY.MONTHLY: tenantSubscription.EndDate = DateTime.UtcNow.AddMonths(1); tenantSubscription.NextBillingDate = DateTime.UtcNow.AddMonths(1); break; case PLAN_FREQUENCY.QUARTERLY: tenantSubscription.EndDate = DateTime.UtcNow.AddMonths(3); tenantSubscription.NextBillingDate = DateTime.UtcNow.AddMonths(3); break; case PLAN_FREQUENCY.HALF_MONTHLY: tenantSubscription.EndDate = DateTime.UtcNow.AddMonths(6); tenantSubscription.NextBillingDate = DateTime.UtcNow.AddMonths(6); break; case PLAN_FREQUENCY.YEARLY: tenantSubscription.EndDate = DateTime.UtcNow.AddMonths(12); tenantSubscription.NextBillingDate = DateTime.UtcNow.AddMonths(12); break; } _context.TenantSubscriptions.Add(tenantSubscription); await _context.SaveChangesAsync(); return Ok(ApiResponse.SuccessResponse(tenantSubscription, "Tenant Subscription Successfully", 200)); } #endregion #region =================================================================== Subscription Plan APIs =================================================================== [HttpGet("list/subscription-plan")] public async Task GetSubscriptionPlanList([FromQuery] int frequency) { await using var _context = await _dbContextFactory.CreateDbContextAsync(); var plans = await _context.SubscriptionPlans.Include(s => s.Currency).ToListAsync(); var vm = await Task.WhenAll(plans.Select(async p => { var response = _mapper.Map(p); switch (frequency) { case 0: response.Price = p.PriceMonthly; break; case 1: response.Price = p.PriceMonthly; break; case 2: response.Price = p.PriceHalfYearly; break; case 3: response.Price = p.PriceYearly; break; } response.Features = await _featureDetailsHelper.GetFeatureDetails(p.FeaturesId); return response; }).ToList()); return Ok(ApiResponse.SuccessResponse(vm, "List of plans fetched successfully", 200)); } [HttpPost("create/subscription-plan")] public async Task CreateSubscriptionPlan([FromBody] SubscriptionPlanDto model) { var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); await using var _context = await _dbContextFactory.CreateDbContextAsync(); using var scope = _serviceScopeFactory.CreateScope(); var _permissionService = scope.ServiceProvider.GetRequiredService(); // A root user should have access regardless of the specific permission. var isRootUser = loggedInEmployee.ApplicationUser?.IsRootUser ?? false; var hasPermission = await _permissionService.HasPermission(PermissionsMaster.ManageTenants, loggedInEmployee.Id); if (!hasPermission || !isRootUser) { _logger.LogWarning("Permission denied: User {EmployeeId} attempted to list tenants without 'ManageTenants' permission or root access.", loggedInEmployee.Id); return StatusCode(403, ApiResponse.ErrorResponse("Access denied", "User does not have the required permissions for this action.", 403)); } var currencyMaster = await _context.CurrencyMaster.AsNoTracking().FirstOrDefaultAsync(c => c.Id == model.CurrencyId); if (currencyMaster == null) { return NotFound(ApiResponse.ErrorResponse("Currency not found", "Currency not found", 404)); } var plan = _mapper.Map(model); var features = _mapper.Map(model.Features); try { await _featureDetailsHelper.AddFeatureDetails(features); } catch (Exception ex) { _logger.LogError(ex, "Exception occured while saving feature in mongoDB"); return StatusCode(500, ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(ex), 500)); } plan.FeaturesId = features.Id; plan.CreatedById = loggedInEmployee.Id; plan.CreateAt = DateTime.UtcNow; _context.SubscriptionPlans.Add(plan); try { await _context.SaveChangesAsync(); } catch (DbUpdateException dbEx) { _logger.LogError(dbEx, "Database Exception occured while saving subscription plan"); return StatusCode(500, ApiResponse.ErrorResponse("Internal Error occured", ExceptionMapper(dbEx), 500)); } var response = _mapper.Map(plan); response.Features = features; response.Currency = currencyMaster; return StatusCode(201, ApiResponse.SuccessResponse(response, "Plan Created Successfully", 201)); } #endregion #region =================================================================== Helper Functions =================================================================== private static object ExceptionMapper(Exception ex) { return new { Message = ex.Message, StackTrace = ex.StackTrace, Source = ex.Source, InnerException = new { Message = ex.InnerException?.Message, StackTrace = ex.InnerException?.StackTrace, Source = ex.InnerException?.Source, } }; } private bool IsBase64String(string? input) { if (string.IsNullOrWhiteSpace(input)) { return false; } string base64Data = input; const string dataUriMarker = "base64,"; int markerIndex = input.IndexOf(dataUriMarker, StringComparison.Ordinal); // If the marker is found, extract the actual Base64 data if (markerIndex >= 0) { base64Data = input.Substring(markerIndex + dataUriMarker.Length); } // Now, validate the extracted payload base64Data = base64Data.Trim(); // Check for valid length (must be a multiple of 4) and non-empty if (base64Data.Length == 0 || base64Data.Length % 4 != 0) { return false; } // The most reliable test is to simply try to convert it. // The .NET converter is strict and will throw a FormatException // for invalid characters or incorrect padding. try { Convert.FromBase64String(base64Data); return true; } catch (FormatException) { // The string is not a valid Base64 payload. return false; } } private TenantFilter? TryDeserializeFilter(string? filter) { if (string.IsNullOrWhiteSpace(filter)) { return null; } var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; TenantFilter? expenseFilter = null; try { // First, try to deserialize directly. This is the expected case (e.g., from a web client). expenseFilter = JsonSerializer.Deserialize(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(filter, options) ?? ""; if (!string.IsNullOrWhiteSpace(unescapedJsonString)) { expenseFilter = JsonSerializer.Deserialize(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 expenseFilter; } #endregion } }