다음을 통해 공유


프로그래밍 방식으로 쿼리를 사용하여 작업 항목 가져오기

Azure DevOps Services

쿼리를 사용하여 작업 항목을 가져오는 것은 Azure DevOps Services의 일반적인 시나리오입니다. 이 문서에서는 REST API 또는 .NET 클라이언트 라이브러리를 사용하여 프로그래밍 방식으로 이 시나리오를 구현하는 방법을 설명합니다.

필수 조건

카테고리 요구 사항
Azure DevOps - 조직
- 작업 항목이 있는 프로젝트에 대한 액세스
인증 다음 방법 중 하나를 선택합니다.
- Microsoft Entra ID 인증 (대화형 앱에 권장)
- 서비스 주체 인증 (자동화에 권장)
- 관리 ID 인증 (Azure 호스팅 앱에 권장)
- 개인용 액세스 토큰 (테스트용)
개발 환경 C# 개발 환경. Visual Studio를 사용할 수 있습니다.

중요합니다

프로덕션 애플리케이션의 경우 PAT(개인 액세스 토큰) 대신 Microsoft Entra ID 인증 을 사용하는 것이 좋습니다. PAT는 테스트 및 개발 시나리오에 적합합니다. 올바른 인증 방법을 선택하는 방법에 대한 지침은 인증 지침을 참조하세요.

인증 옵션

이 문서에서는 다양한 시나리오에 맞게 여러 인증 방법을 보여 줍니다.

사용자 상호 작용을 사용하는 프로덕션 애플리케이션의 경우 Microsoft Entra ID 인증을 사용합니다.

<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="19.225.1" />
<PackageReference Include="Microsoft.VisualStudio.Services.InteractiveClient" Version="19.225.1" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.61.3" />

자동화된 시나리오, CI/CD 파이프라인 및 서버 애플리케이션의 경우:

<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="19.225.1" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.61.3" />

Azure 서비스(Functions, App Service 등)에서 실행되는 애플리케이션의 경우:

<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="19.225.1" />
<PackageReference Include="Azure.Identity" Version="1.10.4" />

개인용 액세스 토큰 인증

개발 및 테스트 시나리오의 경우:

<PackageReference Include="Microsoft.TeamFoundationServer.Client" Version="19.225.1" />

C# 코드 예제

다음 예제에서는 다른 인증 방법을 사용하여 작업 항목을 가져오는 방법을 보여 줍니다.

예제 1: Microsoft Entra ID 인증(대화형)

// NuGet packages:
// Microsoft.TeamFoundationServer.Client
// Microsoft.VisualStudio.Services.InteractiveClient  
// Microsoft.Identity.Client
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;

public class EntraIdQueryExecutor
{
    private readonly Uri uri;

    /// <summary>
    /// Initializes a new instance using Microsoft Entra ID authentication.
    /// </summary>
    /// <param name="orgName">Your Azure DevOps organization name</param>
    public EntraIdQueryExecutor(string orgName)
    {
        this.uri = new Uri("https://dev.azure.com/" + orgName);
    }

    /// <summary>
    /// Execute a WIQL query using Microsoft Entra ID authentication.
    /// </summary>
    /// <param name="project">The name of your project within your organization.</param>
    /// <returns>A list of WorkItem objects representing all the open bugs.</returns>
    public async Task<IList<WorkItem>> QueryOpenBugsAsync(string project)
    {
        // Use Microsoft Entra ID authentication
        var credentials = new VssAadCredential();
        var wiql = new Wiql()
        {
            Query = "SELECT [System.Id], [System.Title], [System.State] " +
                    "FROM WorkItems " +
                    "WHERE [Work Item Type] = 'Bug' " +
                    "AND [System.TeamProject] = '" + project + "' " +
                    "AND [System.State] <> 'Closed' " +
                    "ORDER BY [System.State] ASC, [System.ChangedDate] DESC",
        };

        using (var httpClient = new WorkItemTrackingHttpClient(this.uri, new VssCredentials(credentials)))
        {
            try
            {
                var result = await httpClient.QueryByWiqlAsync(wiql).ConfigureAwait(false);
                var ids = result.WorkItems.Select(item => item.Id).ToArray();

                if (ids.Length == 0)
                {
                    return Array.Empty<WorkItem>();
                }

                var fields = new[] { "System.Id", "System.Title", "System.State", "System.CreatedDate" };
                return await httpClient.GetWorkItemsAsync(ids, fields, result.AsOf).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error querying work items: {ex.Message}");
                return Array.Empty<WorkItem>();
            }
        }
    }

