.NET 5 Web API如何配置不同路由处理数字查询与图片传输
为熟悉“Web APIs”相关技术,我使用Visual Studio 2022创建了一个ASP.NET Core Web API项目。

我已将项目迁移至.NET 5版本。
目前MyControllers.cs中已编写处理Get与Post请求的相关代码。
我删除了Visual Studio自动生成的WeatherForecast.cs文件,同时在launchSettings.json中配置启动时浏览器默认跳转至MyController对应路由。
为练习依赖注入(Dependency Injection),我在解决方案中新增了一个名为TextRepository的类库项目。我此前通过另一个Visual Studio项目生成了包含0–99数字的文本文件,MyController.cs的Get method会读取该文本文件并返回内容,调用接口时浏览器即可展示对应数字列表,我同时定义了接口INumberRepository实现仓储层抽象。

为巩固相关知识点,我额外创建了ImageRepository仓储,目标是扫描Pictures文件夹下的所有图片并存入List集合,为此我通过NuGet包管理器安装了System.Drawing.Common依赖。

我目前在浏览器端针对不同业务场景调用API时遇到问题:我希望保留https://localhost:44355/api/My 路由的原有功能,即调用现有Get方法在浏览器中展示数字列表;请问如何配置独立的路由链接实现图片数据传输?我希望明确API路由的实现方式:是否需要编写第二个Get函数?以及如何正确返回图片的字节数据?
补充说明:我已准备2张分辨率为1920像素×1080像素的测试图片用于功能验证。

