From fc0ec5238b45f7e6fd346535fc7abee628e6be4e Mon Sep 17 00:00:00 2001 From: jarvis Date: Mon, 26 Jan 2026 20:20:35 +0000 Subject: [PATCH] feat: add CancellationToken to async methods (#1175) - Add CancellationToken parameter to IdentityDbInitializer private methods and propagate from SeedAsync to all inner async calls - Add CancellationToken to IRoleService interface methods - Update RoleService to accept and propagate CancellationToken - Update all command/query handlers to pass CancellationToken to service This ensures proper cancellation support throughout the identity module for graceful shutdown and request cancellation scenarios. --- .../Services/IRoleService.cs | 17 ++++--- .../Data/IdentityDbInitializer.cs | 46 +++++++++---------- .../DeleteRole/DeleteRoleCommandHandler.cs | 2 +- .../GetRoleById/GetRoleByIdQueryHandler.cs | 2 +- .../v1/Roles/GetRoles/GetRolesQueryHandler.cs | 2 +- .../Features/v1/Roles/RoleService.cs | 29 ++++++------ .../UpdatePermissionsCommandHandler.cs | 2 +- .../UpsertRole/UpsertRoleCommandHandler.cs | 2 +- 8 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/Modules/Identity/Modules.Identity.Contracts/Services/IRoleService.cs b/src/Modules/Identity/Modules.Identity.Contracts/Services/IRoleService.cs index 83412a3040..0b738fc454 100644 --- a/src/Modules/Identity/Modules.Identity.Contracts/Services/IRoleService.cs +++ b/src/Modules/Identity/Modules.Identity.Contracts/Services/IRoleService.cs @@ -1,14 +1,13 @@ -using FSH.Modules.Identity.Contracts.DTOs; +using FSH.Modules.Identity.Contracts.DTOs; namespace FSH.Modules.Identity.Contracts.Services; public interface IRoleService { - Task> GetRolesAsync(); - Task GetRoleAsync(string id); - Task CreateOrUpdateRoleAsync(string roleId, string name, string description); - Task DeleteRoleAsync(string id); - Task GetWithPermissionsAsync(string id, CancellationToken cancellationToken); - - Task UpdatePermissionsAsync(string roleId, List permissions); -} \ No newline at end of file + Task> GetRolesAsync(CancellationToken cancellationToken = default); + Task GetRoleAsync(string id, CancellationToken cancellationToken = default); + Task CreateOrUpdateRoleAsync(string roleId, string name, string description, CancellationToken cancellationToken = default); + Task DeleteRoleAsync(string id, CancellationToken cancellationToken = default); + Task GetWithPermissionsAsync(string id, CancellationToken cancellationToken = default); + Task UpdatePermissionsAsync(string roleId, List permissions, CancellationToken cancellationToken = default); +} diff --git a/src/Modules/Identity/Modules.Identity/Data/IdentityDbInitializer.cs b/src/Modules/Identity/Modules.Identity/Data/IdentityDbInitializer.cs index f2c05bf99b..1bd4720f90 100644 --- a/src/Modules/Identity/Modules.Identity/Data/IdentityDbInitializer.cs +++ b/src/Modules/Identity/Modules.Identity/Data/IdentityDbInitializer.cs @@ -1,4 +1,4 @@ -using Finbuckle.MultiTenant.Abstractions; +using Finbuckle.MultiTenant.Abstractions; using FSH.Framework.Persistence; using FSH.Framework.Shared.Constants; using FSH.Framework.Shared.Multitenancy; @@ -31,16 +31,16 @@ public async Task MigrateAsync(CancellationToken cancellationToken) public async Task SeedAsync(CancellationToken cancellationToken) { - await SeedRolesAsync(); - await SeedSystemGroupsAsync(); - await SeedAdminUserAsync(); + await SeedRolesAsync(cancellationToken); + await SeedSystemGroupsAsync(cancellationToken); + await SeedAdminUserAsync(cancellationToken); } - private async Task SeedRolesAsync() + private async Task SeedRolesAsync(CancellationToken cancellationToken = default) { foreach (string roleName in RoleConstants.DefaultRoles) { - if (await roleManager.Roles.SingleOrDefaultAsync(r => r.Name == roleName) + if (await roleManager.Roles.SingleOrDefaultAsync(r => r.Name == roleName, cancellationToken) is not FshRole role) { // create role @@ -51,21 +51,21 @@ private async Task SeedRolesAsync() // Assign permissions if (roleName == RoleConstants.Basic) { - await AssignPermissionsToRoleAsync(context, PermissionConstants.Basic, role); + await AssignPermissionsToRoleAsync(context, PermissionConstants.Basic, role, cancellationToken); } else if (roleName == RoleConstants.Admin) { - await AssignPermissionsToRoleAsync(context, PermissionConstants.Admin, role); + await AssignPermissionsToRoleAsync(context, PermissionConstants.Admin, role, cancellationToken); if (multiTenantContextAccessor.MultiTenantContext.TenantInfo?.Id == MultitenancyConstants.Root.Id) { - await AssignPermissionsToRoleAsync(context, PermissionConstants.Root, role); + await AssignPermissionsToRoleAsync(context, PermissionConstants.Root, role, cancellationToken); } } } } - private async Task AssignPermissionsToRoleAsync(IdentityDbContext dbContext, IReadOnlyList permissions, FshRole role) + private async Task AssignPermissionsToRoleAsync(IdentityDbContext dbContext, IReadOnlyList permissions, FshRole role, CancellationToken cancellationToken = default) { var currentClaims = await roleManager.GetClaimsAsync(role); var newClaims = permissions @@ -83,18 +83,18 @@ private async Task AssignPermissionsToRoleAsync(IdentityDbContext dbContext, IRe foreach (var claim in newClaims) { logger.LogInformation("Seeding {Role} Permission '{Permission}' for '{TenantId}' Tenant.", role.Name, claim.ClaimValue, multiTenantContextAccessor.MultiTenantContext.TenantInfo?.Id); - await dbContext.RoleClaims.AddAsync(claim); + await dbContext.RoleClaims.AddAsync(claim, cancellationToken); } // Save changes to the database context if (newClaims.Count != 0) { - await dbContext.SaveChangesAsync(); + await dbContext.SaveChangesAsync(cancellationToken); } } - private async Task SeedSystemGroupsAsync() + private async Task SeedSystemGroupsAsync(CancellationToken cancellationToken = default) { var tenantId = multiTenantContextAccessor.MultiTenantContext.TenantInfo?.Id; if (string.IsNullOrWhiteSpace(tenantId)) @@ -105,7 +105,7 @@ private async Task SeedSystemGroupsAsync() // Seed "All Users" default group - all new users are automatically added to this group const string allUsersGroupName = "All Users"; var allUsersGroup = await context.Groups - .FirstOrDefaultAsync(g => g.Name == allUsersGroupName && g.IsSystemGroup); + .FirstOrDefaultAsync(g => g.Name == allUsersGroupName && g.IsSystemGroup, cancellationToken); if (allUsersGroup is null) { @@ -116,14 +116,14 @@ private async Task SeedSystemGroupsAsync() isSystemGroup: true, createdBy: "System"); - await context.Groups.AddAsync(allUsersGroup); + await context.Groups.AddAsync(allUsersGroup, cancellationToken); logger.LogInformation("Seeding '{GroupName}' system group for '{TenantId}' Tenant.", allUsersGroupName, tenantId); } // Seed "Administrators" group with Admin role const string administratorsGroupName = "Administrators"; var administratorsGroup = await context.Groups - .FirstOrDefaultAsync(g => g.Name == administratorsGroupName && g.IsSystemGroup); + .FirstOrDefaultAsync(g => g.Name == administratorsGroupName && g.IsSystemGroup, cancellationToken); if (administratorsGroup is null) { @@ -134,37 +134,37 @@ private async Task SeedSystemGroupsAsync() isSystemGroup: true, createdBy: "System"); - await context.Groups.AddAsync(administratorsGroup); + await context.Groups.AddAsync(administratorsGroup, cancellationToken); logger.LogInformation("Seeding '{GroupName}' system group for '{TenantId}' Tenant.", administratorsGroupName, tenantId); } - await context.SaveChangesAsync(); + await context.SaveChangesAsync(cancellationToken); // Assign Admin role to Administrators group var adminRole = await roleManager.FindByNameAsync(RoleConstants.Admin); if (adminRole is not null) { var existingGroupRole = await context.GroupRoles - .FirstOrDefaultAsync(gr => gr.GroupId == administratorsGroup.Id && gr.RoleId == adminRole.Id); + .FirstOrDefaultAsync(gr => gr.GroupId == administratorsGroup.Id && gr.RoleId == adminRole.Id, cancellationToken); if (existingGroupRole is null) { context.GroupRoles.Add(GroupRole.Create(administratorsGroup.Id, adminRole.Id)); - await context.SaveChangesAsync(); + await context.SaveChangesAsync(cancellationToken); logger.LogInformation("Assigned Admin role to '{GroupName}' group for '{TenantId}' Tenant.", administratorsGroupName, tenantId); } } } - private async Task SeedAdminUserAsync() + private async Task SeedAdminUserAsync(CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(multiTenantContextAccessor.MultiTenantContext.TenantInfo?.Id) || string.IsNullOrWhiteSpace(multiTenantContextAccessor.MultiTenantContext.TenantInfo?.AdminEmail)) { return; } - if (await userManager.Users.FirstOrDefaultAsync(u => u.Email == multiTenantContextAccessor.MultiTenantContext.TenantInfo!.AdminEmail) + if (await userManager.Users.FirstOrDefaultAsync(u => u.Email == multiTenantContextAccessor.MultiTenantContext.TenantInfo!.AdminEmail, cancellationToken) is not FshUser adminUser) { string adminUserName = $"{multiTenantContextAccessor.MultiTenantContext.TenantInfo?.Id.Trim()}.{RoleConstants.Admin}".ToUpperInvariant(); @@ -195,4 +195,4 @@ private async Task SeedAdminUserAsync() await userManager.AddToRoleAsync(adminUser, RoleConstants.Admin); } } -} \ No newline at end of file +} diff --git a/src/Modules/Identity/Modules.Identity/Features/v1/Roles/DeleteRole/DeleteRoleCommandHandler.cs b/src/Modules/Identity/Modules.Identity/Features/v1/Roles/DeleteRole/DeleteRoleCommandHandler.cs index 901ee7e7b1..8b06c23af0 100644 --- a/src/Modules/Identity/Modules.Identity/Features/v1/Roles/DeleteRole/DeleteRoleCommandHandler.cs +++ b/src/Modules/Identity/Modules.Identity/Features/v1/Roles/DeleteRole/DeleteRoleCommandHandler.cs @@ -17,7 +17,7 @@ public async ValueTask Handle(DeleteRoleCommand command, CancellationToken { ArgumentNullException.ThrowIfNull(command); - await _roleService.DeleteRoleAsync(command.Id).ConfigureAwait(false); + await _roleService.DeleteRoleAsync(command.Id, cancellationToken).ConfigureAwait(false); return Unit.Value; } diff --git a/src/Modules/Identity/Modules.Identity/Features/v1/Roles/GetRoleById/GetRoleByIdQueryHandler.cs b/src/Modules/Identity/Modules.Identity/Features/v1/Roles/GetRoleById/GetRoleByIdQueryHandler.cs index 85b21daf4b..26b58c465c 100644 --- a/src/Modules/Identity/Modules.Identity/Features/v1/Roles/GetRoleById/GetRoleByIdQueryHandler.cs +++ b/src/Modules/Identity/Modules.Identity/Features/v1/Roles/GetRoleById/GetRoleByIdQueryHandler.cs @@ -17,6 +17,6 @@ public GetRoleByIdQueryHandler(IRoleService roleService) public async ValueTask Handle(GetRoleQuery query, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(query); - return await _roleService.GetRoleAsync(query.Id).ConfigureAwait(false); + return await _roleService.GetRoleAsync(query.Id, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Modules/Identity/Modules.Identity/Features/v1/Roles/GetRoles/GetRolesQueryHandler.cs b/src/Modules/Identity/Modules.Identity/Features/v1/Roles/GetRoles/GetRolesQueryHandler.cs index b54e4aa178..5ed116da63 100644 --- a/src/Modules/Identity/Modules.Identity/Features/v1/Roles/GetRoles/GetRolesQueryHandler.cs +++ b/src/Modules/Identity/Modules.Identity/Features/v1/Roles/GetRoles/GetRolesQueryHandler.cs @@ -16,6 +16,6 @@ public GetRolesQueryHandler(IRoleService roleService) public async ValueTask> Handle(GetRolesQuery query, CancellationToken cancellationToken) { - return await _roleService.GetRolesAsync().ConfigureAwait(false); + return await _roleService.GetRolesAsync(cancellationToken).ConfigureAwait(false); } } diff --git a/src/Modules/Identity/Modules.Identity/Features/v1/Roles/RoleService.cs b/src/Modules/Identity/Modules.Identity/Features/v1/Roles/RoleService.cs index 299042e7fd..013935cc00 100644 --- a/src/Modules/Identity/Modules.Identity/Features/v1/Roles/RoleService.cs +++ b/src/Modules/Identity/Modules.Identity/Features/v1/Roles/RoleService.cs @@ -1,4 +1,4 @@ -using Finbuckle.MultiTenant.Abstractions; +using Finbuckle.MultiTenant.Abstractions; using FSH.Framework.Core.Context; using FSH.Framework.Core.Exceptions; using FSH.Framework.Shared.Constants; @@ -17,7 +17,7 @@ public class RoleService(RoleManager roleManager, IMultiTenantContextAccessor multiTenantContextAccessor, ICurrentUser currentUser) : IRoleService { - public async Task> GetRolesAsync() + public async Task> GetRolesAsync(CancellationToken cancellationToken = default) { if (roleManager is null) throw new NotFoundException("RoleManager not resolved. Check Identity registration."); @@ -28,12 +28,12 @@ public async Task> GetRolesAsync() var roles = await roleManager.Roles .Select(role => new RoleDto { Id = role.Id, Name = role.Name!, Description = role.Description }) - .ToListAsync(); + .ToListAsync(cancellationToken); return roles; } - public async Task GetRoleAsync(string id) + public async Task GetRoleAsync(string id, CancellationToken cancellationToken = default) { FshRole? role = await roleManager.FindByIdAsync(id); @@ -42,7 +42,7 @@ public async Task> GetRolesAsync() return new RoleDto { Id = role.Id, Name = role.Name!, Description = role.Description }; } - public async Task CreateOrUpdateRoleAsync(string roleId, string name, string description) + public async Task CreateOrUpdateRoleAsync(string roleId, string name, string description, CancellationToken cancellationToken = default) { FshRole? role = await roleManager.FindByIdAsync(roleId); @@ -61,7 +61,7 @@ public async Task CreateOrUpdateRoleAsync(string roleId, string name, s return new RoleDto { Id = role.Id, Name = role.Name!, Description = role.Description }; } - public async Task DeleteRoleAsync(string id) + public async Task DeleteRoleAsync(string id, CancellationToken cancellationToken = default) { FshRole? role = await roleManager.FindByIdAsync(id); @@ -70,9 +70,9 @@ public async Task DeleteRoleAsync(string id) await roleManager.DeleteAsync(role); } - public async Task GetWithPermissionsAsync(string id, CancellationToken cancellationToken) + public async Task GetWithPermissionsAsync(string id, CancellationToken cancellationToken = default) { - var role = await GetRoleAsync(id); + var role = await GetRoleAsync(id, cancellationToken); _ = role ?? throw new NotFoundException("role not found"); role.Permissions = await context.RoleClaims @@ -83,7 +83,7 @@ public async Task GetWithPermissionsAsync(string id, CancellationToken return role; } - public async Task UpdatePermissionsAsync(string roleId, List permissions) + public async Task UpdatePermissionsAsync(string roleId, List permissions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(permissions); @@ -94,8 +94,8 @@ public async Task UpdatePermissionsAsync(string roleId, List per FilterRootPermissions(permissions); var currentClaims = await roleManager.GetClaimsAsync(role); - await RemoveRevokedPermissionsAsync(role, currentClaims, permissions); - await AddNewPermissionsAsync(role, currentClaims, permissions); + await RemoveRevokedPermissionsAsync(role, currentClaims, permissions, cancellationToken); + await AddNewPermissionsAsync(role, currentClaims, permissions, cancellationToken); return "permissions updated"; } @@ -116,12 +116,13 @@ private void FilterRootPermissions(List permissions) } } - private async Task RemoveRevokedPermissionsAsync(FshRole role, IList currentClaims, List permissions) + private async Task RemoveRevokedPermissionsAsync(FshRole role, IList currentClaims, List permissions, CancellationToken cancellationToken = default) { var claimsToRemove = currentClaims.Where(c => !permissions.Exists(p => p == c.Value)); foreach (var claim in claimsToRemove) { + cancellationToken.ThrowIfCancellationRequested(); var result = await roleManager.RemoveClaimAsync(role, claim); if (!result.Succeeded) { @@ -131,7 +132,7 @@ private async Task RemoveRevokedPermissionsAsync(FshRole role, IList currentClaims, List permissions) + private async Task AddNewPermissionsAsync(FshRole role, IList currentClaims, List permissions, CancellationToken cancellationToken = default) { var newPermissions = permissions .Where(p => !string.IsNullOrEmpty(p) && !currentClaims.Any(c => c.Value == p)) @@ -150,7 +151,7 @@ private async Task AddNewPermissionsAsync(FshRole role, IList 0) { - await context.SaveChangesAsync(); + await context.SaveChangesAsync(cancellationToken); } } } diff --git a/src/Modules/Identity/Modules.Identity/Features/v1/Roles/UpdateRolePermissions/UpdatePermissionsCommandHandler.cs b/src/Modules/Identity/Modules.Identity/Features/v1/Roles/UpdateRolePermissions/UpdatePermissionsCommandHandler.cs index 4104057f88..53c0efda85 100644 --- a/src/Modules/Identity/Modules.Identity/Features/v1/Roles/UpdateRolePermissions/UpdatePermissionsCommandHandler.cs +++ b/src/Modules/Identity/Modules.Identity/Features/v1/Roles/UpdateRolePermissions/UpdatePermissionsCommandHandler.cs @@ -16,6 +16,6 @@ public UpdatePermissionsCommandHandler(IRoleService roleService) public async ValueTask Handle(UpdatePermissionsCommand command, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(command); - return await _roleService.UpdatePermissionsAsync(command.RoleId, command.Permissions).ConfigureAwait(false); + return await _roleService.UpdatePermissionsAsync(command.RoleId, command.Permissions, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Modules/Identity/Modules.Identity/Features/v1/Roles/UpsertRole/UpsertRoleCommandHandler.cs b/src/Modules/Identity/Modules.Identity/Features/v1/Roles/UpsertRole/UpsertRoleCommandHandler.cs index 39d07e02ae..4312388512 100644 --- a/src/Modules/Identity/Modules.Identity/Features/v1/Roles/UpsertRole/UpsertRoleCommandHandler.cs +++ b/src/Modules/Identity/Modules.Identity/Features/v1/Roles/UpsertRole/UpsertRoleCommandHandler.cs @@ -18,7 +18,7 @@ public async ValueTask Handle(UpsertRoleCommand command, CancellationTo { ArgumentNullException.ThrowIfNull(command); - return await _roleService.CreateOrUpdateRoleAsync(command.Id, command.Name, command.Description ?? string.Empty) + return await _roleService.CreateOrUpdateRoleAsync(command.Id, command.Name, command.Description ?? string.Empty, cancellationToken) .ConfigureAwait(false); } }