    /// <summary>
    /// Print the results of the work item query.
    /// </summary>
    public async Task PrintOpenBugsAsync(string project)
    {
        var workItems = await this.QueryOpenBugsAsync(project).ConfigureAwait(false);
        Console.WriteLine($"Query Results: {workItems.Count} items found");

        foreach (var workItem in workItems)
        {
            Console.WriteLine($"{workItem.Id}\t{workItem.Fields["System.Title"]}\t{workItem.Fields["System.State"]}");
        }
    }
}

예제 2: 서비스 주체 인증(자동화된 시나리오)

// NuGet packages:
// Microsoft.TeamFoundationServer.Client
// Microsoft.Identity.Client
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Identity.Client;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;

public class ServicePrincipalQueryExecutor
{
    private readonly Uri uri;
    private readonly string clientId;
    private readonly string clientSecret;
    private readonly string tenantId;

    /// <summary>
    /// Initializes a new instance using Service Principal authentication.
    /// </summary>
    /// <param name="orgName">Your Azure DevOps organization name</param>
    /// <param name="clientId">Service principal client ID</param>
    /// <param name="clientSecret">Service principal client secret</param>
    /// <param name="tenantId">Azure AD tenant ID</param>
    public ServicePrincipalQueryExecutor(string orgName, string clientId, string clientSecret, string tenantId)
    {
        this.uri = new Uri($"https://dev.azure.com/{orgName}");
        this.clientId = clientId;
        this.clientSecret = clientSecret;
        this.tenantId = tenantId;
    }

    /// <summary>
    /// Execute a WIQL query using Service Principal authentication.
    /// </summary>
    public async Task<IList<WorkItem>> QueryOpenBugsAsync(string project)
    {
        // Acquire token using Service Principal
        var app = ConfidentialClientApplicationBuilder
            .Create(this.clientId)
            .WithClientSecret(this.clientSecret)
            .WithAuthority($"https://login.microsoftonline.com/{this.tenantId}")
            .Build();

        var scopes = new[] { "https://app.vssps.visualstudio.com/.default" };
        var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();

        var credentials = new VssOAuthAccessTokenCredential(result.AccessToken);
        var wiql = new Wiql()
        {
            Query = "SELECT [System.Id], [System.Title], [System.State] " +
                    "FROM WorkItems " +
                    "WHERE [Work Item Type] = 'Bug' " +
                    "AND [System.TeamProject] = '" + project + "' " +
                    "AND [System.State] <> 'Closed' " +
                    "ORDER BY [System.State] ASC, [System.ChangedDate] DESC",
        };

        using (var httpClient = new WorkItemTrackingHttpClient(this.uri, new VssCredentials(credentials)))
        {
            try
            {
                var queryResult = await httpClient.QueryByWiqlAsync(wiql).ConfigureAwait(false);
                var ids = queryResult.WorkItems.Select(item => item.Id).ToArray();

                if (ids.Length == 0)
                {
                    return Array.Empty<WorkItem>();
                }

                var fields = new[] { "System.Id", "System.Title", "System.State", "System.CreatedDate" };
                return await httpClient.GetWorkItemsAsync(ids, fields, queryResult.AsOf).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error querying work items: {ex.Message}");
                return Array.Empty<WorkItem>();
            }
        }
    }
}

예제 3: 관리 ID 인증(Azure 호스팅 앱)

// NuGet packages:
// Microsoft.TeamFoundationServer.Client
// Azure.Identity
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Identity;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;

public class ManagedIdentityQueryExecutor
{
    private readonly Uri uri;

    /// <summary>
    /// Initializes a new instance using Managed Identity authentication.
    /// </summary>
    /// <param name="orgName">Your Azure DevOps organization name</param>
    public ManagedIdentityQueryExecutor(string orgName)
    {
        this.uri = new Uri($"https://dev.azure.com/{orgName}");
    }

