Udostępnij za pośrednictwem


Przykłady biblioteki klienta platformy .NET dla usługi Azure DevOps

Azure DevOps Services | Azure DevOps Server 2022 — Azure DevOps Server 2019

Dowiedz się, jak rozszerzyć i zintegrować z usługą Azure DevOps przy użyciu bibliotek klienckich platformy .NET z nowoczesnymi metodami uwierzytelniania i bezpiecznymi praktykami kodowania.

Wymagania wstępne

Wymagane pakiety NuGet:

Zalecenia dotyczące uwierzytelniania:

Ważne

W tym artykule przedstawiono wiele metod uwierzytelniania dla różnych scenariuszy. Wybierz najbardziej odpowiednią metodę na podstawie środowiska wdrażania i wymagań dotyczących zabezpieczeń.

Przykład podstawowego połączenia i elementu roboczego

W tym kompleksowym przykładzie przedstawiono najlepsze rozwiązania dotyczące nawiązywania połączenia z usługą Azure DevOps i pracy z elementami roboczymi:

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

Metody uwierzytelniania

W przypadku aplikacji, które obsługują uwierzytelnianie interakcyjne lub mają tokeny Firmy 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);
}

Uwierzytelnianie głównego elementu usługi

W przypadku scenariuszy automatycznych oraz potoków 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);
}

Uwierzytelnianie tożsamości zarządzanej

W przypadku aplikacji hostowanych na platformie Azure (zalecane w scenariuszach w chmurze):

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

Uwierzytelnianie interakcyjne (tylko platforma.NET Framework)

W przypadku aplikacji komputerowych wymagających logowania się użytkownika:

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

Uwierzytelnianie za pomocą tokenu osobistego dostępu (wersja legacy)

Ostrzeżenie

Osobiste tokeny dostępu są przestarzałe. Zamiast tego użyj nowoczesnych metod uwierzytelniania. Zobacz Wskazówki dotyczące uwierzytelniania dotyczące opcji migracji.

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

Kompletne przykłady użycia

Funkcja platformy Azure z tożsamością zarządzaną

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

Aplikacja konsolowa z jednostką usługi

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

Najlepsze rozwiązania

Zagadnienia dotyczące zabezpieczeń

Zarządzanie poświadczeniami:

  • Nigdy nie zakodowano poświadczeń w kodzie źródłowym
  • Przechowywanie wpisów tajnych przy użyciu usługi Azure Key Vault
  • Preferuj tożsamości zarządzane dla aplikacji hostowanych na platformie Azure
  • Używanie certyfikatów zamiast wpisów tajnych klienta dla jednostek usługi
  • Regularnie zmieniaj poświadczenia zgodnie z zasadami polityki zabezpieczeń

Kontrola dostępu:

  • Stosowanie zasady najniższych uprawnień
  • Używanie określonych zakresów podczas uzyskiwania tokenów
  • Monitorowanie i inspekcja zdarzeń uwierzytelniania
  • Implementowanie zasad dostępu warunkowego w odpowiednich przypadkach

Optymalizacja wydajności

Zarządzanie połączeniami:

  • Ponowne używanie wystąpień programu VssConnection w operacjach
  • Tworzenie puli klientów HTTP za pośrednictwem obiektu połączenia
  • Implementowanie prawidłowych wzorców usuwania
  • Odpowiednio skonfiguruj limity czasu

Operacje wsadowe:

  • Przetwarzanie elementów roboczych w partiach (zalecane: 100 elementów)
  • Używanie przetwarzania równoległego na potrzeby niezależnych operacji
  • Implementowanie logiki ponawiania przy użyciu opóźnienia wykładniczego
  • Buforowanie często używanych danych , jeśli jest to konieczne

Obsługa błędów

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

Wskazówki dotyczące migracji

Od PATs do nowoczesnego uwierzytelniania

Krok 1. Ocena bieżącego użycia

  • Identyfikowanie wszystkich aplikacji przy użyciu paT
  • Określanie środowisk wdrażania (platforma Azure a środowisko lokalne)
  • Ocena wymagań dotyczących zabezpieczeń

Krok 2. Wybieranie metody zastępczej

  • Hostowane na platformie Azure: migrowanie do tożsamości zarządzanych
  • Potoki CI/CD: używanie głównych zasad usług
  • Interaktywne aplikacje: implementowanie uwierzytelniania entra firmy Microsoft
  • Aplikacje desktopowe: rozważ przepływ kodu urządzenia

Krok 3. Implementacja

  • Aktualizowanie kodu uwierzytelniania przy użyciu poprzednich przykładów
  • Dokładnie przetestuj w środowisku projektowym
  • Wdrażanie przyrostowe w środowisku produkcyjnym
  • Monitorowanie problemów z uwierzytelnianiem

Aby uzyskać szczegółowe wskazówki dotyczące migracji, zobacz Zastępowanie PATs tokenami Microsoft Entra.