如何用Microsoft Graph Client枚举用户所属组及VSTS场景问题咨询
Hey there! Let's tackle your VSTS GraphHttpClient issues step by step, covering both the built-in account error fix and the best way to generate a user-group list.
一、内置账户报错的原因与解决办法
The errors you're seeing almost certainly come from VSTS built-in accounts (like Project Collection Valid Users, Build Service accounts, etc.) not being regular user entities. These are either groups (Group type) or service principals (ServicePrincipal type), and the GraphHttpClient's user-focused APIs aren't designed to handle their membership queries—hence the failures.
How to filter out built-in accounts
You can distinguish regular users from these special entities using the PrincipalType property:
- Regular users have
PrincipalType = GraphPrincipalType.User - Built-in groups have
PrincipalType = GraphPrincipalType.Group - Service principals (automation accounts) have
PrincipalType = GraphPrincipalType.ServicePrincipal
Filtering out non-User entities before processing will eliminate most of these errors.
二、Best Practice: Traverse All Users & Generate Group Lists
The core of a solid implementation is paged user retrieval + targeted membership queries + error fallback—this avoids performance issues with large user bases and handles edge cases gracefully.
Full Code Example
using Microsoft.VisualStudio.Services.Common; using Microsoft.VisualStudio.Services.Graph.Client; using Microsoft.VisualStudio.Services.WebApi; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; public async Task GenerateUserGroupList(Uri vstsUri, VssCredentials credentials) { // Initialize connection and Graph client using var connection = new VssConnection(vstsUri, credentials); var graphClient = connection.GetClient<GraphHttpClient>(); int pageSize = 100; // Adjust based on your instance's size int skipCount = 0; bool hasMoreUsers = true; while (hasMoreUsers) { try { // Fetch users in pages to avoid timeouts/performance hits var users = await graphClient.GetUsersAsync(top: pageSize, skip: skipCount); hasMoreUsers = users.Count == pageSize; skipCount += pageSize; foreach (var user in users) { // Skip non-user entities to avoid built-in account errors if (user.PrincipalType != GraphPrincipalType.User) { Console.WriteLine($"Skipping non-user entity: {user.DisplayName} (Type: {user.PrincipalType})"); continue; } try { // Get all direct groups the user belongs to var memberships = await graphClient.GetMembershipsAsync(user.Descriptor, GraphTraversalDirection.MemberOf); var groupNames = memberships.Select(m => m.ContainerDisplayName).ToList(); Console.WriteLine($"User: {user.DisplayName} ({user.UniqueName})"); Console.WriteLine("Groups:"); foreach (var group in groupNames) { Console.WriteLine($"- {group}"); } Console.WriteLine("---------------------"); } catch (Exception ex) { // Handle individual user failures without breaking the whole loop Console.WriteLine($"Failed to fetch groups for {user.DisplayName}: {ex.Message}"); continue; } } } catch (Exception ex) { Console.WriteLine($"Failed to fetch user page: {ex.Message}"); break; } } }
Key Details
- Paged Retrieval: Using
topandskipparameters prevents overwhelming the API with large requests, which can cause timeouts or throttling. - Type Filtering: Skipping non-
Userentities directly avoids most built-in account errors. - Error Fallback: Wrapping individual user queries in a try-catch ensures one problematic user doesn't stop the entire traversal.
- Membership Direction:
GraphTraversalDirection.MemberOfpulls groups the user is part of. UseGraphTraversalDirection.Membersif you need to get users under this user (e.g., team members).
三、Want Indirect Group Membership?
If you need to include indirect groups (e.g., user is in Group A, which is in Group B—so user is indirectly in Group B), add a recursive traversal:
private async Task<List<string>> GetAllDirectAndIndirectGroups(GraphHttpClient graphClient, GraphUser user) { var allGroups = new List<string>(); var processedDescriptors = new HashSet<string>(); // Prevent circular references async Task TraverseGroupHierarchy(GraphDescriptor descriptor) { if (processedDescriptors.Contains(descriptor.ToString())) return; processedDescriptors.Add(descriptor.ToString()); var memberships = await graphClient.GetMembershipsAsync(descriptor, GraphTraversalDirection.MemberOf); foreach (var membership in memberships) { if (membership.ContainerDescriptor.PrincipalType == GraphPrincipalType.Group) { allGroups.Add(membership.ContainerDisplayName); // Recursively check the parent group's memberships await TraverseGroupHierarchy(membership.ContainerDescriptor); } } } await TraverseGroupHierarchy(user.Descriptor); return allGroups.Distinct().ToList(); }
Replace the group-fetching logic in the main code with this method to get full direct + indirect group memberships.
内容的提问来源于stack exchange,提问作者Paul Duer

