You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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.

解决VSTS GraphHttpClient内置账户报错及遍历用户组的最佳实践

一、内置账户报错的原因与解决办法

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

  1. Paged Retrieval: Using top and skip parameters prevents overwhelming the API with large requests, which can cause timeouts or throttling.
  2. Type Filtering: Skipping non-User entities directly avoids most built-in account errors.
  3. Error Fallback: Wrapping individual user queries in a try-catch ensures one problematic user doesn't stop the entire traversal.
  4. Membership Direction: GraphTraversalDirection.MemberOf pulls groups the user is part of. Use GraphTraversalDirection.Members if 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 12:02:13