跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C#

基于 ASP.NET WebAPI 构建轻量级文件服务

综述由AI生成基于 ASP.NET WebAPI 构建轻量级文件服务,涵盖设计思路、核心代码及前后端集成示例。方案采用本地存储配合 GUID 命名,实现上传、获取路径、删除三大功能。后端封装 HTTP 工具类解耦业务逻辑,前端通过 Vue2+ElementUI 实现交互。重点解决了临时文件清理、大文件限制及事务一致性等问题,适合中小型系统快速落地。

星辰大海发布于 2026/4/7更新于 2026/5/2317 浏览
基于 ASP.NET WebAPI 构建轻量级文件服务

基于 ASP.NET WebAPI 构建轻量级文件服务

很多系统在设计初期,往往纠结于是否要引入 MinIO 或 OSS 等对象存储方案。对于大部分内部管理系统而言,复杂的分布式架构可能显得过于沉重。本文将分享如何基于 ASP.NET WebAPI(.NET Framework 4.5)实现一个简单、可控的文件服务。

设计思路

在动手写代码前,我们先明确几个核心原则:

  1. 存储位置:所有文件统一放在网站根目录下的 Uploads 文件夹中。虽然考虑过挂载其他磁盘,但为了通过 URL 直接预览文件,放在网站目录下最为方便。
  2. 命名规范:采用'文件 ID.扩展名'的格式。访问时通过文件 ID 查找,避免文件名冲突和中文乱码问题。
  3. 接口设计:主要包含三个核心接口——上传、获取路径、删除。
  4. 封装调用:将文件服务的 HTTP 请求封装为工具类,降低业务层耦合度。

注意:单个文件限制最大 100MB,具体阈值可根据项目需求调整。同时需关注 WebApi 项目本身的报文大小限制配置。

核心代码实现

1. 基础控制器与配置

为了方便统一返回格式,我们定义一个 BaseController,并允许在配置文件中指定支持的扩展名。

Web.config 配置

<appSettings>
    <add key="AllowedExtensions" value="jpg,jpeg,png,gif,pdf,txt,doc,docx,xls,xlsx,zip,rar"/>
</appSettings>

BaseController 基类

public abstract class BaseController : ApiController
{
    protected IHttpActionResult Success(string message = "", object data = null)
    {
        return Json(new { Code = 0, Message = message, Data = data });
    }

    protected IHttpActionResult Fail(string message = "", object data = null)
    {
        return Json(new { Code = 1, Message = message, Data = data });
    }
}

2. 文件服务控制器

FileController 负责处理具体的文件逻辑。这里的关键点在于使用 Guid 生成唯一 ID,并在上传失败时清理临时文件,防止磁盘占用。

public class FileController : BaseController
{
    private const string folder = "~/Uploads";
    private readonly string uploadFolder;
    private readonly long maxFileSize = 100 * 1024 * 1024; // 100MB
    private readonly string[] allowedExtensions;

    public FileController()
    {
        allowedExtensions = ConfigurationManager.AppSettings["AllowedExtensions"]
            .Split(',')
            .Select(s => "." + s)
            .ToArray();
        
        uploadFolder = HttpContext.Current.Server.MapPath(folder);
        if (!Directory.Exists(uploadFolder))
            Directory.CreateDirectory(uploadFolder);
    }

    [HttpPost]
    public async Task<IHttpActionResult> UploadFile()
    {
        if (!Request.Content.IsMimeMultipartContent())
            return Fail("请选择文件上传");

        var provider = new MultipartFormDataStreamProvider(Path.GetTempPath());
        await Request.Content.ReadAsMultipartAsync(provider);

        if (!provider.FileData.Any())
            return Fail("没有上传文件");

        var resultFiles = new List<object>();
        var savedFiles = new List<string>();
        var baseUrl = $"{Request.RequestUri.Scheme}://{Request.RequestUri.Authority}";
        var relativePath = folder.TrimStart('~');

        try
        {
            foreach (var fileData in provider.FileData)
            {
                var tempPath = fileData.LocalFileName;
                var originalName = fileData.Headers.ContentDisposition.FileName.Trim('"');
                var extension = Path.GetExtension(originalName).ToLower();

                if (!allowedExtensions.Contains(extension))
                    throw new Exception($"不支持的文件类型:{extension}");

                var fileInfo = new FileInfo(tempPath);
                if (fileInfo.Length > maxFileSize)
                    throw new Exception($"文件大小超过 100MB: {originalName}");

                var fileId = Guid.NewGuid().ToString();
                var newFileName = fileId + extension;
                var savePath = Path.Combine(uploadFolder, newFileName);

                File.Move(tempPath, savePath);
                savedFiles.Add(savePath);

                var fileUrl = $"{baseUrl}{relativePath}/{newFileName}";
                resultFiles.Add(new { FileId = fileId, FilePath = fileUrl });
            }
            return Success(data: resultFiles);
        }
        catch (Exception ex)
        {
            // 异常回滚:删除已保存的文件
            foreach (var savedFile in savedFiles)
            {
                try { File.Delete(savedFile); }
                catch { }
            }
            // 清理临时文件
            foreach (var fileData in provider.FileData)
            {
                try { File.Delete(fileData.LocalFileName); }
                catch { }
            }
            return Fail($"文件上传失败:{ex.Message}");
        }
    }

