mirror of
https://github.com/bitwarden/server
synced 2025-12-10 13:23:27 +00:00
[PM-21031] Optimize GET Members endpoint performance (#5907)
* Add new feature flag for Members Get Endpoint Optimization * Add a new version of OrganizationUser_ReadByOrganizationIdWithClaimedDomains that uses CTE for better performance * Add stored procedure OrganizationUserUserDetails_ReadByOrganizationId_V2 for retrieving user details, group associations, and collection associations by organization ID. * Add the sql migration script to add the new stored procedures * Introduce GetManyDetailsByOrganizationAsync_vNext and GetManyByOrganizationWithClaimedDomainsAsync_vNext in IOrganizationUserRepository to enhance performance by reducing database round trips. * Updated GetOrganizationUsersClaimedStatusQuery to use an optimized query when the feature flag is enabled * Updated OrganizationUserUserDetailsQuery to use optimized queries when the feature flag is enabled * Add integration tests for GetManyDetailsByOrganizationAsync_vNext * Add integration tests for GetManyByOrganizationWithClaimedDomainsAsync_vNext to validate behavior with verified and unverified domains. * Optimize performance by conditionally setting permissions only for Custom user types in OrganizationUserUserDetailsQuery. * Create UserEmailDomainView to extract email domains from users' email addresses * Create stored procedure Organization_ReadByClaimedUserEmailDomain_V2 that uses UserEmailDomainView to fetch Email domains * Add GetByVerifiedUserEmailDomainAsync_vNext method to IOrganizationRepository and its implementations * Refactor OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2 stored procedure to use UserEmailDomainView for email domain extraction, improving query efficiency and clarity. * Enhance IOrganizationUserRepository with detailed documentation for GetManyDetailsByOrganizationAsync method, clarifying its purpose and performance optimizations. Added remarks for better understanding of its functionality. * Fix missing newline at the end of Organization_ReadByClaimedUserEmailDomain_V2.sql to adhere to coding standards. * Update the database migration script to include UserEmailDomainView * Bumped the date on the migration script * Remove GetByVerifiedUserEmailDomainAsync_vNext method and its stored procedure. * Refactor UserEmailDomainView index creation to check for existence before creation * Update OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2 to use CTE and add indexes * Remove creation of unique clustered index from UserEmailDomainView and related migration script adjustments * Update indexes and sproc * Fix index name when checking if it already exists * Bump up date on migration script
This commit is contained in:
@@ -268,6 +268,68 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<OrganizationUserUserDetails>> GetManyDetailsByOrganizationAsync_vNext(Guid organizationId, bool includeGroups, bool includeCollections)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
// Use a single call that returns multiple result sets
|
||||
var results = await connection.QueryMultipleAsync(
|
||||
"[dbo].[OrganizationUserUserDetails_ReadByOrganizationId_V2]",
|
||||
new
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
IncludeGroups = includeGroups,
|
||||
IncludeCollections = includeCollections
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
// Read the user details (first result set)
|
||||
var users = (await results.ReadAsync<OrganizationUserUserDetails>()).ToList();
|
||||
|
||||
// Read group associations (second result set, if requested)
|
||||
Dictionary<Guid, List<Guid>>? userGroupMap = null;
|
||||
if (includeGroups)
|
||||
{
|
||||
var groupUsers = await results.ReadAsync<GroupUser>();
|
||||
userGroupMap = groupUsers
|
||||
.GroupBy(gu => gu.OrganizationUserId)
|
||||
.ToDictionary(g => g.Key, g => g.Select(gu => gu.GroupId).ToList());
|
||||
}
|
||||
|
||||
// Read collection associations (third result set, if requested)
|
||||
Dictionary<Guid, List<CollectionAccessSelection>>? userCollectionMap = null;
|
||||
if (includeCollections)
|
||||
{
|
||||
var collectionUsers = await results.ReadAsync<CollectionUser>();
|
||||
userCollectionMap = collectionUsers
|
||||
.GroupBy(cu => cu.OrganizationUserId)
|
||||
.ToDictionary(g => g.Key, g => g.Select(cu => new CollectionAccessSelection
|
||||
{
|
||||
Id = cu.CollectionId,
|
||||
ReadOnly = cu.ReadOnly,
|
||||
HidePasswords = cu.HidePasswords,
|
||||
Manage = cu.Manage
|
||||
}).ToList());
|
||||
}
|
||||
|
||||
// Map the associations to users
|
||||
foreach (var user in users)
|
||||
{
|
||||
if (userGroupMap != null)
|
||||
{
|
||||
user.Groups = userGroupMap.GetValueOrDefault(user.Id, new List<Guid>());
|
||||
}
|
||||
|
||||
if (userCollectionMap != null)
|
||||
{
|
||||
user.Collections = userCollectionMap.GetValueOrDefault(user.Id, new List<CollectionAccessSelection>());
|
||||
}
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<OrganizationUserOrganizationDetails>> GetManyDetailsByUserAsync(Guid userId,
|
||||
OrganizationUserStatusType? status = null)
|
||||
{
|
||||
@@ -558,6 +620,19 @@ public class OrganizationUserRepository : Repository<OrganizationUser, Guid>, IO
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ICollection<OrganizationUser>> GetManyByOrganizationWithClaimedDomainsAsync_vNext(Guid organizationId)
|
||||
{
|
||||
using (var connection = new SqlConnection(ConnectionString))
|
||||
{
|
||||
var results = await connection.QueryAsync<OrganizationUser>(
|
||||
$"[{Schema}].[OrganizationUser_ReadByOrganizationIdWithClaimedDomains_V2]",
|
||||
new { OrganizationId = organizationId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
|
||||
return results.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RevokeManyByIdAsync(IEnumerable<Guid> organizationUserIds)
|
||||
{
|
||||
await using var connection = new SqlConnection(ConnectionString);
|
||||
|
||||
Reference in New Issue
Block a user