    /// <summary>
    /// Execute a WIQL query using Managed Identity authentication.
    /// </summary>
    public async Task<IList<WorkItem>> QueryOpenBugsAsync(string project)
    {
        // Use Managed Identity to acquire token
        var credential = new DefaultAzureCredential();
        var tokenRequestContext = new TokenRequestContext(new[] { "https://app.vssps.visualstudio.com/.default" });
        var tokenResult = await credential.GetTokenAsync(tokenRequestContext);

        var credentials = new VssOAuthAccessTokenCredential(tokenResult.Token);
        var wiql = new Wiql()
        {
            Query = "SELECT [System.Id], [System.Title], [System.State] " +
                    "FROM WorkItems " +
                    "WHERE [Work Item Type] = 'Bug' " +
                    "AND [System.TeamProject] = '" + project + "' " +
                    "AND [System.State] <> 'Closed' " +
                    "ORDER BY [System.State] ASC, [System.ChangedDate] DESC",
        };

        using (var httpClient = new WorkItemTrackingHttpClient(this.uri, new VssCredentials(credentials)))
        {
            try
            {
                var queryResult = await httpClient.QueryByWiqlAsync(wiql).ConfigureAwait(false);
                var ids = queryResult.WorkItems.Select(item => item.Id).ToArray();

                if (ids.Length == 0)
                {
                    return Array.Empty<WorkItem>();
                }

                var fields = new[] { "System.Id", "System.Title", "System.State", "System.CreatedDate" };
                return await httpClient.GetWorkItemsAsync(ids, fields, queryResult.AsOf).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error querying work items: {ex.Message}");
                return Array.Empty<WorkItem>();
            }
        }
    }
}

예제 4: 개인용 액세스 토큰 인증

// NuGet package: Microsoft.TeamFoundationServer.Client
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;

public class PatQueryExecutor
{
    private readonly Uri uri;
    private readonly string personalAccessToken;

    /// <summary>
    /// Initializes a new instance using Personal Access Token authentication.
    /// </summary>
    /// <param name="orgName">Your Azure DevOps organization name</param>
    /// <param name="personalAccessToken">Your Personal Access Token</param>
    public PatQueryExecutor(string orgName, string personalAccessToken)
    {
        this.uri = new Uri("https://dev.azure.com/" + orgName);
        this.personalAccessToken = personalAccessToken;
    }

    /// <summary>
    /// Execute a WIQL query using Personal Access Token authentication.
    /// </summary>
    /// <param name="project">The name of your project within your organization.</param>
    /// <returns>A list of WorkItem objects representing all the open bugs.</returns>
    public async Task<IList<WorkItem>> QueryOpenBugsAsync(string project)
    {
        var credentials = new VssBasicCredential(string.Empty, this.personalAccessToken);
        var wiql = new Wiql()
        {
            Query = "SELECT [System.Id], [System.Title], [System.State] " +
                    "FROM WorkItems " +
                    "WHERE [Work Item Type] = 'Bug' " +
                    "AND [System.TeamProject] = '" + project + "' " +
                    "AND [System.State] <> 'Closed' " +
                    "ORDER BY [System.State] ASC, [System.ChangedDate] DESC",
        };

        using (var httpClient = new WorkItemTrackingHttpClient(this.uri, new VssCredentials(credentials)))
        {
            try
            {
                var result = await httpClient.QueryByWiqlAsync(wiql).ConfigureAwait(false);
                var ids = result.WorkItems.Select(item => item.Id).ToArray();

                if (ids.Length == 0)
                {
                    return Array.Empty<WorkItem>();
                }

                var fields = new[] { "System.Id", "System.Title", "System.State", "System.CreatedDate" };
                return await httpClient.GetWorkItemsAsync(ids, fields, result.AsOf).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error querying work items: {ex.Message}");
                return Array.Empty<WorkItem>();
            }
        }
    }
}

사용 예제

Microsoft Entra ID 인증 사용(대화형)

class Program
{
    static async Task Main(string[] args)
    {
        var executor = new EntraIdQueryExecutor("your-organization-name");
        await executor.PrintOpenBugsAsync("your-project-name");
    }
}

서비스 주체 인증 사용(CI/CD 시나리오)

class Program
{
    static async Task Main(string[] args)
    {
        // These values should come from environment variables or Azure Key Vault
        var clientId = Environment.GetEnvironmentVariable("AZURE_CLIENT_ID");
        var clientSecret = Environment.GetEnvironmentVariable("AZURE_CLIENT_SECRET");
        var tenantId = Environment.GetEnvironmentVariable("AZURE_TENANT_ID");
        
        var executor = new ServicePrincipalQueryExecutor("your-organization-name", clientId, clientSecret, tenantId);
        var workItems = await executor.QueryOpenBugsAsync("your-project-name");
        
        Console.WriteLine($"Found {workItems.Count} open bugs via automation");
        foreach (var item in workItems)
        {
            Console.WriteLine($"Bug {item.Id}: {item.Fields["System.Title"]}");
        }
    }
}

관리 ID 인증 사용(Azure Functions/App Service)

