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 인증(대화형 앱에 권장)
사용자 상호 작용을 사용하는 프로덕션 애플리케이션의 경우 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" />
관리 ID 인증(Azure 호스팅 앱에 권장)
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 절을 추가하고 결과 세트를 제한하기
- 메모리 사용량: 대량 결과 집합을 일괄 처리로 처리
- 속도 제한: 지수 백오프를 사용하여 재시도 논리 구현