    [HttpPost]
    public IHttpActionResult GetFilePaths(string[] fileIds)
    {
        var baseUrl = $"{Request.RequestUri.Scheme}://{Request.RequestUri.Authority}";
        var relativePath = folder.TrimStart('~');
        var resultFiles = new List<object>();

        foreach (string fileId in fileIds)
        {
            var filePath = Directory.GetFiles(uploadFolder, fileId + ".*").FirstOrDefault();
            if (filePath == null)
                return Fail($"文件不存在{fileId}");

            var fileName = Path.GetFileName(filePath);
            resultFiles.Add(new { FileId = fileId, FilePath = $"{baseUrl}{relativePath}/{fileName}" });
        }
        return Success(data: resultFiles);
    }

    [HttpPost]
    public IHttpActionResult DeleteFiles(string[] fileIds)
    {
        if (fileIds == null || fileIds.Length == 0)
            return Fail("请提供要删除的文件 ID");

        using (var scope = new TransactionScope())
        {
            foreach (var fileId in fileIds)
            {
                var filePath = Directory.GetFiles(uploadFolder, fileId + ".*").SingleOrDefault();
                if (filePath == null)
                    return Fail($"文件不存在:{fileId}");
                File.Delete(filePath);
            }
            scope.Complete();
        }
        return Success();
    }
}

3. 客户端调用工具类

为了避免业务代码中重复编写 HTTP 请求逻辑,我们封装一个 FileUploader 类。这样前端只需调用业务接口,无需直接感知文件服务的存在。

public class FileUploader
{
    private string fileServiceUrl;
    private HttpClient http;

    public FileUploader(string fileServiceUrl)
    {
        this.fileServiceUrl = fileServiceUrl;
        http = new HttpClient();
    }

    public async Task<dynamic> Upload(MultipartFileData[] files)
    {
        using (var form = new MultipartFormDataContent())
        {
            foreach (var file in files)
            {
                var fileBytes = File.ReadAllBytes(file.LocalFileName);
                var content = new ByteArrayContent(fileBytes);
                form.Add(content, "file", file.Headers.ContentDisposition.FileName.Trim('"'));
            }
            var url = fileServiceUrl + "/api/File/UploadFile";
            var response = await http.PostAsync(url, form);
            var json = await response.Content.ReadAsStringAsync();
            return Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);
        }
    }

    public async Task<dynamic> GetFilePaths(string[] fileIds)
    {
        var url = fileServiceUrl + "/api/File/GetFilePaths";
        var content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(fileIds), Encoding.UTF8, "application/json");
        var response = await http.PostAsync(url, content);
        var json = await response.Content.ReadAsStringAsync();
        return Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);
    }

    public async Task<dynamic> DeleteFiles(string[] fileIds)
    {
        var url = fileServiceUrl + "/api/File/DeleteFiles";
        var content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(fileIds), Encoding.UTF8, "application/json");
        var response = await http.PostAsync(url, content);
        var json = await response.Content.ReadAsStringAsync();
        return Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(json);
    }
}

服务调用示例

在实际项目中,建议采用后端代理模式:前端不直接调用文件服务,而是通过业务 API 中转。这样能更好地控制权限和日志。

1. 后端业务接口集成

以下是一个用户管理控制器的示例,展示了如何在创建用户时上传图片,并在查询时解析头像路径。

public class MyTestController : ApiController
{
    private FileUploader fileUploader;

    public MyTestController()
    {
        fileUploader = new FileUploader(ConfigurationManager.AppSettings["FileServiceUrl"]);
    }

    [HttpPost]
    public async Task<IHttpActionResult> UploadTest()
    {
        var provider = new MultipartFormDataStreamProvider(Path.GetTempPath());
        await Request.Content.ReadAsMultipartAsync(provider);

        if (!provider.FileData.Any())
            return BadRequest("没有上传文件");

        try
        {
            var result = await fileUploader.Upload(provider.FileData.ToArray());
            return Ok(result);
        }
        catch (Exception ex)
        {
            return BadRequest(ex.Message);
        }
        finally
        {
            // 务必清理临时文件,防止磁盘爆满
            foreach (var file in provider.FileData)
            {
                if (!File.Exists(file.LocalFileName)) continue;
                File.Delete(file.LocalFileName);
            }
        }
    }

