From b78f58c304b27aa33eafa5b1fb939e14e8b03e4f Mon Sep 17 00:00:00 2001 From: "ashutosh.nehete" Date: Mon, 14 Jul 2025 15:08:31 +0530 Subject: [PATCH] Solved Concurrency Issue --- Marco.Pms.CacheHelper/EmployeeCache.cs | 19 +------- .../Helpers/CacheUpdateHelper.cs | 23 +++++++++- Marco.Pms.Services/Helpers/RolesHelper.cs | 43 ++++++++++--------- 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/Marco.Pms.CacheHelper/EmployeeCache.cs b/Marco.Pms.CacheHelper/EmployeeCache.cs index c2a1f7b..4a668f0 100644 --- a/Marco.Pms.CacheHelper/EmployeeCache.cs +++ b/Marco.Pms.CacheHelper/EmployeeCache.cs @@ -20,29 +20,12 @@ namespace Marco.Pms.CacheHelper var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name _collection = mongoDB.GetCollection("EmployeeProfile"); } - public async Task AddApplicationRoleToCache(Guid employeeId, List roleIds) + public async Task AddApplicationRoleToCache(Guid employeeId, List newRoleIds, List newPermissionIds) { - // 1. Guard Clause: Avoid unnecessary database work if there are no roles to add. - if (roleIds == null || !roleIds.Any()) - { - return false; // Nothing to add, so the operation did not result in a change. - } // 2. Perform database queries concurrently for better performance. var employeeIdString = employeeId.ToString(); - Task> getPermissionIdsTask = _context.RolePermissionMappings - .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) - .Select(p => p.FeaturePermissionId.ToString()) - .Distinct() - .ToListAsync(); - - // 3. Prepare role IDs in parallel with the database query. - var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); - - // 4. Await the database query result. - var newPermissionIds = await getPermissionIdsTask; - // 5. Build a single, efficient update operation. var filter = Builders.Filter.Eq(e => e.Id, employeeIdString); diff --git a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs index 4369b5b..5bae90f 100644 --- a/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs +++ b/Marco.Pms.Services/Helpers/CacheUpdateHelper.cs @@ -641,9 +641,30 @@ namespace Marco.Pms.Services.Helpers // ------------------------------------ Employee Profile Cache --------------------------------------- public async Task AddApplicationRole(Guid employeeId, List roleIds) { + // 1. Guard Clause: Avoid unnecessary database work if there are no roles to add. + if (roleIds == null || !roleIds.Any()) + { + return; // Nothing to add, so the operation did not result in a change. + } + Task> getPermissionIdsTask = Task.Run(async () => + { + using var context = _dbContextFactory.CreateDbContext(); + + return await context.RolePermissionMappings + .Where(rp => roleIds.Contains(rp.ApplicationRoleId)) + .Select(p => p.FeaturePermissionId.ToString()) + .Distinct() + .ToListAsync(); + }); + + // 3. Prepare role IDs in parallel with the database query. + var newRoleIds = roleIds.Select(r => r.ToString()).ToList(); + + // 4. Await the database query result. + var newPermissionIds = await getPermissionIdsTask; try { - var response = await _employeeCache.AddApplicationRoleToCache(employeeId, roleIds); + var response = await _employeeCache.AddApplicationRoleToCache(employeeId, newRoleIds, newPermissionIds); } catch (Exception ex) { diff --git a/Marco.Pms.Services/Helpers/RolesHelper.cs b/Marco.Pms.Services/Helpers/RolesHelper.cs index 1688dce..cd73c0f 100644 --- a/Marco.Pms.Services/Helpers/RolesHelper.cs +++ b/Marco.Pms.Services/Helpers/RolesHelper.cs @@ -10,14 +10,16 @@ namespace MarcoBMS.Services.Helpers { public class RolesHelper { + private readonly IDbContextFactory _dbContextFactory; private readonly ApplicationDbContext _context; private readonly CacheUpdateHelper _cache; private readonly ILoggingService _logger; - public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache, ILoggingService logger) + public RolesHelper(ApplicationDbContext context, CacheUpdateHelper cache, ILoggingService logger, IDbContextFactory dbContextFactory) { _context = context; _cache = cache; _logger = logger; + _dbContextFactory = dbContextFactory; } /// @@ -32,56 +34,57 @@ namespace MarcoBMS.Services.Helpers try { - // --- Step 1: Define the subquery for the employee's roles --- - // This is an IQueryable, not a list. It will be composed directly into the main query - // by Entity Framework, avoiding a separate database call. + // --- Step 1: Define the subquery using the main thread's context --- + // This is safe because the query is not executed yet. var employeeRoleIdsQuery = _context.EmployeeRoleMappings - .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled == true) + .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled) .Select(erm => erm.RoleId); - // --- Step 2: Asynchronously update the cache in the background (Fire and Forget) --- - // This task is started but not awaited. The main function continues immediately, - // reducing latency. The cache will be updated eventually without blocking the user. + // --- Step 2: Asynchronously update the cache using the DbContextFactory --- _ = Task.Run(async () => { try { - var roleIds = await employeeRoleIdsQuery.ToListAsync(); // Execute the query for the cache + // Create a NEW, short-lived DbContext instance for this background task. + await using var contextForCache = await _dbContextFactory.CreateDbContextAsync(); + + // Now, re-create and execute the query using this new, isolated context. + var roleIds = await contextForCache.EmployeeRoleMappings + .Where(erm => erm.EmployeeId == EmployeeId && erm.IsEnabled) + .Select(erm => erm.RoleId) + .ToListAsync(); + if (roleIds.Any()) { + // The cache service might also need its own context, or you can pass the data directly. + // Assuming AddApplicationRole takes the data, not a context. await _cache.AddApplicationRole(EmployeeId, roleIds); _logger.LogInfo("Successfully queued cache update for EmployeeId: {EmployeeId}", EmployeeId); } } catch (Exception ex) { - // Log errors from the background task so they are not lost. _logger.LogWarning("Background cache update failed for EmployeeId {EmployeeId} : {Error}", EmployeeId, ex.Message); } }); - // --- Step 3: Execute the main query to get permissions in a single database call --- - // This single, efficient query gets all the required data at once. + // --- Step 3: Execute the main query on the main thread using its original context --- + // This is now safe because the background task is using a different DbContext instance. var permissions = await ( from rpm in _context.RolePermissionMappings - join fp in _context.FeaturePermissions.Include(f => f.Feature) // Include related Feature data + join fp in _context.FeaturePermissions.Include(f => f.Feature) on rpm.FeaturePermissionId equals fp.Id - // The 'employeeRoleIdsQuery' subquery is seamlessly integrated here by EF Core, - // resulting in a SQL "IN (SELECT ...)" clause. where employeeRoleIdsQuery.Contains(rpm.ApplicationRoleId) && fp.IsEnabled == true select fp) - .Distinct() // Ensures each permission is returned only once + .Distinct() .ToListAsync(); _logger.LogInfo("Successfully retrieved {PermissionCount} unique permissions for EmployeeId: {EmployeeId}", permissions.Count, EmployeeId); - return permissions; } catch (Exception ex) { - _logger.LogError("An error occurred while fetching permissions for EmployeeId {EmployeeId} :{Error}", EmployeeId, ex.Message); - // Depending on your application's error handling strategy, you might re-throw, - // or return an empty list to prevent downstream failures. + _logger.LogError("An error occurred while fetching permissions for EmployeeId {EmployeeId} : {Error}", EmployeeId, ex.Message); return new List(); } }