使用 .NET 客户端库在 Azure DevOps Services 中创建错误

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

以编程方式创建工作项是 Azure DevOps Services 中的常见自动化方案。 本文介绍如何使用 .NET 客户端库和新式身份验证方法来创建缺陷(或其他工作项)。

先决条件

类别 要求
Azure DevOps - 组织
- 访问可在其中创建工作项的项目
身份验证 选择下列选项之一:
- Microsoft Entra ID 身份验证 (建议)
- 个人访问令牌 (PAT) (用于测试)
开发环境 C# 开发环境。 可以使用 Visual Studio

重要

对于生产应用程序,我们建议使用 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、应用服务等):

<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.Threading.Tasks;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.WebApi;
using Microsoft.VisualStudio.Services.WebApi.Patch;
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;

public class EntraIdBugCreator
{
    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 EntraIdBugCreator(string orgName)
    {
        this.uri = new Uri($"https://dev.azure.com/{orgName}");
    }

    /// <summary>
    /// Create a bug using Microsoft Entra ID authentication.
    /// </summary>
    /// <param name="project">The name of your project</param>
    /// <param name="title">Bug title</param>
    /// <param name="reproSteps">Reproduction steps</param>
    /// <param name="priority">Priority level (1-4)</param>
    /// <param name="severity">Severity level</param>
    /// <returns>The created WorkItem</returns>
    public async Task<WorkItem> CreateBugAsync(string project, string title, string reproSteps, int priority = 2, string severity = "3 - Medium")
    {
        // Use Microsoft Entra ID authentication
        var credentials = new VssAadCredential();
        var patchDocument = new JsonPatchDocument();

        // Add required and optional fields
        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/System.Title",
            Value = title
        });

        if (!string.IsNullOrEmpty(reproSteps))
        {
            patchDocument.Add(new JsonPatchOperation()
            {
                Operation = Operation.Add,
                Path = "/fields/Microsoft.VSTS.TCM.ReproSteps",
                Value = reproSteps
            });
        }

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Priority",
            Value = priority.ToString()
        });

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Severity",
            Value = severity
        });

        using (var connection = new VssConnection(this.uri, new VssCredentials(credentials)))
        {
            var workItemTrackingHttpClient = connection.GetClient<WorkItemTrackingHttpClient>();

            try
            {
                var result = await workItemTrackingHttpClient.CreateWorkItemAsync(patchDocument, project, "Bug").ConfigureAwait(false);
                Console.WriteLine($"Bug successfully created: Bug #{result.Id}");
                return result;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error creating bug: {ex.Message}");
                throw;
            }
        }
    }
}

示例 2:服务主体身份验证(自动化方案)

// NuGet packages:
// Microsoft.TeamFoundationServer.Client
// Microsoft.Identity.Client
using System;
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;
using Microsoft.VisualStudio.Services.WebApi.Patch;
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;

public class ServicePrincipalBugCreator
{
    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 ServicePrincipalBugCreator(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>
    /// Create a bug using Service Principal authentication.
    /// </summary>
    public async Task<WorkItem> CreateBugAsync(string project, string title, string reproSteps, int priority = 2, string severity = "3 - Medium")
    {
        // 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 patchDocument = new JsonPatchDocument();

        // Add work item fields
        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/System.Title",
            Value = title
        });

        if (!string.IsNullOrEmpty(reproSteps))
        {
            patchDocument.Add(new JsonPatchOperation()
            {
                Operation = Operation.Add,
                Path = "/fields/Microsoft.VSTS.TCM.ReproSteps",
                Value = reproSteps
            });
        }

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Priority",
            Value = priority.ToString()
        });

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Severity",
            Value = severity
        });

        using (var connection = new VssConnection(this.uri, new VssCredentials(credentials)))
        {
            var workItemTrackingHttpClient = connection.GetClient<WorkItemTrackingHttpClient>();

            try
            {
                var workItem = await workItemTrackingHttpClient.CreateWorkItemAsync(patchDocument, project, "Bug").ConfigureAwait(false);
                Console.WriteLine($"Bug successfully created: Bug #{workItem.Id}");
                return workItem;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error creating bug: {ex.Message}");
                throw;
            }
        }
    }
}

示例 3:托管身份验证(Azure 托管应用)

// NuGet packages:
// Microsoft.TeamFoundationServer.Client
// Azure.Identity
using System;
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;
using Microsoft.VisualStudio.Services.WebApi.Patch;
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;

public class ManagedIdentityBugCreator
{
    private readonly Uri uri;

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

