使用MS Graph API C#无法获取OneDrive指定文件夹全部文件和文件夹
实际(根目录/SGIM文件夹下):
- 文件:62,683
- 文件夹:13,598
程序统计结果:
- 文件:25,887
- 文件夹:13,576
程序无异常抛出,但统计数量少于实际,以下是代码片段:
private HttpClient oneDriveHttpClient; public string AccessToken = "eyJ0eXAiOiJKV1QiLCJub25j........................"; public string RefreshToken = "..............................."; List<Item> allitems = new List<Item>(); public MainProgram() { string requestUri = "/v1.0/users/d7ddb19e-xxxx-xxxx-xxxx-xxxxxxx/drive/root:/SGIM:/children"; this.oneDriveHttpClient = new HttpClient() { BaseAddress = new Uri("https://graph.microsoft.com") }; await GetOneDriveFolderView(requestUri); } public async Task GetOneDriveFolderView(string requestUri) { string errorlog = Global.ErrorLog; ConcurrentQueue<string> pendingitems = new ConcurrentQueue<string>(); List<Item> items = new List<Item>(); try { pendingitems.Enqueue(requestUri); do { items = new List<Item>(); try { pendingitems.TryDequeue(out requestUri); items = await GetOneDriveChildItems(requestUri); if (items != null && items.Count > 0) { var folders = items.Where(s => s != null && s.Folder != null).ToList(); if (folders != null && folders.Count > 0) { foreach (var folder in folders) { string requestUri2 = ""; if (requestUri.EndsWith("/root/children")) { requestUri2 = requestUri.Replace(":/children", "").Replace("/children", "") + ":/" + Uri.EscapeUriString(folder.Name) + ":/children"; } else { requestUri2 = requestUri.Replace(":/children", "").Replace("/children", "") + "/" + Uri.EscapeUriString(folder.Name) + ":/children"; } pendingitems.Enqueue(requestUri2); } } } } catch (Exception ex) { System.IO.File.AppendAllText(errorlog, "\r\nDateTime:" + DateTime.Now.ToString() + "\r\nRequestUri" + requestUri + "\r\n" + ex.Message + "\r\n" + ex.StackTrace); } } while (pendingitems.Count > 0); long totalcount = allitems.Count(); long folderscount = allitems.Where(s => s.Folder != null).Count(); long filescount = allitems.Where(s => s.File != null).Count(); System.IO.File.AppendAllText(errorlog, "\r\nDateTime:" + DateTime.Now.ToString() + "\r\n\r\nFolders Count: " + folderscount + "\r\nFiles Count: " + filescount + "\r\nTotal Count: " + totalcount); } catch (Exception ex) { System.IO.File.AppendAllText(errorlog, "\r\nDateTime:" + DateTime.Now.ToString() + "\r\n" + ex.Message + "\r\n" + ex.StackTrace); } } public async Task<List<Item>> GetOneDriveChildItems(string requestUri) { List<Item> files = null; List<Item> tmpitems = new List<Item>(); try { OneDriveResponse<Item[]> oneDriveResponse = null; do { oneDriveResponse = null; int retry = 3; for (int i = 1; i <= retry; i++) { try { oneDriveResponse = await this.GetOneDriveItemSet<Item[]>(requestUri).ConfigureAwait(false); break; } catch (Exception ex) { System.IO.File.AppendAllText(errorlog, "\r\nDateTime:" + DateTime.Now.ToString() + "\r\nRetry Attempt " + i.ToString() + " - FilesResource.ListRequest Execute For RequestURI " + requestUri + "\r\n" + ex.Message + "\r\n" + ex.StackTrace); Thread.Sleep(3000 * i); oneDriveResponse = null; } if (i > retry) { break; } } if (oneDriveResponse != null) { requestUri = oneDriveResponse.NextLink; } else { requestUri = null; } if (oneDriveResponse != null && oneDriveResponse.Value != null) { files = oneDriveResponse.Value.ToList(); if (files != null && files.Count > 0) { allitems.AddRange(files); tmpitems.AddRange(files); } } } while (requestUri != null); } catch (Exception ex) { System.IO.File.AppendAllText(errorlog, "\r\nDateTime:" + DateTime.Now.ToString() + "\r\n" + ex.Message + "\r\n" + ex.StackTrace); } return tmpitems; } private async Task<OneDriveResponse<T>> GetOneDriveItemSet<T>(string requestUri) { try { // Send the request to OneDrive and get the response. var response = await this.SendOneDriveRequest(new HttpRequestMessage(HttpMethod.Get, requestUri)).ConfigureAwait(false); // Request was successful. Read the content returned. string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); return JsonConvert.DeserializeObject<OneDriveResponse<T>>(content); } catch (Exception ex) { throw ex; } } private async Task<HttpResponseMessage> SendOneDriveRequest(HttpRequestMessage request) { request.Headers.Authorization = new AuthenticationHeaderValue("bearer", this.AccessToken); var response = await this.oneDriveHttpClient.SendAsync(request).ConfigureAwait(false); // Check for token refresh if (response.StatusCode == HttpStatusCode.Unauthorized) { var authresult = new AuthenticationResult(); authresult.Code = this.RefreshToken; TokenResponse tokenresponse = await GetAccessToken3(authresult); this.AccessToken = tokenresponse.AccessToken; this.RefreshToken = tokenresponse.RefreshToken; var newRequest = await request.Clone().ConfigureAwait(false); newRequest.Headers.Authorization = new AuthenticationHeaderValue("bearer", this.AccessToken); response.Dispose(); response = await this.oneDriveHttpClient.SendAsync(newRequest).ConfigureAwait(false); } // Any failures (including those from re-issuing after a refresh) will ne handled here if (!response.IsSuccessStatusCode) { throw OneDriveHttpException.FromResponse(response); } return response; } public class OneDriveResponse<T> { [JsonProperty("@odata.context")] public string Context { get; set; } [JsonProperty("@odata.nextLink")] public string NextLink { get; set; } [JsonProperty("@odata.deltaLink")] public string DeltaLink { get; set; } [JsonProperty("@delta.token")] public string DeltaToken { get; set; } [JsonProperty("value")] public T Value { get; set; } public OneDriveResponse() { } public OneDriveResponse(T value) { this.Value = value; } } public class Item { [JsonProperty("id")] public string Id { get; set; } [JsonProperty("createdBy")] public IdentitySet CreatedBy { get; set; } [JsonProperty("createdDateTime")] public DateTime CreatedDateTime { get; set; } [JsonProperty("cTag")] public string CTag { get; set; } [JsonProperty("description")] public string Description { get; set; } [JsonProperty("eTag")] public string ETag { get; set; } [JsonProperty("file")] public FileFacet File { get; set; } [JsonProperty("fileSystemInfo")] public FileSystemInfoFacet FileSystemInfo { get; set; } [JsonProperty("folder")] public FolderFacet Folder { get; set; } [JsonProperty("lastModifiedBy")] public IdentitySet LastModifiedBy { get; set; } [JsonProperty("lastModifiedDateTime")] public DateTime? LastModifiedDateTime { get; set; } [JsonProperty("name")] public string Name { get; set; } [JsonProperty("parentReference")] public ItemReference ParentReference { get; set; } [JsonProperty("size")] public long Size { get; set; } [JsonProperty("deleted")] public DeletedFacet Deleted { get; set; } public string PathDisplay { get; set; } } public class FileFacet { [JsonProperty("mimeType")] public string MimeType { get; set; } [JsonProperty("hashes")] public HashesFacet Hashes { get; set; } } public class FolderFacet { [JsonProperty("childCount")] public long ChildCount { get; set; } }
排查出的核心问题
1. 文件夹路径拼接逻辑错误
在GetOneDriveFolderView中拼接子文件夹请求URI时,非根目录场景下使用/拼接会导致URI格式错误,部分文件夹的内容请求会失败(但未触发异常,因为错误被捕获后仅记录日志,程序继续执行),导致这部分文件夹下的文件全部丢失统计。
2. 集合线程安全问题
allitems使用普通List<Item>,在异步并发场景下,多个任务同时调用AddRange会导致集合内部结构损坏,直接造成元素丢失。List<T>本身不是线程安全集合,并发修改无锁保护必然出现数据丢失。
3. 重试逻辑无效且未处理失败场景
GetOneDriveChildItems中的if (i > retry)判断完全冗余(循环条件是i <= retry,永远不会触发);当三次重试都失败时,oneDriveResponse为null,程序直接跳过该文件夹的分页处理,导致该文件夹下所有内容都未被统计。
4. 未验证分页完整性
虽然代码处理了NextLink,但未确认@odata.nextLink是否被正确反序列化(虽然JsonProperty注解正确,但需确保响应中该字段存在时能被正常读取),若分页未被完整遍历,会丢失某一页的文件数据。
修复建议
修改文件夹请求方式:
放弃路径拼接,改用文件夹Id构造请求URI,彻底避免路径格式错误:string requestUri2 = $"/v1.0/users/d7ddb19e-xxxx-xxxx-xxxx-xxxxxxx/drive/items/{folder.Id}/children";替换为线程安全集合:
将List<Item> allitems = new List<Item>()改为:ConcurrentBag<Item> allitems = new ConcurrentBag<Item>();或保留
List但添加锁保护:lock(allitems) { allitems.AddRange(files); tmpitems.AddRange(files); }修复重试逻辑:
移除冗余的if (i > retry)判断,在重试结束后添加失败处理:if (oneDriveResponse == null) { System.IO.File.AppendAllText(errorlog, "\r\nDateTime:" + DateTime.Now.ToString() + "\r\n所有重试失败,请求URI:" + requestUri); break; // 终止该文件夹的分页遍历 }增强错误日志:
在GetOneDriveFolderView的循环中,当items == null || items.Count == 0时记录日志,定位哪些文件夹未返回内容,辅助排查问题。
内容的提问来源于stack exchange,提问作者Gags

