- 后端: ASP.NET Core Web API,它轻量、高性能且跨平台。
- 前端: WinForms +
HttpClient,这是 .NET 中进行 HTTP 通信的标准方式。
第一步:创建服务器端 (ASP.NET Core Web API)
服务器端将提供一个 API 端点,用于接收 HTTP POST 请求,并处理上传的文件。

创建项目
打开 Visual Studio,创建一个新的项目,选择 "ASP.NET Core Web API" 模板。
配置服务和中间件
在 Program.cs 文件中,我们需要配置文件上传的大小限制,默认限制很小(约 28MB),我们需要根据需求进行调整。
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// 1. 添加服务到容器。
// 允许将大文件上传到最多 1 GB
builder.Services.Configure<FormOptions>(options =>
{
options.MultipartBodyLengthLimit = 1073741824; // 1 GB
options.MultipartHeadersLengthLimit = 1024 * 1024; // 1 MB for headers
});
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// 2. 配置 HTTP 请求管道。
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
创建上传控制器
创建一个新的控制器,UploadController.cs,并添加一个用于处理文件上传的 Action。
// UploadController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using System.IO;
using System.Threading.Tasks;
using System;
using Microsoft.AspNetCore.Hosting; // 需要注入 IWebHostEnvironment
namespace FileUploadServer.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class UploadController : ControllerBase
{
private readonly IWebHostEnvironment _hostingEnvironment;
// 通过构造函数注入 IWebHostEnvironment
public UploadController(IWebHostEnvironment hostingEnvironment)
{
_hostingEnvironment = hostingEnvironment;
}
[HttpPost("uploadfile")]
public async Task<IActionResult> UploadFile(IFormFile file)
{
// 检查文件是否为空
if (file == null || file.Length == 0)
{
return BadRequest("没有选择文件或文件为空。");
}
// 定义服务器上保存文件的路径(在 wwwroot/uploads 文件夹下)
var uploadsFolderPath = Path.Combine(_hostingEnvironment.WebRootPath, "uploads");
// 如果文件夹不存在,则创建它
if (!Directory.Exists(uploadsFolderPath))
{
Directory.CreateDirectory(uploadsFolderPath);
}
// 创建一个唯一的文件名,防止文件名冲突
var uniqueFileName = Guid.NewGuid().ToString() + "_" + file.FileName;
var filePath = Path.Combine(uploadsFolderPath, uniqueFileName);
// 将文件保存到服务器
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
// 返回成功响应,包含文件的访问路径
return Ok(new { filePath = $"/uploads/{uniqueFileName}", message = "文件上传成功!" });
}
// 如果你需要一次上传多个文件
[HttpPost("uploadmultiple")]
public async Task<IActionResult> UploadMultipleFiles(List<IFormFile> files)
{
if (files == null || files.Count == 0)
{
return BadRequest("没有选择文件。");
}
var uploadedFiles = new List<string>();
var uploadsFolderPath = Path.Combine(_hostingEnvironment.WebRootPath, "uploads");
if (!Directory.Exists(uploadsFolderPath))
{
Directory.CreateDirectory(uploadsFolderPath);
}
foreach (var file in files)
{
if (file.Length > 0)
{
var uniqueFileName = Guid.NewGuid().ToString() + "_" + file.FileName;
var filePath = Path.Combine(uploadsFolderPath, uniqueFileName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
uploadedFiles.Add($"/uploads/{uniqueFileName}");
}
}
return Ok(new { filePaths = uploadedFiles, message = $"成功上传 {uploadedFiles.Count} 个文件。" });
}
}
}
说明:

