使用FastEndpoints时,从Azure Blob返回FileStreamResult报错的解决方法
解决FastEndpoints返回FileStreamResult时的System.NotSupportedException错误
错误原因
- FastEndpoints响应类型约束:FastEndpoints要求泛型参数
TResponse是具备无参数构造函数的普通DTO类,框架会自动实例化并序列化该类型作为API响应。而FileStreamResult是ASP.NET Core专门用于输出文件流的ActionResult类型,它没有无参数构造函数,也不是为序列化设计的DTO,因此框架尝试实例化它时抛出异常。 - 流提前释放问题:原代码中
DownloadBlobAsync用using包裹MemoryStream,返回的FileStreamResult持有已被释放的流,即使解决了构造函数问题,后续也会出现流无法读取的错误。
解决方案
步骤1:修改Endpoint的响应类型
将Endpoint的泛型响应类型改为EmptyResponse,告诉FastEndpoints我们将手动处理响应,不需要自动序列化DTO。
步骤2:优化Blob下载逻辑
移除MemoryStream的using语句,改用BlobClient.OpenReadAsync直接获取Blob的只读流,既避免流提前释放,又减少内存缓存开销。
步骤3:手动发送FileStreamResult
在HandleAsync方法中使用SendAsync直接发送FileStreamResult,这是FastEndpoints支持发送ActionResult的标准方式。
修改后的完整代码
Endpoint类
public class Endpoint : Endpoint<DownloadFileRequest, EmptyResponse> { private readonly DatabaseContext _context; private readonly ILogger<Endpoint> _logger; private readonly IConfiguration _configuration; public Endpoint(DatabaseContext context, ILogger<Endpoint> logger, IConfiguration configuration) { _context = context; _logger = logger; _configuration = configuration; } public override void Configure() { Get("/file/{id}/download"); Roles(Role.Administrator, Role.ProjectManager, Role.GraphicDesigner, Role.Accountant); Description(b => b.WithName("DownloadFile")); } public override async Task HandleAsync(DownloadFileRequest r, CancellationToken c) { var file = await Data.GetTriadaFileAsync(_context, r.Id, c); if (file == null) { await SendNotFoundAsync(c); return; } var fileStreamResult = await Data.DownloadBlobAsync(file.AzureName, _configuration, c); if (fileStreamResult != null) { await SendAsync(fileStreamResult, cancellation: c); } else { await SendNotFoundAsync(c); } } }
Data工具类
public static class Data { public static async Task<TriadaFile?> GetTriadaFileAsync(DatabaseContext context, Guid id, CancellationToken c) { return await context.Files .FirstOrDefaultAsync(x => x.Id == id, c); } public static async Task<FileStreamResult?> DownloadBlobAsync(string blobFileName, IConfiguration configuration, CancellationToken c) { string connectionString = configuration.GetSection("Azure").GetValue<string>("BlobConnectionString")!; string containerName = configuration.GetSection("Azure").GetValue<string>("BlobContainerName")!; BlobServiceClient blobServiceClient = new BlobServiceClient(connectionString); BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient(containerName); BlobClient blobClient = containerClient.GetBlobClient(blobFileName); if (!await blobClient.ExistsAsync(c)) { return null; } // 直接获取Blob的只读流,无需内存缓存 var stream = await blobClient.OpenReadAsync(cancellationToken: c); var properties = await blobClient.GetPropertiesAsync(cancellationToken: c); var contentType = properties.Value.ContentType ?? "application/octet-stream"; return new FileStreamResult(stream, contentType) { FileDownloadName = blobFileName }; } }
补充说明
- 原
DownloadFileResponse类可以删除,因为我们不再使用自定义DTO作为响应。 - 新增了Blob存在性检查,避免尝试下载不存在的Blob时抛出异常。
内容的提问来源于stack exchange,提问作者Petr Barabáš
相关产品推荐
相关产品推荐

