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

ASP.NET Core Web API中使用Microsoft Graph createUploadSession传大文件遇损坏

问题描述

我正在开发ASP.NET Core Web API,需要向SharePoint驱动器上传4MB以上的大文件。经调研使用Microsoft Graph的createUploadSession端点:

https://graph.microsoft.com/v1.0/me/drive/root:/{fileName}:/createUploadSession

该端点支持大文件上传,但使用Graph API 5.50.0版本时,虽能成功创建UploadSession并完成上传,但仅txt等小文件可正常打开,图片及更大文件上传后会损坏。以下是我的代码:

public async Task UploadFileInChunks(string uploadUrl, Stream fileStream)
{
    const int maxChunkSize = 320 * 1024; // 320KB
    var buffer = new byte[maxChunkSize];
    long fileLength = fileStream.Length;
    long totalBytesRead = 0;
    int bytesRead;

    while ((bytesRead = await fileStream.ReadAsync(buffer, 0, maxChunkSize)) > 0)
    {
        var chunkData = new ByteArrayContent(buffer, 0, bytesRead);
        chunkData.Headers.ContentRange = new ContentRangeHeaderValue(totalBytesRead, totalBytesRead + bytesRead - 1, fileLength);

        using (var requestMessage = new HttpRequestMessage(HttpMethod.Put, uploadUrl))
        {
            requestMessage.Content = chunkData;

            var response = await new HttpClient().SendAsync(requestMessage);
            response.EnsureSuccessStatusCode();
        }

        totalBytesRead += bytesRead;
    }
}

public async Task<string?> CreateDriveItem(FileDTO file)
{
    var fileBytes = Convert.FromBase64String(file.Base64);

    var fileNameWithExtension = file.FileName + file.FileType;

    var requestBody = new CreateUploadSessionPostRequestBody
    {
        Item = new DriveItemUploadableProperties
        {
            Name = fileNameWithExtension,
        }
    };

    var uploadSession = await _graphClient.Graph.Drives[_driveId].Root.ItemWithPath(fileNameWithExtension).CreateUploadSession.PostAsync(requestBody);

    if (uploadSession == null)  
    {
        return null; 
    }

    using (var stream = new MemoryStream(fileBytes))
    {
        await UploadFileInChunks(uploadSession.UploadUrl ?? "", stream);
    }

    return "File uploaded successfully.";
}
解决方案

文件损坏的核心问题有两个:

  • 每次创建新HttpClient实例:循环内反复创建HttpClient会导致连接池耗尽,引发请求异常或数据传输不完整。
  • 未处理上传会话的响应状态:除最后一块外,大部分分块上传的响应是202 Accepted,需根据响应头中的剩余上传范围更新已上传字节数,而非单纯累加读取的字节数。

修改后的代码

首先优化UploadFileInChunks方法,复用HttpClient并正确处理响应:

// 复用HttpClient,不要在循环内创建实例
private readonly HttpClient _httpClient = new HttpClient();

public async Task UploadFileInChunks(string uploadUrl, Stream fileStream)
{
    const int maxChunkSize = 320 * 1024; // 320KB,需为320KB的整数倍(Microsoft Graph要求)
    var buffer = new byte[maxChunkSize];
    long fileLength = fileStream.Length;
    long totalBytesUploaded = 0;

    while (totalBytesUploaded < fileLength)
    {
        var bytesToRead = (int)Math.Min(maxChunkSize, fileLength - totalBytesUploaded);
        int bytesRead = await fileStream.ReadAsync(buffer, 0, bytesToRead);
        if (bytesRead == 0) break;

        var chunkData = new ByteArrayContent(buffer, 0, bytesRead);
        chunkData.Headers.ContentRange = new ContentRangeHeaderValue(
            totalBytesUploaded, 
            totalBytesUploaded + bytesRead - 1, 
            fileLength
        );
        // 明确设置内容类型,避免默认类型导致的解析问题
        chunkData.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

        using (var requestMessage = new HttpRequestMessage(HttpMethod.Put, uploadUrl))
        {
            requestMessage.Content = chunkData;
            var response = await _httpClient.SendAsync(requestMessage);

            if (response.StatusCode == HttpStatusCode.Accepted)
            {
                // 解析响应头中的已完成范围,更新已上传字节数
                var rangeHeader = response.Content.Headers.ContentRange;
                if (rangeHeader != null && rangeHeader.Ranges.Any())
                {
                    totalBytesUploaded = rangeHeader.Ranges.First().To.Value + 1;
                }
            }
            else if (response.StatusCode == HttpStatusCode.Created)
            {
                // 最后一块上传完成,退出循环
                break;
            }
            else
            {
                response.EnsureSuccessStatusCode();
            }
        }
    }
}

检查Base64转换逻辑

确保传入的file.Base64是纯Base64字符串,若包含如data:image/png;base64,的前缀,需先去除:

public async Task<string?> CreateDriveItem(FileDTO file)
{
    // 处理可能的Base64前缀
    var base64Data = file.Base64;
    if (base64Data.Contains(","))
    {
        base64Data = base64Data.Split(',')[1];
    }
    var fileBytes = Convert.FromBase64String(base64Data);

    var fileNameWithExtension = file.FileName + file.FileType;

    var requestBody = new CreateUploadSessionPostRequestBody
    {
        Item = new DriveItemUploadableProperties
        {
            Name = fileNameWithExtension,
        }
    };

    var uploadSession = await _graphClient.Graph.Drives[_driveId].Root.ItemWithPath(fileNameWithExtension).CreateUploadSession.PostAsync(requestBody);

    if (uploadSession == null)  
    {
        return null; 
    }

    using (var stream = new MemoryStream(fileBytes))
    {
        await UploadFileInChunks(uploadSession.UploadUrl ?? "", stream);
    }

    return "File uploaded successfully.";
}

补充说明

  • Microsoft Graph分块上传要求每个块的大小必须是320KB的整数倍(最后一块除外),当前设置符合要求。
  • 复用HttpClient是避免连接问题的关键,频繁创建销毁实例会导致网络不稳定。
  • 根据响应的Content-Range更新已上传字节数,可处理网络波动导致的重传场景,避免范围错误引发文件损坏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 21:52:03