    /// <summary>
    /// Create a bug using Managed Identity authentication.
    /// </summary>
    public async Task<WorkItem> CreateBugAsync(string project, string title, string reproSteps, int priority = 2, string severity = "3 - Medium")
    {
        // 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 patchDocument = new JsonPatchDocument();

        // Add work item fields
        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/System.Title",
            Value = title
        });

        if (!string.IsNullOrEmpty(reproSteps))
        {
            patchDocument.Add(new JsonPatchOperation()
            {
                Operation = Operation.Add,
                Path = "/fields/Microsoft.VSTS.TCM.ReproSteps",
                Value = reproSteps
            });
        }

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Priority",
            Value = priority.ToString()
        });

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Severity",
            Value = severity
        });

        using (var connection = new VssConnection(this.uri, new VssCredentials(credentials)))
        {
            var workItemTrackingHttpClient = connection.GetClient<WorkItemTrackingHttpClient>();

            try
            {
                var workItem = await workItemTrackingHttpClient.CreateWorkItemAsync(patchDocument, project, "Bug").ConfigureAwait(false);
                Console.WriteLine($"Bug successfully created: Bug #{workItem.Id}");
                return workItem;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error creating bug: {ex.Message}");
                throw;
            }
        }
    }
}

示例 4:个人访问令牌身份验证

// NuGet package: Microsoft.TeamFoundationServer.Client
using System;
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;
using Microsoft.VisualStudio.Services.WebApi.Patch;
using Microsoft.VisualStudio.Services.WebApi.Patch.Json;

public class PatBugCreator
{
    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 PatBugCreator(string orgName, string personalAccessToken)
    {
        this.uri = new Uri($"https://dev.azure.com/{orgName}");
        this.personalAccessToken = personalAccessToken;
    }

    /// <summary>
    /// Create a bug using Personal Access Token authentication.
    /// </summary>
    /// <param name="project">The name of your project</param>
    /// <param name="title">Bug title</param>
    /// <param name="reproSteps">Reproduction steps</param>
    /// <param name="priority">Priority level (1-4)</param>
    /// <param name="severity">Severity level</param>
    /// <returns>The created WorkItem</returns>
    public async Task<WorkItem> CreateBugAsync(string project, string title, string reproSteps, int priority = 2, string severity = "3 - Medium")
    {
        var credentials = new VssBasicCredential(string.Empty, this.personalAccessToken);
        var patchDocument = new JsonPatchDocument();

        // Add required and optional fields
        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/System.Title",
            Value = title
        });

        if (!string.IsNullOrEmpty(reproSteps))
        {
            patchDocument.Add(new JsonPatchOperation()
            {
                Operation = Operation.Add,
                Path = "/fields/Microsoft.VSTS.TCM.ReproSteps",
                Value = reproSteps
            });
        }

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Priority",
            Value = priority.ToString()
        });

        patchDocument.Add(new JsonPatchOperation()
        {
            Operation = Operation.Add,
            Path = "/fields/Microsoft.VSTS.Common.Severity",
            Value = severity
        });

        using (var connection = new VssConnection(this.uri, new VssCredentials(credentials)))
        {
            var workItemTrackingHttpClient = connection.GetClient<WorkItemTrackingHttpClient>();

            try
            {
                var result = await workItemTrackingHttpClient.CreateWorkItemAsync(patchDocument, project, "Bug").ConfigureAwait(false);
                Console.WriteLine($"Bug successfully created: Bug #{result.Id}");
                return result;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error creating bug: {ex.Message}");
                throw;
            }
        }
    }
}

用法示例

使用 Microsoft Entra ID 身份验证 (交互式)

class Program
{
    static async Task Main(string[] args)
    {
        var bugCreator = new EntraIdBugCreator("your-organization-name");
        
        var bug = await bugCreator.CreateBugAsync(
            project: "your-project-name",
            title: "Authorization Errors with Microsoft Accounts",
            reproSteps: "Our authorization logic needs to allow for users with Microsoft accounts (formerly Live IDs) - https://docs.microsoft.com/library/live/hh826547.aspx",
            priority: 1,
            severity: "2 - High"
        );
        
        Console.WriteLine($"Created bug with ID: {bug.Id}");
    }
}

使用服务主体身份验证(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 bugCreator = new ServicePrincipalBugCreator("your-organization-name", clientId, clientSecret, tenantId);
        
        var bug = await bugCreator.CreateBugAsync(
            project: "your-project-name",
            title: "Automated Bug Report",
            reproSteps: "Issue detected by automated testing...",
            priority: 2,
            severity: "3 - Medium"
        );
        
        Console.WriteLine($"Automated bug created: #{bug.Id}");
    }
}

使用托管身份验证 (Azure Functions/应用服务)

