Solved Concurrency Issue
This commit is contained in:
parent
4ba533f647
commit
0c84bb11a3
@ -20,29 +20,12 @@ namespace Marco.Pms.CacheHelper
|
||||
var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name
|
||||
_collection = mongoDB.GetCollection<EmployeePermissionMongoDB>("EmployeeProfile");
|
||||
}
|
||||
public async Task<bool> AddApplicationRoleToCache(Guid employeeId, List<Guid> roleIds)
|
||||
public async Task<bool> AddApplicationRoleToCache(Guid employeeId, List<string> newRoleIds, List<string> 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<List<string>> 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<EmployeePermissionMongoDB>.Filter.Eq(e => e.Id, employeeIdString);
|
||||
|
||||
|
@ -641,9 +641,30 @@ namespace Marco.Pms.Services.Helpers
|
||||
// ------------------------------------ Employee Profile Cache ---------------------------------------
|
||||
public async Task AddApplicationRole(Guid employeeId, List<Guid> 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<List<string>> 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)
|
||||
{
|
||||
|
@ -10,14 +10,16 @@ namespace MarcoBMS.Services.Helpers
|
||||
{
|
||||
public class RolesHelper
|
||||
{
|
||||
private readonly IDbContextFactory<ApplicationDbContext> _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<ApplicationDbContext> dbContextFactory)
|
||||
{
|
||||
_context = context;
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
_dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -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<FeaturePermission>();
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user