Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Azure DevOps Services | Azure DevOps Server 2022 - Azure DevOps Server 2019
Learn how to extend and integrate with Azure DevOps using the .NET client libraries with modern authentication methods and secure coding practices.
Prerequisites
Required NuGet packages:
- Microsoft.TeamFoundationServer.Client - Core Azure DevOps APIs
- Microsoft.VisualStudio.Services.Client - Connection and authentication
- Microsoft.VisualStudio.Services.InteractiveClient - Interactive authentication flows
Authentication recommendations:
- Azure-hosted applications: Use managed identities
- CI/CD pipelines: Use service principals
- Interactive applications: Use Microsoft Entra authentication
- Legacy scenarios only: Use personal access tokens
Important
This article shows multiple authentication methods for different scenarios. Choose the most appropriate method based on your deployment environment and security requirements.
Core connection and work item example
This comprehensive example demonstrates best practices for connecting to Azure DevOps and working with work items:
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
/// <summary>
/// Demonstrates secure Azure DevOps integration with proper error handling and resource management
/// </summary>
public class AzureDevOpsService
{
private readonly VssConnection _connection;
private readonly WorkItemTrackingHttpClient _witClient;
public AzureDevOpsService(string organizationUrl, VssCredentials credentials)
{
// Create connection with proper credential management
_connection = new VssConnection(new Uri(organizationUrl), credentials);
// Get work item tracking client (reused for efficiency)
_witClient = _connection.GetClient<WorkItemTrackingHttpClient>();
}
/// <summary>
/// Creates a work item query, executes it, and returns results with proper error handling
/// </summary>
public async Task<IEnumerable<WorkItem>> GetNewBugsAsync(string projectName)
{
try
{
// Get query hierarchy with proper depth control
var queryHierarchyItems = await _witClient.GetQueriesAsync(projectName, depth: 2);
// Find 'My Queries' folder using safe navigation
var myQueriesFolder = queryHierarchyItems
.FirstOrDefault(qhi => qhi.Name.Equals("My Queries", StringComparison.OrdinalIgnoreCase));
if (myQueriesFolder == null)
{
throw new InvalidOperationException("'My Queries' folder not found in project.");
}
const string queryName = "New Bugs Query";
// Check if query already exists
var existingQuery = myQueriesFolder.Children?
.FirstOrDefault(qhi => qhi.Name.Equals(queryName, StringComparison.OrdinalIgnoreCase));
QueryHierarchyItem query;
if (existingQuery == null)
{
// Create new query with proper WIQL
query = new QueryHierarchyItem
{
Name = queryName,
Wiql = @"
SELECT [System.Id], [System.WorkItemType], [System.Title],
[System.AssignedTo], [System.State], [System.Tags]
FROM WorkItems
WHERE [System.TeamProject] = @project
AND [System.WorkItemType] = 'Bug'
AND [System.State] = 'New'
ORDER BY [System.CreatedDate] DESC",
IsFolder = false
};
query = await _witClient.CreateQueryAsync(query, projectName, myQueriesFolder.Name);
}
else
{
query = existingQuery;
}
// Execute query and get results
var queryResult = await _witClient.QueryByIdAsync(query.Id);
if (!queryResult.WorkItems.Any())
{
return Enumerable.Empty<WorkItem>();
}
// Batch process work items for efficiency
const int batchSize = 100;
var allWorkItems = new List<WorkItem>();
for (int skip = 0; skip < queryResult.WorkItems.Count(); skip += batchSize)
{
var batch = queryResult.WorkItems.Skip(skip).Take(batchSize);
var workItemIds = batch.Select(wir => wir.Id).ToArray();
// Get detailed work item information
var workItems = await _witClient.GetWorkItemsAsync(
ids: workItemIds,
fields: new[] { "System.Id", "System.Title", "System.State",
"System.AssignedTo", "System.CreatedDate" });
allWorkItems.AddRange(workItems);
}
return allWorkItems;
}
catch (Exception ex)
{
// Log error appropriately in real applications
throw new InvalidOperationException($"Failed to retrieve work items: {ex.Message}", ex);
}
}
/// <summary>
/// Properly dispose of resources
/// </summary>
public void Dispose()
{
_witClient?.Dispose();
_connection?.Dispose();
}
}
Authentication methods
Microsoft Entra authentication (Recommended)
For applications that support interactive authentication or have Microsoft Entra tokens:
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.Common;
/// <summary>
/// Authenticate using Microsoft Entra ID credentials
/// Recommended for interactive applications and modern authentication scenarios
/// </summary>
public static VssConnection CreateEntraConnection(string organizationUrl, string accessToken)
{
// Use Microsoft Entra access token for authentication
var credentials = new VssOAuthAccessTokenCredential(accessToken);
return new VssConnection(new Uri(organizationUrl), credentials);
}
/// <summary>
/// For username/password scenarios (less secure, avoid when possible)
/// </summary>
public static VssConnection CreateEntraUsernameConnection(string organizationUrl, string username, string password)
{
var credentials = new VssAadCredential(username, password);
return new VssConnection(new Uri(organizationUrl), credentials);
}
Service principal authentication
For automated scenarios and CI/CD pipelines:
using Microsoft.Identity.Client;
using Microsoft.VisualStudio.Services.Client;
/// <summary>
/// Authenticate using service principal with certificate (most secure)
/// Recommended for production automation scenarios
/// </summary>
public static async Task<VssConnection> CreateServicePrincipalConnectionAsync(
string organizationUrl,
string clientId,
string tenantId,
X509Certificate2 certificate)
{
try
{
// Create confidential client application with certificate
var app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithCertificate(certificate)
.WithAuthority(new Uri($"https://login.microsoftonline.com/{tenantId}"))
.Build();
// Acquire token for Azure DevOps
var result = await app
.AcquireTokenForClient(new[] { "https://app.vssps.visualstudio.com/.default" })
.ExecuteAsync();
// Create connection with acquired token
var credentials = new VssOAuthAccessTokenCredential(result.AccessToken);
return new VssConnection(new Uri(organizationUrl), credentials);
}
catch (Exception ex)
{
throw new AuthenticationException($"Failed to authenticate service principal: {ex.Message}", ex);
}
}
/// <summary>
/// Service principal with client secret (less secure than certificate)
/// </summary>
public static async Task<VssConnection> CreateServicePrincipalSecretConnectionAsync(
string organizationUrl,
string clientId,
string tenantId,
string clientSecret)
{
var app = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithClientSecret(clientSecret)
.WithAuthority(new Uri($"https://login.microsoftonline.com/{tenantId}"))
.Build();
var result = await app
.AcquireTokenForClient(new[] { "https://app.vssps.visualstudio.com/.default" })
.ExecuteAsync();
var credentials = new VssOAuthAccessTokenCredential(result.AccessToken);
return new VssConnection(new Uri(organizationUrl), credentials);
}
Managed identity authentication
For Azure-hosted applications (recommended for cloud scenarios):
using Azure.Identity;
using Azure.Core;
using Microsoft.VisualStudio.Services.Client;
/// <summary>
/// Authenticate using managed identity (most secure for Azure-hosted apps)
/// No credentials to manage - Azure handles everything automatically
/// </summary>
public static async Task<VssConnection> CreateManagedIdentityConnectionAsync(string organizationUrl)
{
try
{
// Use system-assigned managed identity
var credential = new ManagedIdentityCredential();
// Acquire token for Azure DevOps
var tokenRequest = new TokenRequestContext(new[] { "https://app.vssps.visualstudio.com/.default" });
var tokenResponse = await credential.GetTokenAsync(tokenRequest);
// Create connection with managed identity token
var credentials = new VssOAuthAccessTokenCredential(tokenResponse.Token);
return new VssConnection(new Uri(organizationUrl), credentials);
}
catch (Exception ex)
{
throw new AuthenticationException($"Failed to authenticate with managed identity: {ex.Message}", ex);
}
}
/// <summary>
/// Use user-assigned managed identity with specific client ID
/// </summary>
public static async Task<VssConnection> CreateUserAssignedManagedIdentityConnectionAsync(
string organizationUrl,
string managedIdentityClientId)
{
var credential = new ManagedIdentityCredential(managedIdentityClientId);
var tokenRequest = new TokenRequestContext(new[] { "https://app.vssps.visualstudio.com/.default" });
var tokenResponse = await credential.GetTokenAsync(tokenRequest);
var credentials = new VssOAuthAccessTokenCredential(tokenResponse.Token);
return new VssConnection(new Uri(organizationUrl), credentials);
}
Interactive authentication (.NET Framework only)
For desktop applications requiring user sign-in:
/// <summary>
/// Interactive authentication with Visual Studio sign-in prompt
/// .NET Framework only - not supported in .NET Core/.NET 5+
/// </summary>
public static VssConnection CreateInteractiveConnection(string organizationUrl)
{
var credentials = new VssClientCredentials();
return new VssConnection(new Uri(organizationUrl), credentials);
}
Personal access token authentication (Legacy)
Warning
Personal access tokens are being deprecated. Use modern authentication methods instead. See Authentication guidance for migration options.
/// <summary>
/// Personal Access Token authentication (legacy - use modern auth instead)
/// Only use for migration scenarios or when modern auth isn't available
/// </summary>
public static VssConnection CreatePATConnection(string organizationUrl, string personalAccessToken)
{
var credentials = new VssBasicCredential(string.Empty, personalAccessToken);
return new VssConnection(new Uri(organizationUrl), credentials);
}
Complete usage examples
Azure Function with managed identity
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
public class AzureDevOpsFunction
{
private readonly ILogger<AzureDevOpsFunction> _logger;
public AzureDevOpsFunction(ILogger<AzureDevOpsFunction> logger)
{
_logger = logger;
}
[Function("ProcessWorkItems")]
public async Task<string> ProcessWorkItems(
[TimerTrigger("0 0 8 * * MON")] TimerInfo timer)
{
try
{
var organizationUrl = Environment.GetEnvironmentVariable("AZURE_DEVOPS_ORG_URL");
var projectName = Environment.GetEnvironmentVariable("AZURE_DEVOPS_PROJECT");
// Use managed identity for secure authentication
using var connection = await CreateManagedIdentityConnectionAsync(organizationUrl);
using var service = new AzureDevOpsService(organizationUrl, connection.Credentials);
var workItems = await service.GetNewBugsAsync(projectName);
_logger.LogInformation($"Processed {workItems.Count()} work items");
return $"Successfully processed {workItems.Count()} work items";
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process work items");
throw;
}
}
}
Console application with service principal
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
class Program
{
static async Task Main(string[] args)
{
// Configure logging and configuration
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.Build();
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = loggerFactory.CreateLogger<Program>();
try
{
var settings = configuration.GetSection("AzureDevOps");
var organizationUrl = settings["OrganizationUrl"];
var projectName = settings["ProjectName"];
var clientId = settings["ClientId"];
var tenantId = settings["TenantId"];
var clientSecret = settings["ClientSecret"]; // Better: use Key Vault
// Authenticate with service principal
using var connection = await CreateServicePrincipalSecretConnectionAsync(
organizationUrl, clientId, tenantId, clientSecret);
using var service = new AzureDevOpsService(organizationUrl, connection.Credentials);
// Process work items
var workItems = await service.GetNewBugsAsync(projectName);
foreach (var workItem in workItems)
{
Console.WriteLine($"Bug {workItem.Id}: {workItem.Fields["System.Title"]}");
}
logger.LogInformation($"Successfully processed {workItems.Count()} work items");
}
catch (Exception ex)
{
logger.LogError(ex, "Application failed");
Environment.Exit(1);
}
}
}
Best practices
Security considerations
Credential management:
- Never hardcode credentials in source code
- Use Azure Key Vault for storing secrets
- Prefer managed identities for Azure-hosted applications
- Use certificates over client secrets for service principals
- Rotate credentials regularly following security policies
Access control:
- Apply principle of least privilege
- Use specific scopes when acquiring tokens
- Monitor and audit authentication events
- Implement conditional access policies where appropriate
Performance optimization
Connection management:
- Reuse VssConnection instances across operations
- Pool HTTP clients through the connection object
- Implement proper disposal patterns
- Configure timeouts appropriately
Batch operations:
- Process work items in batches (recommended: 100 items)
- Use parallel processing for independent operations
- Implement retry logic with exponential backoff
- Cache frequently accessed data when appropriate
Error handling
public async Task<T> ExecuteWithRetryAsync<T>(Func<Task<T>> operation, int maxRetries = 3)
{
var retryCount = 0;
var baseDelay = TimeSpan.FromSeconds(1);
while (retryCount < maxRetries)
{
try
{
return await operation();
}
catch (Exception ex) when (IsTransientError(ex) && retryCount < maxRetries - 1)
{
retryCount++;
var delay = TimeSpan.FromMilliseconds(baseDelay.TotalMilliseconds * Math.Pow(2, retryCount));
await Task.Delay(delay);
}
}
// Final attempt without catch
return await operation();
}
private static bool IsTransientError(Exception ex)
{
return ex is HttpRequestException ||
ex is TaskCanceledException ||
(ex is VssServiceException vssEx && vssEx.HttpStatusCode >= 500);
}
Migration guidance
From PATs to modern authentication
Step 1: Assess current usage
- Identify all applications using PATs
- Determine deployment environments (Azure vs. on-premises)
- Evaluate security requirements
Step 2: Choose replacement method
- Azure-hosted: Migrate to managed identities
- CI/CD pipelines: Use service principals
- Interactive apps: Implement Microsoft Entra authentication
- Desktop apps: Consider device code flow
Step 3: Implementation
- Update authentication code using the previous examples
- Test thoroughly in development environment
- Deploy incrementally to production
- Monitor for authentication issues
For detailed migration guidance, see Replace PATs with Microsoft Entra tokens.