    [HttpGet]
    public async Task<IHttpActionResult> QueryUserDatas()
    {
        user[] users;
        using (var dbContext = new jonas_testEntities())
            users = dbContext.user.ToArray();

        var result = await fileUploader.GetFilePaths(users.Select(u => u.IconImageId).ToArray());
        if (result.Code != 0)
            return Json(result);

        var fileInfos = ((IEnumerable<dynamic>)result.Data).Select(d => new { d.FileId, d.FilePath }).ToArray();
        List<QueryUserDatasResultItem> list = new List<QueryUserDatasResultItem>();

        foreach (var user in users)
        {
            QueryUserDatasResultItem model = new QueryUserDatasResultItem
            {
                Account = user.Account,
                RealName = user.RealName
            };
            model.IconImageUrl = fileInfos.Single(s => s.FileId == user.IconImageId).FilePath;
            list.Add(model);
        }
        return Json(list);
    }

    [HttpPost]
    public async Task<IHttpActionResult> AddUser()
    {
        if (!Request.Content.IsMimeMultipartContent())
            return Json(new { Code = 1, Message = "请求格式错误" });

        var provider = new MultipartFormDataStreamProvider(Path.GetTempPath());
        await Request.Content.ReadAsMultipartAsync(provider);

        try
        {
            var formData = provider.FormData;
            var user = new user
            {
                Account = formData["Account"],
                RealName = formData["RealName"]
            };

            if (provider.FileData.Count > 0)
            {
                var fileData = provider.FileData[0];
                var result = await fileUploader.Upload(new[] { fileData });
                if (result.Code != 0)
                    return Json(result);
                user.IconImageId = result.Data[0].FileId;
            }

            using (var dbContext = new jonas_testEntities())
            {
                if (dbContext.user.Any(u => u.Account == user.Account))
                    return Json(new { Code = 1, Message = "账号已经存在" });
                dbContext.user.Add(user);
                await dbContext.SaveChangesAsync();
            }
            return Json(new { Code = 0 });
        }
        catch (Exception ex)
        {
            return Json(new { Code = 1, Message = ex.Message });
        }
        finally
        {
            foreach (var fileData in provider.FileData)
            {
                try
                {
                    if (File.Exists(fileData.LocalFileName))
                        File.Delete(fileData.LocalFileName);
                }
                catch { }
            }
        }
    }

    [HttpPost]
    public async Task<IHttpActionResult> DeleteUser(string account)
    {
        user user = null;
        using (var dbContext = new jonas_testEntities())
        {
            user = dbContext.user.SingleOrDefault(u => u.Account == account);
            if (user == null)
                return Json(new { Code = 1, Message = "用户不存在" });
            dbContext.user.Remove(user);
            await dbContext.SaveChangesAsync();
        }
        await fileUploader.DeleteFiles(new string[] { user.IconImageId });
        return Json(new { Code = 0 });
    }
}

2. 前端交互示例(Vue2 + ElementUI)

前端部分主要涉及表单提交和图片预览。这里使用了 Axios 发送 multipart/form-data 请求。

<template>
  <div id="app">
    <el-table :data="tableDatas" style="width: 800px;" stripe border>
      <el-table-column label="账号" prop="Account" width="150"></el-table-column>
      <el-table-column label="真实姓名" prop="RealName" width="150"></el-table-column>
      <el-table-column label="头像" width="100">
        <template slot-scope="scope">
          <span style="color: #409EFF; cursor: pointer;" @click="previewIconHandler(scope.row.IconImageUrl)">预览</span>
        </template>
      </el-table-column>
      <el-table-column width="150">
        <template #header>
          <el-button type="primary" size="medium" @click="dialogAdd.show = true">新增</el-button>
        </template>
        <template slot-scope="scope">
          <el-button type="danger" size="small" @click="removeUserHandler(scope.row.Account)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 预览弹窗 -->
    <el-dialog title="预览头像" width="500px" :visible.sync="dialogPreviewIcon.show" append-to-body>
      <div style="width: 100%; height: 500px;">
        <img :src="dialogPreviewIcon.src" style="width: 100%; height: 100%;">
      </div>
    </el-dialog>

    <!-- 新增用户弹窗 -->
    <el-dialog title="新增用户" width="800px" :visible.sync="dialogAdd.show" append-to-body>
      <el-form :model="dialogAdd.userInfo" label-width="150px" class="demo-ruleForm">
        <el-form-item label="账号">
          <el-input v-model="dialogAdd.userInfo.Account"></el-input>
        </el-form-item>
        <el-form-item label="真实姓名">
          <el-input v-model="dialogAdd.userInfo.RealName"></el-input>
        </el-form-item>
        <el-form-item label="头像">
          <el-upload class="upload-demo" ref="upload" action=""
            :on-remove="removeOrChangeHandler" :file-list="dialogAdd.files"
            auto-upload="false" :on-change="removeOrChangeHandler">
            <el-button slot="trigger" size="small" type="primary">选取文件</el-button>
          </el-upload>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="submitFormHandler">提交</el-button>
        </el-form-item>
      </el-form>
    </el-dialog>
  </div>
