You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用C# API编程下载Azure DevOps用户故事的附件?

我来帮你搞定这个问题!那些旧的NuGet包早就被微软淘汰了,现在咱们得用官方新出的Azure DevOps .NET SDK来操作VSTS/Azure DevOps服务,这是目前最靠谱的方案。

解决方法:用Azure DevOps .NET SDK批量下载用户故事附件

第一步:安装正确的NuGet包

你之前找的那两个旧包已经被弃用,现在只需要安装一个官方包就够了:Microsoft.TeamFoundationServer.Client,它包含了所有操作工作项、附件的必要API。

  • 可以通过NuGet包管理器搜索安装,或者用命令行:
    # NuGet包管理器控制台命令
    Install-Package Microsoft.TeamFoundationServer.Client
    
    # 或者.NET CLI命令
    dotnet add package Microsoft.TeamFoundationServer.Client
    

第二步:完整可运行的代码示例

下面是我整理的代码,直接替换你的组织、项目、工作项ID和PAT就能用,专门处理批量附件下载:

using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

class WorkItemAttachmentDownloader
{
    static async Task Main(string[] args)
    {
        // 替换成你的实际信息
        string orgUrl = "https://dev.azure.com/你的组织名";
        string projectName = "你的项目名";
        int targetWorkItemId = 1234; // 你的用户故事ID
        string patToken = "你的个人访问令牌";

        // 建立连接
        var credentials = new VssBasicCredential(string.Empty, patToken);
        using (var connection = new VssConnection(new Uri(orgUrl), credentials))
        {
            var workItemClient = connection.GetClient<WorkItemTrackingHttpClient>();

            // 获取工作项的完整信息(必须指定Relations扩展才能拿到附件)
            var workItem = await workItemClient.GetWorkItemAsync(
                projectName, 
                targetWorkItemId, 
                expand: WorkItemExpand.Relations
            );

            // 筛选出所有附件类型的关联项
            var attachments = workItem.Relations?
                .Where(rel => rel.Rel == "AttachedFile")
                .ToList();

            if (attachments == null || !attachments.Any())
            {
                Console.WriteLine("这个用户故事没有附件哦");
                return;
            }

            // 创建本地保存目录
            string saveDir = Path.Combine(Environment.CurrentDirectory, $"WorkItem_{targetWorkItemId}_附件");
            Directory.CreateDirectory(saveDir);
            Console.WriteLine($"准备下载 {attachments.Count} 个附件到目录:{saveDir}");

            // 批量下载附件
            foreach (var attachment in attachments)
            {
                try
                {
                    if (attachment.Attributes.TryGetValue("name", out var fileNameObj))
                    {
                        string fileName = fileNameObj.ToString();
                        string attachmentUrl = attachment.Url;

                        // 处理同名文件,避免覆盖
                        string savePath = GetUniqueFilePath(saveDir, fileName);

                        // 下载并保存
                        using (var httpClient = connection.Client)
                        {
                            var response = await httpClient.GetAsync(attachmentUrl);
                            response.EnsureSuccessStatusCode();

                            using (var stream = await response.Content.ReadAsStreamAsync())
                            using (var fileStream = new FileStream(savePath, FileMode.Create))
                            {
                                await stream.CopyToAsync(fileStream);
                                Console.WriteLine($"✅ 已下载:{fileName}");
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"❌ 下载失败:{attachment.Attributes["name"]},错误信息:{ex.Message}");
                }
            }

            Console.WriteLine("\n所有附件处理完成!");
        }
    }

    // 辅助方法:生成不重复的文件路径
    private static string GetUniqueFilePath(string directory, string fileName)
    {
        string basePath = Path.Combine(directory, fileName);
        string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
        string ext = Path.GetExtension(fileName);
        int counter = 1;

        while (File.Exists(basePath))
        {
            basePath = Path.Combine(directory, $"{fileNameWithoutExt}_{counter}{ext}");
            counter++;
        }

        return basePath;
    }
}

重要注意事项

  • 个人访问令牌(PAT):你需要在Azure DevOps门户生成一个PAT,权限至少要勾选工作项(读取)和附件(读取和下载),否则会有权限错误。
  • 批量处理优化:代码里加了异常捕获,单个附件下载失败不会影响其他附件;还加了同名文件处理,避免覆盖旧文件。
  • 权限验证:如果遇到401/403错误,先检查PAT的权限范围和有效期,确保它能访问目标项目。

常见问题排查

  • 找不到WorkItemTrackingHttpClient?确认你已经安装了Microsoft.TeamFoundationServer.Client包,并且引用了对应的命名空间。
  • 附件列表为空?检查工作项ID是否正确,或者工作项的附件是不是真的存在(有时候附件可能被删除了)。
  • 下载速度慢?如果附件很多,可以考虑加个异步并行处理(比如用Parallel.ForEach),但注意不要触发API限流。

内容的提问来源于stack exchange,提问作者ray500

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 10:08:24