From c89946a70e8d71156095e5fe929799e932aff48b Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 28 Mar 2025 15:07:25 +0800 Subject: [PATCH 1/2] feat: add includeChildren parameter to GetMembersCountAsync for hierarchical member counting --- .../Identity/IOrganizationUnitRepository.cs | 1 + .../EfCoreOrganizationUnitRepository.cs | 32 +++++++++++++++---- .../MongoOrganizationUnitRepository.cs | 24 ++++++++++++-- .../OrganizationUnitRepository_Tests.cs | 7 ++++ 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs index a9a92e9e43..b1c1df1676 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs @@ -110,6 +110,7 @@ public interface IOrganizationUnitRepository : IBasicRepository GetMembersCountAsync( OrganizationUnit organizationUnit, string filter = null, + bool includeChildren = false, CancellationToken cancellationToken = default ); diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs index 03649b3c73..1af35eafcc 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs @@ -231,9 +231,10 @@ public class EfCoreOrganizationUnitRepository public virtual async Task GetMembersCountAsync( OrganizationUnit organizationUnit, string filter = null, + bool includeChildren = false, CancellationToken cancellationToken = default) { - var query = await CreateGetMembersFilteredQueryAsync(organizationUnit, filter); + var query = await CreateGetMembersFilteredQueryAsync(organizationUnit, filter, includeChildren); return await query.CountAsync(GetCancellationToken(cancellationToken)); } @@ -324,14 +325,33 @@ public class EfCoreOrganizationUnitRepository dbContext.Set().RemoveRange(ouMembersQuery); } - protected virtual async Task> CreateGetMembersFilteredQueryAsync(OrganizationUnit organizationUnit, string filter = null) + protected virtual async Task> CreateGetMembersFilteredQueryAsync( + OrganizationUnit organizationUnit, + string filter = null, + bool includeChildren = false) { var dbContext = await GetDbContextAsync(); - var query = from userOu in dbContext.Set() - join user in dbContext.Users on userOu.UserId equals user.Id - where userOu.OrganizationUnitId == organizationUnit.Id - select user; + IQueryable query; + if (includeChildren) + { + var childrenIds = await (await GetDbSetAsync()) + .Where(ou => ou.Code.StartsWith(organizationUnit.Code)) + .Select(x => x.Id) + .ToListAsync(); + + query = from userOu in dbContext.Set() + join user in dbContext.Users on userOu.UserId equals user.Id + where childrenIds.Contains(userOu.OrganizationUnitId) + select user; + } + else + { + query = from userOu in dbContext.Set() + join user in dbContext.Users on userOu.UserId equals user.Id + where userOu.OrganizationUnitId == organizationUnit.Id + select user; + } if (!filter.IsNullOrWhiteSpace()) { diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs index 78838c9c6c..5515015438 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs @@ -193,7 +193,7 @@ public class MongoOrganizationUnitRepository CancellationToken cancellationToken = default) { cancellationToken = GetCancellationToken(cancellationToken); - var query = await CreateGetMembersFilteredQueryAsync(organizationUnit, filter, cancellationToken); + var query = await CreateGetMembersFilteredQueryAsync(organizationUnit, filter, false, cancellationToken); return await query .OrderBy(sorting.IsNullOrEmpty() ? nameof(IdentityUser.UserName) : sorting) .As>() @@ -212,10 +212,11 @@ public class MongoOrganizationUnitRepository public virtual async Task GetMembersCountAsync( OrganizationUnit organizationUnit, string filter = null, + bool includeChildren = false, CancellationToken cancellationToken = default) { cancellationToken = GetCancellationToken(cancellationToken); - var query = await CreateGetMembersFilteredQueryAsync(organizationUnit, filter, cancellationToken); + var query = await CreateGetMembersFilteredQueryAsync(organizationUnit, filter, includeChildren, cancellationToken); return await query.CountAsync(cancellationToken); } @@ -286,8 +287,27 @@ public class MongoOrganizationUnitRepository protected virtual async Task> CreateGetMembersFilteredQueryAsync( OrganizationUnit organizationUnit, string filter = null, + bool includeChildren = false, CancellationToken cancellationToken = default) { + if (includeChildren) + { + var childrenIds = await (await GetMongoQueryableAsync(cancellationToken)) + .Where(ou => ou.Code.StartsWith(organizationUnit.Code)) + .Select(ou => ou.Id) + .ToListAsync(GetCancellationToken(cancellationToken)); + + return (await GetMongoQueryableAsync(cancellationToken)) + .Where(u => u.OrganizationUnits.Any(uou => childrenIds.Contains(uou.OrganizationUnitId))) + .WhereIf>( + !filter.IsNullOrWhiteSpace(), + u => + u.UserName.Contains(filter) || + u.Email.Contains(filter) || + (u.PhoneNumber != null && u.PhoneNumber.Contains(filter)) + ); + } + return (await GetMongoQueryableAsync(cancellationToken)) .Where(u => u.OrganizationUnits.Any(uou => uou.OrganizationUnitId == organizationUnit.Id)) .WhereIf>( diff --git a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs index 8746c3a1fd..ce78147ace 100644 --- a/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs +++ b/modules/identity/test/Volo.Abp.Identity.TestBase/Volo/Abp/Identity/OrganizationUnitRepository_Tests.cs @@ -267,6 +267,10 @@ public abstract class OrganizationUnitRepository_Tests : AbpIden var usersCount = await _organizationUnitRepository.GetMembersCountAsync(ou); usersCount.ShouldBeGreaterThan(1); + + usersCount = await _organizationUnitRepository.GetMembersCountAsync(ou, includeChildren: true); + + usersCount.ShouldBeGreaterThanOrEqualTo(2); } [Fact] @@ -301,6 +305,9 @@ public abstract class OrganizationUnitRepository_Tests : AbpIden await _organizationUnitRepository.RemoveAllMembersAsync(ou); var newCount = await _organizationUnitRepository.GetMembersCountAsync(ou); newCount.ShouldBe(0); + + newCount = await _organizationUnitRepository.GetMembersCountAsync(ou, includeChildren: true); + newCount.ShouldBe(0); } [Fact] From f7a74906edda09743295ad56597c39738e6c1166 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 28 Mar 2025 16:18:33 +0800 Subject: [PATCH 2/2] feat: enhance organization management by including child organization units in user updates --- .../Identity/IOrganizationUnitRepository.cs | 2 ++ .../Volo/Abp/Identity/IdentityUserManager.cs | 4 ++-- .../EfCoreIdentityUserRepository.cs | 16 ++++++++++++-- .../EfCoreOrganizationUnitRepository.cs | 14 +++++------- .../MongoDB/MongoIdentityUserRepository.cs | 22 +++++++++++++++++-- .../MongoOrganizationUnitRepository.cs | 11 +++++----- 6 files changed, 50 insertions(+), 19 deletions(-) diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs index b1c1df1676..20f39bc55f 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IOrganizationUnitRepository.cs @@ -98,12 +98,14 @@ public interface IOrganizationUnitRepository : IBasicRepository> GetMemberIdsAsync( Guid id, + bool includeChildren = false, CancellationToken cancellationToken = default ); diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs index 4ca6d9fab3..f10113eb05 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityUserManager.cs @@ -395,14 +395,14 @@ public class IdentityUserManager : UserManager, IDomainService var sourceOrganization = await OrganizationUnitRepository.GetAsync(sourceOrganizationId, cancellationToken: CancellationToken); Logger.LogDebug($"Remove dynamic claims cache for users of organization: {sourceOrganizationId}"); - var userIdList = await OrganizationUnitRepository.GetMemberIdsAsync(sourceOrganizationId, cancellationToken: CancellationToken); + var userIdList = await OrganizationUnitRepository.GetMemberIdsAsync(sourceOrganizationId, includeChildren: true, cancellationToken: CancellationToken); await DynamicClaimCache.RemoveManyAsync(userIdList.Select(userId => AbpDynamicClaimCacheItem.CalculateCacheKey(userId, sourceOrganization.TenantId)), token: CancellationToken); var targetOrganization = targetOrganizationId.HasValue ? await OrganizationUnitRepository.GetAsync(targetOrganizationId.Value, cancellationToken: CancellationToken) : null; if (targetOrganization != null) { Logger.LogDebug($"Remove dynamic claims cache for users of organization: {targetOrganizationId}"); - userIdList = await OrganizationUnitRepository.GetMemberIdsAsync(targetOrganizationId.Value, cancellationToken: CancellationToken); + userIdList = await OrganizationUnitRepository.GetMemberIdsAsync(targetOrganizationId.Value, includeChildren: true, cancellationToken: CancellationToken); await DynamicClaimCache.RemoveManyAsync(userIdList.Select(userId => AbpDynamicClaimCacheItem.CalculateCacheKey(userId, targetOrganization.TenantId)), token: CancellationToken); } diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs index 86682b85db..2069a8f69f 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityUserRepository.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore; @@ -420,9 +421,20 @@ public class EfCoreIdentityUserRepository : EfCoreRepository().FirstOrDefaultAsync(x => x.Id == sourceOrganizationId, cancellationToken: cancellationToken); + if (sourceOrganization == null) + { + throw new EntityNotFoundException(typeof(OrganizationUnit), sourceOrganizationId); + } + + var allSourceOrganizationIds = await (await GetDbContextAsync()).Set() + .Where(x => x.Code.StartsWith(sourceOrganization.Code)) + .Select(x => x.Id).ToArrayAsync(cancellationToken: cancellationToken); + var users = await (await GetDbContextAsync()).Set().Where(x => x.OrganizationUnitId == targetOrganizationId).Select(x => x.UserId).ToArrayAsync(cancellationToken: cancellationToken); - await (await GetDbContextAsync()).Set().Where(x => x.OrganizationUnitId == sourceOrganizationId && !users.Contains(x.UserId)).ExecuteUpdateAsync(t => t.SetProperty(e => e.OrganizationUnitId, targetOrganizationId), GetCancellationToken(cancellationToken)); - await (await GetDbContextAsync()).Set().Where(x => x.OrganizationUnitId == sourceOrganizationId).ExecuteDeleteAsync(GetCancellationToken(cancellationToken)); + + await (await GetDbContextAsync()).Set().Where(x => allSourceOrganizationIds.Contains(x.OrganizationUnitId) && !users.Contains(x.UserId)).ExecuteUpdateAsync(t => t.SetProperty(e => e.OrganizationUnitId, targetOrganizationId), GetCancellationToken(cancellationToken)); + await (await GetDbContextAsync()).Set().Where(x => allSourceOrganizationIds.Contains(x.OrganizationUnitId)).ExecuteDeleteAsync(GetCancellationToken(cancellationToken)); } else { diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs index 1af35eafcc..c0d664fbd4 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs @@ -208,24 +208,22 @@ public class EfCoreOrganizationUnitRepository int maxResultCount = int.MaxValue, int skipCount = 0, string filter = null, + bool includeChildren = false, bool includeDetails = false, CancellationToken cancellationToken = default) { - var query = await CreateGetMembersFilteredQueryAsync(organizationUnit, filter); + var query = await CreateGetMembersFilteredQueryAsync(organizationUnit, filter, includeChildren); return await query.IncludeDetails(includeDetails).OrderBy(sorting.IsNullOrEmpty() ? nameof(IdentityUser.UserName) : sorting) .PageBy(skipCount, maxResultCount) .ToListAsync(GetCancellationToken(cancellationToken)); } - public virtual async Task> GetMemberIdsAsync(Guid id, CancellationToken cancellationToken = default) + public virtual async Task> GetMemberIdsAsync(Guid id, bool includeChildren = false, CancellationToken cancellationToken = default) { - var dbContext = await GetDbContextAsync(); - - return await (from userOu in dbContext.Set() - join user in dbContext.Users on userOu.UserId equals user.Id - where userOu.OrganizationUnitId == id - select user.Id).ToListAsync(cancellationToken); + var organizationUnit = await GetAsync(id, cancellationToken: cancellationToken); + var query = await CreateGetMembersFilteredQueryAsync(organizationUnit, null, includeChildren); + return await query.Select(x => x.Id).ToListAsync(cancellationToken); } public virtual async Task GetMembersCountAsync( diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs index 58486893ec..7cc594c126 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityUserRepository.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using JetBrains.Annotations; using MongoDB.Driver; using MongoDB.Driver.Linq; +using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories.MongoDB; using Volo.Abp.MongoDB; @@ -358,13 +359,30 @@ public class MongoIdentityUserRepository : MongoDbRepository(cancellationToken)) + .Where(x => x.Id == sourceOrganizationId) + .FirstOrDefaultAsync(cancellationToken: cancellationToken); + if (sourceOrganizationUnit == null) + { + throw new EntityNotFoundException(typeof(OrganizationUnit), sourceOrganizationId); + } + + var allSourceOrganizationIds = await (await GetMongoQueryableAsync(cancellationToken)) + .Where(x => x.Code.StartsWith(sourceOrganizationUnit.Code)) + .Select(x => x.Id) + .ToListAsync(cancellationToken: cancellationToken); + var users = await (await GetMongoQueryableAsync(cancellationToken)) - .Where(x => x.OrganizationUnits.Any(r => r.OrganizationUnitId == sourceOrganizationId)) + .Where(x => x.OrganizationUnits.Any(r => allSourceOrganizationIds.Contains(r.OrganizationUnitId))) .ToListAsync(GetCancellationToken(cancellationToken)); foreach (var user in users) { - user.RemoveOrganizationUnit(sourceOrganizationId); + foreach (var organizationId in allSourceOrganizationIds) + { + user.RemoveOrganizationUnit(organizationId); + } + if (targetOrganizationId.HasValue) { user.AddOrganizationUnit(targetOrganizationId.Value); diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs index 5515015438..1b76f81713 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoOrganizationUnitRepository.cs @@ -189,11 +189,12 @@ public class MongoOrganizationUnitRepository int maxResultCount = int.MaxValue, int skipCount = 0, string filter = null, + bool includeChildren = false, bool includeDetails = false, CancellationToken cancellationToken = default) { cancellationToken = GetCancellationToken(cancellationToken); - var query = await CreateGetMembersFilteredQueryAsync(organizationUnit, filter, false, cancellationToken); + var query = await CreateGetMembersFilteredQueryAsync(organizationUnit, filter, includeChildren, cancellationToken); return await query .OrderBy(sorting.IsNullOrEmpty() ? nameof(IdentityUser.UserName) : sorting) .As>() @@ -201,12 +202,12 @@ public class MongoOrganizationUnitRepository .ToListAsync(cancellationToken); } - public virtual async Task> GetMemberIdsAsync(Guid id, CancellationToken cancellationToken = default) + public virtual async Task> GetMemberIdsAsync(Guid id, bool includeChildren = false, CancellationToken cancellationToken = default) { cancellationToken = GetCancellationToken(cancellationToken); - return await (await GetMongoQueryableAsync(cancellationToken)) - .Where(u => u.OrganizationUnits.Any(uou => uou.OrganizationUnitId == id)).Select(x => x.Id) - .ToListAsync(cancellationToken); + var organizationUnit = await GetAsync(id, cancellationToken: cancellationToken); + var query = await CreateGetMembersFilteredQueryAsync(organizationUnit, null, includeChildren, cancellationToken); + return await query.Select(x => x.Id).ToListAsync(cancellationToken); } public virtual async Task GetMembersCountAsync(