WebApplication2 项目
Startup.cs 服务注册代码
services.AddScoped<TextRepository.INumberRepository, TextRepository.NumberTextFileRepository>(); services.AddScoped<ImageRepository.IImageTransferRepository, ImageRepository.ImageTransferRepository>();
MyController.cs 控制器代码
using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using TextRepository; using ImageRepository; namespace WebApplication2.Controllers { [Route("api/[controller]")] [ApiController] public class MyController : ControllerBase { private readonly INumberRepository numberRepository; private readonly IImageTransferRepository imageRepository; public MyController(INumberRepository numberRepository, IImageTransferRepository imageTransferRepository) { this.numberRepository = numberRepository; this.imageRepository = imageTransferRepository; } [HttpGet] public ActionResult<List<int>> Get() { List<int> numbers = this.numberRepository.GetNumbers(); List<System.Drawing.Bitmap> foundImages = this.imageRepository.GetImages(); return Ok(numbers); } [HttpPost] public IActionResult Post([FromBody] DataTransferObject transferObject) { return Ok(transferObject.PassedString + $" {transferObject.Zahl}"); } } }
TextRepository 类库
TextFileParser.cs
using System; using System.Collections.Generic; using System.IO; namespace TextRepository { public class TextFileParser { public static List<int> ReadAllData(string path) { if (!File.Exists(path)) { throw new FileNotFoundException(path); } string[] allLines = System.IO.File.ReadAllLines(path); if (allLines == null) { throw new Exception("War null"); } if (allLines.Length < 1) { throw new Exception("Die Datei enthält 0 Zeilen."); } if (allLines.Length == 1) { return new List<int>(); } List<int> numbers = new List<int>(); for (int i = 1; i < allLines.Length; i++) { if (int.TryParse(allLines[i], out int result)) { numbers.Add(result); } else { Console.WriteLine($"In der Textdatei, in Zeile {i + 1}, ist eine inkorrekte Zahl aufgetreten."); continue; } } return numbers; } } }
NumberTextFileRepository
using System.Collections.Generic; namespace TextRepository { public class NumberTextFileRepository : INumberRepository { public List<int> GetNumbers() { System.IO.DirectoryInfo Root = new System.IO.DirectoryInfo(System.IO.Directory.GetCurrentDirectory()); return TextFileParser.ReadAllData(Root.Parent.FullName + "\\Textdatei.txt"); } } }
INumberRepository.cs
using System.Collections.Generic; namespace TextRepository { public interface INumberRepository { List<int> GetNumbers(); } }
ImageTransferRepository 类库
ImageTransferRepository.cs
using System; using System.Collections.Generic; namespace ImageRepository { public class ImageTransferRepository : IImageTransferRepository { public List<System.Drawing.Bitmap> GetImages() { return ImageTransfer.GetImagesFromFolder(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)); } } }
ImageTransfer.cs
using System.Collections.Generic; using System.IO; using System.Linq; namespace ImageRepository { public class ImageTransfer { public static List<System.Drawing.Bitmap> GetImagesFromFolder(string path) { if (!Directory.Exists(path)) { throw new FileNotFoundException(path); } List<FileInfo> fileInfos = new List<FileInfo>(); fileInfos.AddRange(new DirectoryInfo(path).EnumerateFiles().Where(f => IsValidFile(f))); fileInfos = fileInfos.OrderBy(x => x.CreationTime).ToList(); // The newest image should be at the top of the list. return (from FileInfo file in fileInfos select new System.Drawing.Bitmap(file.FullName)).ToList(); } private static bool IsValidFile(FileInfo File) { return File.FullName.ToLower().EndsWith(".bmp") ^ File.FullName.ToLower().EndsWith(".jpeg") ^ File.FullName.ToLower().EndsWith(".jpg") ^ File.FullName.ToLower().EndsWith(".png"); } } }
IImageTransferRepository.cs
using System.Collections.Generic; using System.Drawing; namespace ImageRepository { public interface IImageTransferRepository { List<Bitmap> GetImages(); } }
核心结论
- 原有
/api/My路由的数字列表功能完全不需要改动,只需要新增独立的Action处理图片请求即可,不会产生路由冲突 - 必须新增独立的Get方法处理图片逻辑,ASP.NET Core的特性路由支持在同一个HttpGet标记下通过路由模板区分不同接口
- 不要直接返回
Bitmap对象,需要转成字节数组通过File方法返回,设置正确的MIME类型后浏览器可直接渲染
现有代码修正点
- 删除原有
Get()方法中无意义的图片加载代码:List<System.Drawing.Bitmap> foundImages = this.imageRepository.GetImages();,这行代码会在每次请求数字列表时全量加载Pictures文件夹下的所有图片到内存,造成不必要的资源浪费。 - 修复
ImageTransfer.cs中文件后缀判断的逻辑错误:原代码用异或运算符^判断后缀,逻辑存在隐患,替换为或运算符||。 - 调整仓储层返回值:不要直接返回
Bitmap列表,Bitmap属于非托管资源,直接序列化会报错且存在内存泄漏风险,改为返回图片字节数组+对应MIME类型的结构。
具体实现步骤
第一步:调整ImageRepository层代码
修改IImageTransferRepository.cs,定义图片数据结构,更新接口约定:
using System.Collections.Generic; namespace ImageRepository { public class ImageData { public byte[] Content { get; set; } public string ContentType { get; set; } public string FileName { get; set; } } public interface IImageTransferRepository { List<ImageData> GetImages(); ImageData GetImage(int index); } }
修改ImageTransfer.cs,直接读取文件为字节数组,避免不必要的Bitmap资源加载(如果后续需要做图片裁剪、压缩等处理再使用Bitmap,处理完成后要立即调用Dispose()方法释放资源):
using System.Collections.Generic; using System.IO; using System.Linq; namespace ImageRepository { public class ImageTransfer { public static List<ImageData> GetImagesFromFolder(string path) { if (!Directory.Exists(path)) { throw new FileNotFoundException(path); } List<FileInfo> fileInfos = new List<FileInfo>(); fileInfos.AddRange(new DirectoryInfo(path).EnumerateFiles().Where(f => IsValidFile(f))); fileInfos = fileInfos.OrderBy(x => x.CreationTime).ToList(); List<ImageData> result = new List<ImageData>(); foreach (var file in fileInfos) { string ext = file.Extension.ToLower(); string contentType = ext switch { ".png" => "image/png", ".jpg" => "image/jpeg", ".jpeg" => "image/jpeg", ".bmp" => "image/bmp", _ => "application/octet-stream" }; result.Add(new ImageData { Content = File.ReadAllBytes(file.FullName), ContentType = contentType, FileName = file.Name }); } return result; } private static bool IsValidFile(FileInfo File) { string ext = File.Extension.ToLower(); return ext == ".bmp" || ext == ".jpeg" || ext == ".jpg" || ext == ".png"; } } }
修改ImageTransferRepository.cs实现新接口,增加简单缓存避免重复读取磁盘:
using System; using System.Collections.Generic; using System.Linq; namespace ImageRepository { public class ImageTransferRepository : IImageTransferRepository { private List<ImageData> _imagesCache; public List<ImageData> GetImages() { _imagesCache ??= ImageTransfer.GetImagesFromFolder(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures)); return _imagesCache; } public ImageData GetImage(int index) { var images = GetImages(); if (index < 0 || index >= images.Count) return null; return images[index]; } } }
第二步:在MyController中新增图片接口
原有数字列表和Post接口完全保留,新增两个Get接口处理图片请求,路由独立:
using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; using System.Linq; using TextRepository; using ImageRepository; namespace WebApplication2.Controllers { [Route("api/[controller]")] [ApiController] public class MyController : ControllerBase { private readonly INumberRepository numberRepository; private readonly IImageTransferRepository imageRepository; public MyController(INumberRepository numberRepository, IImageTransferRepository imageTransferRepository) { this.numberRepository = numberRepository; this.imageRepository = imageTransferRepository; } // 原有接口完全不动,GET /api/My 依旧返回数字列表 [HttpGet] public ActionResult<List<int>> Get() { List<int> numbers = this.numberRepository.GetNumbers(); return Ok(numbers); } // 新增:获取图片列表接口,路由为 GET /api/My/images [HttpGet("images")] public ActionResult<List<object>> GetImageList() { var images = this.imageRepository.GetImages(); return Ok(images.Select((img, index) => new { Index = index, img.FileName }).ToList()); } // 新增:获取单张图片字节接口,路由为 GET /api/My/images/{id} [HttpGet("images/{id:int}")] public IActionResult GetImageById(int id) { var targetImg = this.imageRepository.GetImage(id); if (targetImg == null) return NotFound(); // 通过File方法返回字节数据,设置正确的MIME类型,浏览器可直接渲染 return File(targetImg.Content, targetImg.ContentType); } [HttpPost] public IActionResult Post([FromBody] DataTransferObject transferObject) { return Ok(transferObject.PassedString + $" {transferObject.Zahl}"); } } }
验证方式
- 访问
https://localhost:44355/api/My:和原有逻辑一致,返回0-99的数字列表 - 访问
https://localhost:44355/api/My/images:返回Pictures文件夹下所有图片的索引和文件名 - 访问
https://localhost:44355/api/My/images/0:直接在浏览器中渲染第一张测试图片,/api/My/images/1对应第二张1920*1080测试图
注意事项
- 不建议一次性返回所有图片的字节数据,单张按需请求可以减少带宽占用和加载时间
System.Drawing.Common仅支持Windows系统,如果后续需要部署到Linux环境,需要替换为SixLabors.ImageSharp等跨平台图像处理库- 如果图片文件体积较大,可以调整ASP.NET Core的响应缓冲配置,避免大文件传输时的内存占用过高问题
内容的提问来源于stack exchange,提问作者Daniel

