ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C# WebApi上位机开发实战:文件下载、Vue对接与工业设备集成

C# WebApi上位机开发实战:文件下载、Vue对接与工业设备集成 简介这份C# WebAPI示例代码面向.NET初学者以及需要快速搭建RESTful接口的开发者基于ASP.NET框架演示了从零构建HTTP服务的完整流程。项目围绕SQL Server数据库交互展开包含模型类、ApiController控制器、路由配置与数据访问层等关键模块可通过Entity Framework或ADO.NET实现数据的增删改查操作并清楚展示GET、POST、PUT、DELETE等动作如何映射到具体方法返回JSON或XML格式的响应。默认路由采用/{controller}/{id}的规则便于理解URL与资源的对应关系。资源包为zip压缩格式文件总数显示为0整体大小约172.56MB已有291人学习下载可作为课程设计、项目起步或个人学习的参考骨架。示例未内置用户认证与缓存机制方便开发者聚焦业务逻辑在此基础上可继续扩展OAuth、JWT身份验证与Redis缓存逐步打造安全高效的生产级API服务。 做上位机开发的朋友应该都有过这种经历程序跑得好好的客户突然提一句能不能给MES系统开个接口或者搞个网页看板看看设备状态。我早期遇到这类需求时第一反应是写TCP协议自己定报文格式结果前端联调花了一周还没搞定。后来老老实实改用C# WebApi做数据出口情况一下子清爽了。这篇文章就从一个覆盖真实需求的C# WebApi Demo项目说起讲讲怎么把文件下载、Vue前端对接、扫码枪、机器视觉这些场景串起来顺便把我在VS2022建项目、发IIS、调CORS过程中踩过的坑一次说清楚。现在很多教程给的WebApi示例就是Hello World级别的返回值根本解决不了实际问题。我这篇Demo的设计目标是一个上位机程序同时对外提供设备状态查询、报告文件下载、扫码记录写入三个核心能力Vue前端和MES系统都能直接对接。下面按照我在实际项目里的落地顺序来拆。1. 为什么我建议上位机项目用WebApi做数据出口写设备通讯的工程师对TCP、UDP、串口这类底层协议都不陌生Modbus、S7、Fins这些工业协议也玩得转。但这类协议的痛点在于通讯双方必须提前约定报文格式、字节顺序、数据长度一旦对面换了人或者换了系统联调就是一场灾难。WebApi的解题思路完全不同。它基于HTTP走JSON格式字段名就是字段名含义自解释。不管对面是Vue前端、Java写的MES、Python跑的数据分析还是手机App只要会发HTTP请求就能对接。我做过的项目里甚至有客户用Excel VBA直接调接口拉产线数据零成本搞定。具体到技术选型ASP.NET Core WebApi对比老式的WCF和WebService有压倒性的优势跨平台、启动快、内存占用低还能以独立线程的方式宿主在上位机程序内部同一个进程里既跑着WPF设备界面又开着Kestrel服务对外提供HTTP接口互不干扰。这意味着你不需要额外部署一套IIS工控机上只要跑着上位机程序外部系统就能通过HTTP拿到数据。从Demo设计的角度来想我会在项目里保留三个典型功能点设备状态查询接口供Vue看板轮询实时数据。报告文件下载接口解决怎么把Excel/PDF从后端安全地交给前端这个高频问题。数据写入接口承接扫码枪、相机检测结果的回传。这三个功能基本覆盖了上位机项目80%的对外交互需求。把这三个接口写好遇到其他需求基本就是复制粘贴再改改逻辑的体力活。2. VS2022创建WebApi Demo项目的正确姿势与常见坑2.1 项目模板选项按这个思路选就对了VS2022里新建项目搜索Web API选C#标签下的ASP.NET Core Web API。注意别选成ASP.NET Core Web App那是返回页面的MVC项目不是纯接口。框架版本如果客户工控机是Win10以上直接用.NET 8如果是老旧的Win7工控机老老实实用.NET 6或者.NET Core 3.1不然目标机器跑不起运行时。创建向导里有几个选项值得展开说说。Authentication类型选无工业内网场景不需要微软账户体系后续要鉴权自己加JWT或者简单Token就行。HTTPS配置默认是勾上的开发机没感觉但部署到局域网内网后自签名证书会引发一堆证书信任问题。我一般建完项目直接改launchSettings.json把http配置设成启动项https注释掉省得给自己添堵。Docker支持和最小API除非团队明确要走容器化否则不勾。2.2 Controller、Service、Helper的目录划分很多人写Demo喜欢把业务逻辑全塞进Controller接口少的时候没问题接口一多代码就成了一锅粥。我在这个Demo里按三层结构组织Controller层只负责接收HTTP请求、调用服务、返回统一格式结果。Service层放业务逻辑比如扫码记录的校验、报告文件路径的拼装。Helper/Infrastructure层处理硬件通讯比如串口扫码枪的封装、相机SDK的调用。有人觉得项目小没必要分层我吃过亏才悟出这个道理上位机项目的复杂度是慢慢涨上来的刚开始只有一个取状态接口过两个月加扫码枪再过半年加视觉检测如果Controller里堆满了串口操作代码光整理就够你喝一壶的。2.3 统一响应模型让前端少写一半判断逻辑接口联调时我最头疼的就是返回值格式乱七八糟。这个接口返回{data: ...}那个接口直接返回数组Vue端每对接一个接口就得写一套解析很浪费时间。从第一个Demo接口开始就应该统一响应模型这是成本最低收益最高的设计。public class ApiResultT { public int Code { get; set; } public string Message { get; set; } string.Empty; public T? Data { get; set; } public static ApiResultT Success(T data) new() { Code 0, Message ok, Data data }; public static ApiResultT Fail(string message, int code 1) new() { Code code, Message message }; }Controller里这样用[HttpGet(status)] public IActionResult GetDeviceStatus() { var status _deviceService.GetCurrentStatus(); return Ok(ApiResultobject.Success(status)); }前端拿到的一律是{ code, message, data }Vue的axios拦截器统一判断code非0就弹message整个前端只写一套错误处理就行。这个习惯养成之后后面不管接多少个新接口前端代码几乎不用改拦截逻辑。3. 文件下载接口与Vue端blob文件名保真的完整链路3.1 后端接口怎么返回文件流不踩坑上位机场景里导出检测报告、工艺参数、统计Excel是非常常见的需求。很多初学WebApi的朋友搞不定的是怎么让接口返回一个文件而不是把文件路径给前端让浏览器自己访问。最省事的方式是把文件读成字节数组用File方法返回文件名的编码是核心细节[HttpGet(export/{id})] public async TaskIActionResult ExportReport(int id) { var filePath await _reportService.BuildReportAsync(id); var fileName $检测报告_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx; return PhysicalFile(filePath, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, fileName); }注意PhysicalFile返回的是物理文件不用自己读字节流。文件名带中文和日期时间ASP.NET Core会自动做RFC 5987编码输出filename*字段。我见过有人为了让文件名看起来正常手动拼接Content-Disposition头结果把中文文件名彻底搞乱的情况这里建议直接用框架内置的FileResult机制不要自己造轮子。还有一个容易忽略的点报告文件如果是动态生成的记得考虑并发——两个前端同时请求导出同一份报告时后端别生成两个同名文件互相覆盖。最简单的做法是文件名带GUID或时间戳生成完返回实际落盘路径前端下载完成后再由后端定时清理临时文件。3.2 Vue前端blob下载与文件名乱码的终极解法前端这块是最容易出现玄学问题的地方。直接用window.open(url)去下载WebApi的文件要么碰上浏览器拦截弹窗要么因为请求带了Token认证信息而下载失败。正确做法是用axios带responseType: blob拉取二进制流再通过Blob对象触发下载export function downloadReport(id) { return request({ url: /api/report/export/${id}, method: get, responseType: blob }); }拿到blob响应后关键一步是从响应头里解析文件名。如何保持文件名不变 blob这个热搜词背后就是这个问题。后端返回的文件名放在Content-Disposition头里格式看着像这样Content-Disposition: attachment; filenamereport.xlsx; filename*UTF-8%E6%A3%80%E6%B5%8B%E6%8A%A5%E5%91%8A_20250601_103000.xlsx解析函数我写在项目里直接用function getFileNameFromDisposition(disposition) { if (!disposition) { return download; } // 优先取 filename*RFC 5987 编码支持中文 const utf8Match disposition.match(/filename\*UTF-8([^;])/i); if (utf8Match) { try { return decodeURIComponent(utf8Match[1]); } catch (e) { // 解码失败降级处理 } } // 兼容老浏览器退回到 filename 字段 const plainMatch disposition.match(/filename?([^])?/i); return plainMatch ? plainMatch[1] : download; }拿到文件名后创建临时a元素触发点击下载function blobDownload(blobData, disposition) { const fileName getFileNameFromDisposition(disposition); const blob new Blob([blobData], { type: blobData.type }); const link document.createElement(a); link.href URL.createObjectURL(blob); link.download fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); }这里有两个细节容易踩坑一是link必须追加到document.body上否则Firefox会忽略点击事件二是下载完成调用URL.revokeObjectURL释放内存否则页面跑久了会卡。最终的效果就是后端文件名是检测报告_20250601_103000.xlsx前端下载下来就是这个名字保持原样不变不再出现乱码或者undefined这种鬼名字。3.3 大文件下载的取舍上位机导出的检测报告如果带图片文件动不动就上百兆。用WebApi直接返回字节数组的方式在高并发下不可取。我在Demo里预留了一个优化点通过流式响应用FileStreamResult边读边发避免大文件一次性加载进内存[HttpGet(export-stream/{id})] public IActionResult ExportStream(int id) { var filePath _reportService.GetReportPath(id); var stream System.IO.File.OpenRead(filePath); return File(stream, application/octet-stream, Path.GetFileName(filePath)); }这样IIS/Kestrel会按块把文件刷给客户端内存占用很小。但要注意控制器方法要写成IActionResult而不是async TaskIActionResult因为流的释放由框架监管手动Dispose反而容易挂。如果想做断点续传ASP.NET Core的PhysicalFile底层其实支持Range请求前端用axios的时候会默认带上Range头但对于普通的上位机下载场景没必要把复杂度拉这么高。4. CORS跨域配置与IIS发布这两个环节最掉链子4.1 CORS配置的三种姿势别把*用得满天飞Vue开发服务器跑在http://localhost:5173WebApi跑在http://localhost:5000端口不同必然产生跨域问题。浏览器在发跨域请求前先发一次OPTIONS预检后端如果不正确响应前端就报CORS policy错误这是联调第一天最容易碰见的红屏。.NET 8里配置CORS非常简单Program.cs中两行搞定builder.Services.AddCors(options { options.AddPolicy(VueDev, policy policy.WithOrigins(http://localhost:5173, http://localhost:8080) .AllowAnyHeader() .AllowAnyMethod()); }); // 在app.MapControllers()之前启用 app.UseCors(VueDev);这里有个细节很多人不知道如果前端用了withCredentials: true也就是请求里携带了Cookie或HTTP认证头那么WithOrigins里绝对不能写*通配符浏览器会直接拒绝请求。业界零容忍的一条规则就是凡是带凭证的跨域请求源地址必须白名单一个一个列出来。你可能会问CORS配置放在Demo项目里有什么讲究我的建议是分环境拆开开发环境允许localhost:5173等本地地址生产环境只允许部署看板的那台服务器IP。别图省事统一用AllowAnyOrigin()否则随便一个网页挂在公网上都能往你工控机接口发请求安全上完全不可控。4.2 发布WebApi项目到IIS的经典三连坑热搜词里有发布webapi项目我猜不少人卡在IIS部署这一步。网上教程一大堆但三个坑最容易把人绕晕第一个坑是服务器必须装.NET Core Hosting Bundle。很多人以为把发布目录拷到服务器上就完事了结果站点启动直接502.5看日志才发现运行时都没装。Hosting Bundle包含了ASP.NET Core Module和运行时装完记得重启IIS。第二个坑是应用程序池的CLR版本要设为无托管代码。传统.NET Framework项目要选v4.0但.NET Core项目完全反过来了一旦设成v4.0站点会启动失败或者无限重启。这个细节和直觉完全相反踩坑的人最多。第三个坑是发布目录的权限。IIS默认应用程序池身份是DefaultAppPool如果你的发布目录在C盘Program Files下面AppPool没有写权限日志、临时文件都写不进去。更稳妥的方案是把发布目录放到独立的D盘目录并给IIS_IUSRS组授予修改权限。4.3 appsettings.json外部化的习惯要早点养成Demo项目里的连接字符串、服务器地址、相机IP这些配置绝对不能写死在代码里。ASP.NET Core的配置文件机制天生支持外部化发布后可以直接改appsettings.json也可以设置ASPNETCORE_ENVIRONMENT来加载不同的环境配置。我的习惯是只用appsettings.json但把敏感配置单独放在appsettings.Production.json里IIS应用中通过环境变量指定。写死配置的教训我吃过太多次了开发机上连的都是本地库发布到工控机上忘记改连接字符串程序启动报错排查半天发现是连了不存在的数据库。把配置外置化运维兄弟也能自己改不用每次找你重新编一个发布包。5. 扫码枪、上位机通讯与机器视觉的集成方案5.1 扫码枪接入的上位机典型写法热搜词里有c# 扫码枪触发事件和c# 工业级网口通讯助手这两条放在一起指向的是工业数据采集场景。扫码枪的接入方式主流有两种USB-HID模式模拟键盘输入和串口模式。USB-HID模式下扫码枪物理上就是一个键盘焦点在哪字就打在哪。这种模式接入最简单但触发事件不是真正的串口事件而是文本框内容变化你在WPF窗体上放一个始终聚焦的TextBoxTextChanged事件里判断是不是完整条码然后清空等待下一次扫描。串口模式下才是真正的扫码枪触发事件public class BarCodeScanner { private readonly SerialPort _serialPort; private readonly StringBuilder _buffer new(); public event Actionstring? BarCodeScanned; public BarCodeScanner(string portName, int baudRate 9600) { _serialPort new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One); _serialPort.DataReceived OnDataReceived; } private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { var data _serialPort.ReadExisting(); _buffer.Append(data); // 扫码枪默认以回车符作为条码结束标记 if (_buffer.ToString().EndsWith(\r)) { var code _buffer.ToString().TrimEnd(\r, \n); _buffer.Clear(); BarCodeScanned?.Invoke(code); } } public void Open() _serialPort.Open(); public void Close() _serialPort.Close(); }拿到条码之后上位机主程序把它和当前工单号、设备编号封装成JSONPOST到WebApi的/api/trace/record接口完成落库MES系统和Vue看板就能实时看到最新扫码记录。这里有个并发细节扫码枪可能连续快速扫多个码后端接收写入如果用的是同步数据库操作高吞吐下容易阻塞。我在Demo里引入了Channel做生产消费队列扫码枪只管入队后台消费者批量写入数据库稳定得多。5.2 海康相机VisionMaster与C#上位机通讯协议选型的经验那条热搜写得很具体海康相机软件VisionMaster与C#上位机软件通讯使用什么协议比较好这个问题我一开始也纠结过。VisionMaster提供了C#的SDK官方推荐方式是通过SDK回调函数直接获取检测结果、图像数据和判定OK/NG信号。SDK方式最大的好处是强类型拿到的就是一个结构体不用解析字符串开发效率最高适合单机单工位的视觉检测。但如果你做的是多工位视觉检测平台一台C#上位机要管理好几台VisionMaster我建议用TCP私有协议。VisionMaster软件自带通信模块可以在视觉流程结束时作为TCP客户端或服务端发送检测结果。C#侧用TcpListener做服务端接收或者用TcpClient主动拉取报文格式定义为帧头长度JSON内容的简单结构。为什么优先选TCP而不是串口或HTTP因为VisionMaster的检测节拍非常快可能一秒钟出几十个结果串口的波特率传输不了这么大的数据量而上位机进程内的WebApi只负责对外通知不会直接和VisionMaster搞HTTP通讯内部走TCP是最轻量的。5.3 WebApi在上位机系统中的定位是数据总线我在这个Demo里想表达的核心观点是WebApi在上位机系统里不只是一个接口层它本质上是整个系统的数据总线。WPF主窗体负责设备交互和人员操作扫码枪注入数据相机检测产生数据WebApi负责把这些数据统一封装成HTTP服务供外部系统消费。实现方式不复杂在WPF程序启动时后台线程拉起Kestrelpublic class ApiHostService { private WebApplication? _app; public void Start() { var builder WebApplication.CreateBuilder(); builder.WebHost.UseUrls(http://0.0.0.0:8080); // 注册服务 builder.Services.AddSingletonIDeviceService(_deviceService); // ... _app builder.Build(); _app.MapControllers(); _app.RunAsync(); } }这样整个系统对外就只有一个程序进程启动WPF同时也启动了WebApi不需要额外部署IIS只要防火墙放行8080端口外部系统的接入路径就通了。这个方案在国外叫Edge Gateway模式在国内就是工控机上最常见的上位机架构。6. 进阶扩展反射、并发控制与性能要点6.1 用反射写一个通用设备状态接口热搜词里有c#反射在WebApi里一个很实用的场景是通用设备状态接口。假设你的产线上有20台设备每台设备一个状态类字段各不相同与其给每台设备写一个查询接口不如用反射写一个通用接口[HttpGet(device/{deviceType}/status)] public IActionResult GetDeviceStatus(string deviceType) { var type Type.GetType($MyApp.Devices.{deviceType}); if (type null) return Ok(ApiResultobject.Fail($未找到设备类型: {deviceType})); var device (IDevice)_services.GetType().GetMethod(GetDevice)! .MakeGenericMethod(type) .Invoke(_services, null)!; return Ok(ApiResultobject.Success(device.GetStatus())); }这种反射的写法在新增设备类型时完全不用改Controller只要新增对应的类并注册服务就能自动对外暴露接口扩展性很强。不过反射有性能损耗和类型安全风险适合低频的状态查询接口高频的生产数据写入接口还是老老实实写专用代码。6.2 并发控制从lock到SemaphoreSlim热搜词c# tcp连接数量多少本质是在问并发。WebApi本身是异步模型Kestrel能支撑的连接数对工业场景来说根本不是瓶颈真正的瓶颈在你操作共享资源的时候。我在Demo里用SemaphoreSlim而不是lock因为lock不能跨异步方法使用一旦await语句出现在lock块里就编译报错。private readonly SemaphoreSlim _gate new(1, 1); public async Taskint AddTraceRecordAsync(TraceRecord record) { await _gate.WaitAsync(); try { // 操作共享的Excel文件、串口、或者数据库连接池 return await _repository.InsertAsync(record); } finally { _gate.Release(); } }还有一个思路值得借鉴对于大量写入请求用System.Threading.Channels做批量缓冲每攒够100条或者500毫秒就flush一次入库比单条插入性能提升好几倍。这个模式在扫码枪一秒连续触发、相机一秒出几十个结果的场景里特别管用。6.3 性能优化的几个小习惯Demo项目虽然小但从一开始就注意性能可以少走很多弯路。一是所有可能耗时的操作都做异步化比如File.ReadAllBytes要改成await File.ReadAllBytesAsync文件流的复制要走CopyToAsync二是高频查询加内存缓存比如设备状态5秒内不变就直接从缓存取不用每次都去底层PLC读三是JSON序列化用System.Text.Json开箱即用性能足够四是给Swagger配置好让前端同学自己看接口文档省得你一遍遍解释字段含义。这几条不属于炫技范畴都是生产环境里实打实会用到的。代码写到这里Demo已经覆盖了创建项目、统一响应、文件下载、Vue对接、CORS部署、扫码枪接入、相机通讯、反射和并发控制这几个典型场景。最后再分享一个实际运营中的小经验WebApi上线后一定要在Program.cs里把日志配好用Serilog输出到文件和Seq/数据库都可以。很多现场问题你坐在办公室根本复现不了全靠现场日志回传才能定位。另外给所有对外接口加一个简单的API Key中间件30行代码就能挡住网上扫描器的骚扰这个在下位机暴露到局域网时特别有用。本文还有配套的精品资源点击获取
返回列表