public class WorkItemQueryFunction
{
    [FunctionName("QueryOpenBugs")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req,
        ILogger log)
    {
        var executor = new ManagedIdentityQueryExecutor("your-organization-name");
        var workItems = await executor.QueryOpenBugsAsync("your-project-name");
        
        return new OkObjectResult(new { 
            Count = workItems.Count,
            Items = workItems.Select(wi => new { 
                Id = wi.Id, 
                Title = wi.Fields["System.Title"],
                State = wi.Fields["System.State"]
            })
        });
    }
}

개인용 액세스 토큰 인증 사용(개발/테스트)

class Program
{
    static async Task Main(string[] args)
    {
        var pat = Environment.GetEnvironmentVariable("AZURE_DEVOPS_PAT"); // Never hardcode PATs
        var executor = new PatQueryExecutor("your-organization-name", pat);
        var workItems = await executor.QueryOpenBugsAsync("your-project-name");
        
        Console.WriteLine($"Found {workItems.Count} open bugs");
        foreach (var item in workItems)
        {
            Console.WriteLine($"Bug {item.Id}: {item.Fields["System.Title"]}");
        }
    }
}

모범 사례

인증

  • 사용자 로그인을 사용하여 대화형 애플리케이션에 Microsoft Entra ID 사용
  • 자동화된 시나리오, CI/CD 파이프라인 및 서버 애플리케이션에 서비스 주체 사용
  • Azure 서비스(Functions, App Service, VM)에서 실행되는 애플리케이션에 관리 ID 사용
  • 프로덕션 환경에서 개인용 액세스 토큰을 사용하지 마세요. 개발 및 테스트에만 사용
  • 소스 코드에서 자격 증명을 하드 코딩하지 마세요. 환경 변수 또는 Azure Key Vault 사용
  • 장기 실행 애플리케이션에 대한 자격 증명 회전 구현
  • 적절한 범위 확인: 작업 항목 쿼리에는 Azure DevOps에서 적절한 읽기 권한이 필요합니다.

오류 처리

  • 일시적인 오류에 대한 지수 백오프를 사용하여 재시도 논리 구현
  • 디버깅 및 모니터링을 위해 오류를 적절하게 로그하세요
  • 인증 실패 및 네트워크 시간 제한과 같은 특정 예외 처리
  • 장기 실행 작업에 취소 토큰 사용

성능

  • 여러 항목을 쿼리할 때 작업 항목을 일괄적으로 검색합니다.
  • 큰 데이터 세트에 TOP 절을 사용하여 쿼리 결과 제한
  • 자주 액세스하는 데이터를 캐시 하여 API 호출을 줄입니다.
  • 적절한 필드를 사용하여 데이터 전송 최소화

쿼리 최적화

  • 성능 향상을 위해 SELECT * 대신 특정 필드 이름 사용
  • 서버에서 결과를 필터링하는 적절한 WHERE 절 추가
  • 사용 사례에 적합한 주문 결과
  • 큰 결과 집합에 대한 쿼리 제한 및 페이지 매김 고려

문제 해결

인증 문제

  • Microsoft Entra ID 인증 실패: 사용자에게 적절한 권한이 있고 Azure DevOps에 로그인되었는지 확인
  • 서비스 주체 인증 실패: 클라이언트 ID, 비밀 및 테넌트 ID가 올바른지 확인합니다. Azure DevOps에서 서비스 주체 권한 확인
  • 관리 ID 인증 실패: Azure 리소스에 관리 ID를 사용하도록 설정하고 적절한 권한이 있는지 확인합니다.
  • PAT 인증 실패: 토큰이 유효하고 적절한 범위가 있는지 확인합니다(vso.work 작업 항목 액세스의 경우).
  • 토큰 만료: PAT가 만료되었는지 확인하고 필요한 경우 새 토큰을 생성합니다.

쿼리 이슈

  • 잘못된 WIQL 구문: 작업 항목 쿼리 언어 구문이 올바른지 확인합니다.
  • 프로젝트 이름 오류: 프로젝트 이름이 존재하고 철자가 올바른지 확인합니다.
  • 필드 이름 오류: 올바른 시스템 필드 이름 사용(예: System.Id, System.Title)

일반적인 예외

  • VssUnauthorizedException: 인증 자격 증명 및 사용 권한 확인
  • ArgumentException: 모든 필수 매개 변수가 제공되고 유효한지 확인합니다.
  • HttpRequestException: 네트워크 연결 및 서비스 가용성 확인

성능 문제

  • 느린 쿼리: 적절한 WHERE 절을 추가하고 결과 세트를 제한하기
  • 메모리 사용량: 대량 결과 집합을 일괄 처리로 처리
  • 속도 제한: 지수 백오프를 사용하여 재시도 논리 구현

다음 단계