</template>

<script>
import axios from 'axios';
const baseUrl = 'http://localhost:4623'

export default {
  data() {
    return {
      tableDatas: [],
      dialogPreviewIcon: { show: false, src: '' },
      dialogAdd: {
        show: false,
        userInfo: { Account: '', RealName: '' },
        files: []
      }
    };
  },
  methods: {
    async queryDatas() {
      const { data: res } = await axios.get(baseUrl + '/api/MyTest/QueryUserDatas');
      this.tableDatas = res;
    },
    previewIconHandler(IconImageUrl) {
      this.dialogPreviewIcon.src = IconImageUrl;
      this.dialogPreviewIcon.show = true;
    },
    async submitFormHandler() {
      console.log(this.dialogAdd);
      if (this.dialogAdd.userInfo.Account === '' || this.dialogAdd.userInfo.RealName === '' || this.dialogAdd.files.length === 0)
        return alert('请填写完整信息');

      let formdata = new FormData();
      formdata.append('file', this.dialogAdd.files[0].raw);
      formdata.append('Account', this.dialogAdd.userInfo.Account);
      formdata.append('RealName', this.dialogAdd.userInfo.RealName);

      const { data: res } = await axios.post(baseUrl + '/api/MyTest/AddUser', formdata, {
        headers: { 'Content-Type': 'multipart/form-data' }
      });

      if (res.Code != 0) return alert(res.Message);
      this.dialogAdd.show = false;
      this.queryDatas();
    },
    async removeUserHandler(account) {
      const { data: res } = await axios.post(baseUrl + '/api/MyTest/DeleteUser?account=' + account);
      if (res.Code != 0) return alert(res.Message);
      this.queryDatas();
    },
    removeOrChangeHandler(file, files) {
      this.dialogAdd.files = files;
    },
  },
  created() {
    this.queryDatas();
  }
}
</script>

3. 效果说明

完成上述配置后,系统即可支持用户头像的上传、展示及删除。实际运行中,请注意以下几点:

  • 临时文件清理:代码中多次使用了 finally 块来删除 Path.GetTempPath() 中的临时文件,这是为了防止服务器磁盘被缓存文件占满。
  • 事务一致性:删除用户时,先删数据库记录,再删物理文件,确保数据与文件状态一致。
  • 安全性:生产环境建议增加身份验证(如 JWT),并校验文件内容而非仅依赖扩展名。

整体流程跑通后,前端界面会显示用户列表,点击新增可上传头像,点击预览可查看大图,删除操作则同步移除数据库记录和本地文件。

目录

  1. 基于 ASP.NET WebAPI 构建轻量级文件服务
  2. 设计思路
  3. 核心代码实现
  4. 1. 基础控制器与配置
  5. 2. 文件服务控制器
  6. 3. 客户端调用工具类
  7. 服务调用示例
  8. 1. 后端业务接口集成
  9. 2. 前端交互示例(Vue2 + ElementUI)
  10. 3. 效果说明
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Android 3D 模型查看器:STL OBJ PLY 格式支持与渲染实现
  • Stable Diffusion AI 绘画入门与实战指南
  • Web 前端基础:HTML 核心语法和常用标签
  • 百度为何不做 Sora:解析其多模态技术路线与 iRAG 策略
  • OpenCode 使用 GitHub Copilot 计费异常分析与解决方案
  • 图灵奖得主对人工智能发展的深远影响
  • 基于 Z-Image 的瑜伽女孩 AI 绘画模型部署与使用指南
  • HDFS 常用命令与 Java API 编程实践
  • 基于魔搭社区免费 GPU 使用 LLaMaFactory 微调大模型
  • Go Web 开发必备核心理论
  • 大模型学习资料汇总与学习路线指南
  • 大模型面试常见问题与核心解析
  • 基于 Streamlit 与腾讯云混元快速搭建 LLM 聊天应用
  • 三大智能家居平台深度评测:如何选择适合你的方案
  • Python 异步编程与协程实战指南
  • LightRAG 架构深度解析:基于图的检索增强生成系统
  • Java 调用 CPLEX 优化器:安装配置与示例
  • 大语言模型(LLM)基础原理与应用
  • 互联网产品经理转型大模型 AI 产品经理实战指南
  • C语言排序算法:插入排序与希尔排序

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online