public class BugReportFunction
{
    [FunctionName("CreateBugReport")]
    public static async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
        ILogger log)
    {
        var bugCreator = new ManagedIdentityBugCreator("your-organization-name");
        
        var bug = await bugCreator.CreateBugAsync(
            project: "your-project-name",
            title: "Function-detected Issue",
            reproSteps: "Issue reported via Azure Function...",
            priority: 3,
            severity: "4 - Low"
        );
        
        return new OkObjectResult($"Bug created: {bug.Id}");
    }
}

使用个人访问令牌身份验证 (开发/测试)

class Program
{
    static async Task Main(string[] args)
    {
        var pat = Environment.GetEnvironmentVariable("AZURE_DEVOPS_PAT"); // Never hardcode PATs
        var bugCreator = new PatBugCreator("your-organization-name", pat);
        
        var bug = await bugCreator.CreateBugAsync(
            project: "your-project-name",
            title: "Sample Bug Title",
            reproSteps: "Steps to reproduce the issue...",
            priority: 2,
            severity: "3 - Medium"
        );
        
        Console.WriteLine($"Bug created successfully: #{bug.Id}");
    }
}

工作项字段参考

创建工作项时,通常使用以下字段:

必填字段

  • System.Title:工作项标题(所有工作项类型都需要)
  • System.WorkItemType:在 API 调用中指定类型时自动设置

常见可选字段

  • Microsoft.VSTS.TCM.ReproSteps:详细复制步骤
  • Microsoft.VSTS.Common.Priority:优先级(1=最高,4=最低)
  • Microsoft.VSTS.Common.Severity:严重程度分类
  • System.Description:常规说明或其他详细信息
  • System.AssignedTo:负责工作项的人员
  • System.AreaPath:区域分类
  • System.IterationPath:迭代冲刺分配

优先级值

  • 1:严重/最高优先级
  • 2:高优先级
  • 3:中等优先级(默认值)
  • 4:低优先级

常见严重性值

  • 1 - 严重:系统不可用,阻止进度
  • 2 - 高:主要功能损坏
  • 3 - 中等:某些功能中断(默认值)
  • 4 - 低:小问题或不重要的问题

最佳做法

身份验证

  • 使用 Microsoft Entra ID 为交互式应用程序提供用户登录功能
  • 对自动方案、CI/CD 管道和服务器应用程序使用服务主体
  • 使用托管身份 用于在 Azure 服务上运行的应用程序(函数、应用服务、虚拟机)
  • 避免在生产环境中使用个人访问令牌;仅用于开发和测试
  • 绝不要将凭据硬编码在源代码中, 使用环境变量或 Azure Key Vault
  • 为长时间运行的应用程序实现凭据轮换
  • 确保适当的范围:创建工作项需要在 Azure DevOps 中具有适当的权限

错误处理

  • 为身份验证和 API 失败实现适当的异常处理
  • 在尝试创建工作项之前验证字段值
  • 处理 API 返回的字段验证错误
  • 使用 async/await 模式 提高应用程序响应能力

性能

  • 创建多个工作项时的批处理操作
  • 进行多个 API 调用时缓存连接
  • 在长时间运行的操作中使用适当的超时值
  • 实现针对 暂时性故障的指数退避的重试逻辑

数据验证

  • 在 API 调用之前验证必填字段
  • 检查字段权限 和工作项类型规则
  • 清理用户输入 以防止注入攻击
  • 遵循项目特定的字段要求和命名规则

故障排除

身份验证问题

  • Microsoft Entra ID 身份验证失败:确保用户具有创建工作项的适当权限
  • 服务主体身份验证失败:验证客户端 ID、机密和租户 ID 是否正确;在 Azure DevOps 中检查服务主体权限
  • 托管标识身份验证失败:确保 Azure 资源已启用托管标识并具有适当的权限
  • PAT 身份验证失败:验证令牌是否具有 vso.work_write 范围且未过期
  • 403 禁止访问错误:检查项目权限和工作项类型访问权限

字段验证错误

  • 缺少必填字段:确保修补程序文档中包括所有必填字段
  • 无效的字段值:验证字段值是否与预期格式和允许的值匹配
  • 找不到字段:请检查字段名称是否拼写正确,并确保其在相应的工作项类型中存在。
  • 只读字段错误:创建过程中无法设置某些字段(例如 System.CreatedBy)

常见异常

  • VssUnauthorizedException:身份验证失败或权限不足
  • VssServiceException:服务器端验证错误或 API 问题
  • ArgumentException:参数无效或修补程序文档格式不正确
  • JsonReaderException:JSON 序列化/反序列化的问题

性能问题

  • API 响应缓慢:检查网络连接和 Azure DevOps 服务状态
  • 内存使用情况:妥善处理连接和客户端
  • 速率限制:实现 API 调用之间的适当延迟

后续步骤