Partager via


Exemples de bibliothèque de client .NET pour Azure DevOps

Azure DevOps Services | Azure DevOps Server | Azure DevOps Server 2022

Découvrez comment étendre et intégrer Azure DevOps à l’aide des bibliothèques clientes .NET avec des méthodes d’authentification modernes et des pratiques de codage sécurisées.

Conditions préalables

Packages NuGet requis :

Recommandations d’authentification :

Importante

Cet article présente plusieurs méthodes d’authentification pour différents scénarios. Choisissez la méthode la plus appropriée en fonction de votre environnement de déploiement et des exigences de sécurité.

Exemple de connexion principale et d’élément de travail

Cet exemple complet illustre les meilleures pratiques pour la connexion à Azure DevOps et l’utilisation d’éléments de travail :

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();
    }
}

Méthodes d’authentification

Pour les applications qui prennent en charge l’authentification interactive ou qui ont des jetons Microsoft Entra :

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);
}

Authentification d’un principal du service

Pour les scénarios automatisés et les pipelines CI/CD :

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);
}

Authentification d’une identité managée

Pour les applications hébergées par Azure (recommandées pour les scénarios cloud) :

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);
}

Authentification interactive (.NET Framework uniquement)

Pour les applications de bureau nécessitant une connexion utilisateur :

/// <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);
}

Authentification par jeton d’accès personnel (hérité)

Avertissement

Les jetons d’accès personnels sont dépréciés. Utilisez plutôt des méthodes d’authentification modernes. Consultez les conseils d’authentification pour les options de migration.

/// <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);
}

Exemples d’utilisation complets

Fonction Azure avec identité managée

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;
        }
    }
}

Application console avec principal de service

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);
        }
    }
}

Meilleures pratiques

Considérations relatives à la sécurité

Gestion des informations d’identification :

  • Ne jamais coder en dur les informations d’identification dans le code source
  • Utiliser Azure Key Vault pour stocker des secrets
  • Préférer les identités managées pour les applications hébergées par Azure
  • Utiliser des certificats plutôt que des secrets client pour les principaux de service
  • Renouveler les identifiants régulièrement en suivant les stratégies de sécurité

Contrôle d’accès:

  • Appliquer le principe du privilège minimum
  • Utilisez des étendues spécifiques lors de l’acquisition de jetons
  • Surveiller et auditer les événements d’authentification
  • Implémenter des stratégies d’accès conditionnel le cas échéant

Optimisation des performances

Gestion des connexions :

  • Réutiliser des instances VssConnection entre les opérations
  • Regrouper les clients HTTP à travers l’objet de connexion
  • Implémenter des modèles d’élimination appropriés
  • Configurer les délais d’expiration de manière appropriée

Opérations par lots :

  • Traiter les éléments de travail par lots (recommandé : 100 éléments)
  • Utiliser le traitement parallèle pour les opérations indépendantes
  • Implémenter une stratégie de réessai avec retrait exponentiel
  • Mettre en cache les données fréquemment sollicitées en cas de besoin

Gestion des erreurs

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);
}

Conseils sur la migration

Des jetons d'accès personnels à l'authentification moderne

Étape 1 : Évaluer l’utilisation actuelle

  • Identifier toutes les applications à l’aide de PAT
  • Déterminer les environnements de déploiement (Azure ou local)
  • Évaluer les exigences de sécurité

Étape 2 : Choisir la méthode de remplacement

  • Hébergé par Azure : Migrer vers des identités managées
  • Pipelines CI/CD : utiliser des principaux de service
  • Applications interactives : Implémenter l’authentification Microsoft Entra
  • Applications de bureau : envisagez le flux de code de l’appareil

Étape 3 : Implémentation

  • Mettre à jour le code d’authentification à l’aide des exemples précédents
  • Tester soigneusement dans l’environnement de développement
  • Déployer de manière incrémentielle en production
  • Surveiller les problèmes d’authentification

Pour obtenir des instructions détaillées sur la migration, consultez Remplacer les paTs par des jetons Microsoft Entra.