如何用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
相关产品推荐
相关产品推荐