[HttpPost("uploadfile")]定义了一个 POST 请求的路由。IFormFile file是 ASP.NET Core 用于接收上传文件的参数。IWebHostEnvironment用于获取应用程序的根路径,方便我们找到wwwroot文件夹。- 我们将文件保存在
wwwroot/uploads目录下,这样文件就可以通过 URL 直接访问(http://localhost:5000/uploads/your_file_name.jpg)。
第二步:创建客户端 (WinForms 应用程序)
现在我们来创建 WinForms 客户端,它将选择文件并发送到我们刚刚创建的 API。
创建项目
打开 Visual Studio,创建一个新的 "Windows Forms App (.NET Framework)" 或 "Windows Forms App" (.NET 6/7/8) 项目。
设计窗体
打开 Form1.cs [Design],从工具箱中拖拽以下控件到窗体上:
Button(命名为btnSelectFile)TextBox(命名为txtFilePath)Button(命名为btnUpload)ProgressBar(命名为progressBarUpload)Label(用于显示状态信息)
布局可以参考下图:

编写上传逻辑
双击 btnSelectFile 和 btnUpload 按钮,在 Form1.cs 中编写代码。
// Form1.cs
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFormsFileUploader
{
public partial class Form1 : Form
{
// 创建一个静态的 HttpClient 实例,避免重复创建,提高性能
private static readonly HttpClient client = new HttpClient();
public Form1()
{
InitializeComponent();
// 设置进度条初始状态
progressBarUpload.Visible = false;
}
private void btnSelectFile_Click(object sender, EventArgs e)
{
using (OpenFileDialog openFileDialog = new OpenFileDialog())
{
openFileDialog.InitialDirectory = "c:\\";
openFileDialog.Filter = "所有文件 (*.*)|*.*|文本文件 (*.txt)|*.txt|图片文件 (*.jpg;*.png)|*.jpg;*.png";
openFileDialog.FilterIndex = 1;
openFileDialog.RestoreDirectory = true;
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
// 获取选中的文件路径
txtFilePath.Text = openFileDialog.FileName;
}
}
}
private async void btnUpload_Click(object sender, EventArgs e)
{
// 检查是否选择了文件
if (string.IsNullOrEmpty(txtFilePath.Text))
{
MessageBox.Show("请先选择一个文件!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 禁用上传按钮,防止重复点击
btnUpload.Enabled = false;
progressBarUpload.Visible = true;
progressBarUpload.Value = 0;
lblStatus.Text = "上传中...";
try
{
// 创建一个 MultipartFormDataContent 对象,用于构建包含文件的请求
using (var form = new MultipartFormDataContent())
{
// 读取文件流
byte[] fileBytes = File.ReadAllBytes(txtFilePath.Text);
// 将文件流添加到请求中
// "file" 必须与服务器端 Action 中的 IFormFile 参数名 "file" 匹配
form.Add(new ByteArrayContent(fileBytes), "file", Path.GetFileName(txtFilePath.Text));
// 服务器 API 的 URL (请替换为你的实际地址)
// 使用 localhost 时,如果你的 WinForms 和 Web API 在同一台机器上运行,
// Web API 运行在 https 上,WinForms 也需要使用 https。
// 为了方便开发,可以暂时关闭 Web API 的 HTTPS。
string apiUrl = "https://localhost:7123/api/upload/uploadfile"; // 注意端口号
// 发送 POST 请求并获取响应
HttpResponseMessage response = await client.PostAsync(apiUrl, form);
// 检查响应是否成功
response.EnsureSuccessStatusCode();
// 读取响应内容
string result = await response.Content.ReadAsStringAsync();
// 显示成功消息
MessageBox.Show("文件上传成功!\n服务器响应: " + result, "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
lblStatus.Text = "上传完成";
}
}
catch (HttpRequestException ex)
{
MessageBox.Show($"上传失败: {ex.Message}\n请检查服务器是否正在运行,以及URL是否正确。", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
lblStatus.Text = "上传失败";
}
catch (Exception ex)
{
MessageBox.Show($"发生未知错误: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
lblStatus.Text = "上传失败";
}
finally
{
// 无论成功或失败,都重新启用上传按钮
btnUpload.Enabled = true;
progressBarUpload.Visible = false;
}
}
}
}
显示上传进度 (进阶功能)
上面的代码没有显示上传进度。HttpClient 本身不直接提供进度事件,但我们可以通过计算已发送的字节数来实现,这需要一些额外的代码。
我们需要一个自定义的 ByteArrayContent 来跟踪上传进度。
// 在 Form1.cs 文件中添加这个类
public class ProgressableByteArrayContent : ByteArrayContent
{
private readonly Action<int> _progress;
public ProgressableByteArrayContent(byte[] bytes, Action<int> progress) : base(bytes)
{
_progress = progress;
}
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
// 计算总字节数
var length = this.Headers.ContentLength ?? 0;
// 使用流包装器来跟踪已写入的字节数
var wrappedStream = new ProgressStream(stream, (bytesWritten) =>
{
// 计算进度百分比 (0-100)
var progressPercentage = (int)((double)bytesWritten / length * 100);
_progress(progressPercentage);
});
await base.SerializeToStreamAsync(wrappedStream, context);
}
}
// 再添加一个 ProgressStream 类
public class ProgressStream : Stream
{
private readonly Stream _stream;
private readonly Action<long> _progress;
public ProgressStream(Stream stream, Action<long> progress)
{
_stream = stream;
_progress = progress;
}
public override bool CanRead => _stream.CanRead;
public override bool CanSeek => _stream.CanSeek;
public override bool CanWrite => _stream.CanWrite;
public override long Length => _stream.Length;
public override long Position { get => _stream.Position; set => _stream.Position = value; }
public override void Flush() => _stream.Flush();
public override int Read(byte[] buffer, int offset, int count) => _stream.Read(buffer, offset, count);
public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin);
public override void SetLength(long value) => _stream.SetLength(value);
public override void Write(byte[] buffer, int offset, int count)
{
_stream.Write(buffer, offset, count);
_progress?.Invoke(_stream.Position);
}
protected override void Dispose(bool disposing)
{
_stream.Dispose();
base.Dispose(disposing);
}
}
然后修改 btnUpload_Click 方法中的 form.Add 部分:
// 替换原来的 form.Add 这一行
// form.Add(new ByteArrayContent(fileBytes), "file", Path.GetFileName(txtFilePath.Text));
// 使用新的 ProgressableByteArrayContent
form.Add(new ProgressableByteArrayContent(fileBytes, (progress) =>
{
// 在 UI 线程上更新进度条
this.Invoke((MethodInvoker)delegate
{
progressBarUpload.Value = progress;
})
}), "file", Path.GetFileName(txtFilePath.Text));
注意: this.Invoke 是必需的,因为 HttpClient 的回调是在后台线程中执行的,而 WinForms 控件(如 ProgressBar)只能在主 UI 线程上访问。
第三步:运行和测试
- 运行服务器: 按
F5运行你的 ASP.NET Core Web API 项目,它会启动并在浏览器中打开 Swagger UI。 - 运行客户端: 再按
F5运行你的 WinForms 项目。 - 测试:
- 在 WinForms 窗体中点击 "选择文件",选择一个本地文件。
- 点击 "上传文件" 按钮。
- 你会看到进度条在移动,上传完成后会弹出成功提示。
- 回到 Swagger UI 页面,你可以直接测试 API,上传一个文件,然后去服务器项目的
wwwroot/uploads文件夹中查看文件是否已成功保存。
总结与最佳实践
- 异常处理: 客户端和服务器端都要有完善的异常处理。
- 文件大小限制: 服务器端一定要设置
FormOptions的MultipartBodyLengthLimit,防止因上传过大文件而导致服务崩溃。 - 安全性: 在生产环境中,要对上传的文件类型、文件名进行严格的验证和清理,防止恶意文件上传(如病毒、脚本等)。
HttpClient的生命周期: 最好在整个应用程序生命周期内只创建一个HttpClient实例(如static readonly),而不是每次上传都创建一个新的,这样可以避免端口耗尽问题。- 取消操作: 可以添加一个
CancellationToken来允许用户取消正在进行的上传操作。 - 异步编程: 客户端的上传操作是
I/O密集型的,使用async/await可以避免阻塞 UI 线程,保持应用程序的响应性。
