ARTICLE DETAIL

资讯详情

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

C# WebApi实战:路由、参数绑定与上位机部署调用指南

C# WebApi实战:路由、参数绑定与上位机部署调用指南 简介这是一套基于C# ASP.NET Web API的实战项目Demo通过完整可运行的示例演示跨平台客户端如何调用HTTP接口。项目包含路由配置、控制器编写、数据查询以及HttpResponseException异常处理等典型场景适合已有C#基础、希望快速掌握Web API开发流程的学习者。资源共402个文件压缩包约27.17MB包含DLL运行库、XML文档、C#源码、cshtml视图、config配置及js/css前端资源等分别对应程序集依赖、注释说明、业务逻辑、页面展示与样式请求等不同层面便于对照理解项目结构。目前已有3558人学习使用。借助其中的接口定义方式如获取电影信息的GetMovie方法和请求路径设计可以梳理参数绑定、响应格式、HTTP状态码处理等Web API核心概念并借鉴其分层实现方式快速迁移到自己的项目中。1. 从上位机想给别人接口到 C# WebApi做上位机的同行大多经历过这种场景WinForm 里已经把西门子 1200 的数据采上来了界面上显示得好好的MES 或者看板系统找你要数据你说我写个接口给你。结果对方要的既不是 TCP 裸报文也不是 DLL而是一个 HTTP 接口GET http://192.168.1.50:8080/api/record/1001返回 JSON。这就是 C# WebApi 的地盘不管有没有网页只要提供数据接口就算 WebApi。这篇用一个能查设备记录、能接收上位机上报数据的 C# WebApi 实战 Demo把路由、参数绑定、发布部署、WinForm 调用整条链路讲清楚。新手能照着跑通写过几年代码的人也能在参数细节和部署的坑上有点收获。Demo 的目的不是做出大系统而是用最短的代码量看到完整的 WebApi 工作方式再替换成自己的业务。2. 先理解 WebApi 的三条主线路由、参数绑定、返回格式2.1 路由匹配URL 到 Action 的映射规则WebApi 的核心是路由一个 HTTP 请求进来框架要从 URL 和 HTTP 动词里找出该调用 Controller 的哪个 Action。先写一个最小控制器using Microsoft.AspNetCore.Mvc; [ApiController] [Route(api/[controller])] public class RecordController : ControllerBase { [HttpGet({id})] public IActionResult GetById(int id) { if (id 0) { return BadRequest(id 必须大于 0); } return Ok($收到 id {id}); } }[Route(api/[controller])]里的[controller]是占位符运行时会替换成控制器名去掉 Controller 后缀的形式RecordController 对应 Record所以访问路径是api/record。[HttpGet({id})]把 URL 里的{id}绑定到方法参数int id并且限定这个 Action 只响应 GET 动词。如果上位机用 POST 发到同一个地址框架直接回 405 Method Not Allowed这是新手最先遇到的报错之一。[ApiController]特性值得单独说。它自动做了几件事参数校验失败时返回 400不再需要你手动判断ModelState.IsValid要求复杂类型参数必须显式声明绑定来源出错时自动生成ProblemDetails格式的错误信息。我建议所有 Demo 和项目都加上这个特性能少写大量样板代码。2.2 参数绑定这些参数到底从哪里来这是 WebApi 新手最常翻车的地方。Action 参数不是只能从 URL 里取它有明确的来源规则由参数类型和绑定特性共同决定绑定来源特性请求示例适用场景URL 路径[FromRoute]/api/record/1001按 ID 查单条资源查询字符串[FromQuery]/api/record?page1size20列表分页、筛选请求体 JSON[FromBody]POST 体是 JSON提交复杂对象表单[FromForm]POST 体是 urlencoded老设备上报、WinForm 提交规则记住两条就够简单类型int、string、double、bool 这类默认从 URL 路径或查询字符串绑定自定义 class、record 这类复杂类型默认从请求体绑定[ApiController]还强制你显式写清来源。比如后面会出现的[FromForm] DeviceRecordInput input就是告诉框架去表单里解析而不是去 URL 里找。一个容易忽略的对应关系POST 的 body 是 JSON 时Content-Type 必须带application/jsonbody 是表单时则是application/x-www-form-urlencoded。框架靠 Content-Type 选择格式化器两边对不上就报 415 Unsupported Media Type。很多接口调不通的问题其实都停在这一步。2.3 返回格式Ok、NotFound、BadRequest 的区别ControllerBase 提供一组快捷方法控制返回结果return Ok(record); // 200响应体带 JSON return NotFound(); // 404响应体为空 return BadRequest(...); // 400响应体带错误信息 return CreatedAtAction(nameof(GetById), new { id 1 }, record); // 201它们都返回IActionResult最终由框架统一序列化成 JSON。HTTP 状态码表达传输层成功失败JSON 体承载业务数据。有人习惯业务失败也返回 200在 body 里写 code500上位机就得先解析 JSON 再判断等于把协议层的错误降级成业务错误排查链路多一层。生产接口请让状态码表达成败body 只表达业务结果。2.4 用 Swagger 把第一个接口拉起来用模板生成项目后Swagger 的配置已经在Program.cs里var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers(); // 注册控制器相关服务 builder.Services.AddEndpointsApiExplorer(); // Swagger 需要的元数据服务 builder.Services.AddSwaggerGen(); // 生成 Swagger 文档 var app builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); // 输出 /swagger/v1/swagger.json app.UseSwaggerUI(); // 提供网页调试界面 } app.MapControllers(); // 映射所有带 [Route] 的控制器 app.Run();跑起来访问/swagger就能看到接口列表、参数类型和返回模型页面直接点击就能发请求。对学 WebApi 来说这是比 Postman 更短的反馈回路Demo 一上来就把这个页面打开。3. 做一个能查设备记录、能收上位机数据的 WebApi 实战 Demo3.1 场景先放一批模拟 PLC 采集记录假设现场有若干台设备定期上报温度、湿度和采集时间。需求有两个一是查询单条记录给 MES 看板用二是接收设备上报模拟上位机 POST 数据进来。先定义数据模型public class DeviceRecord { public int Id { get; set; } public string DeviceName { get; set; } public double Temperature { get; set; } public double Humidity { get; set; } public DateTime CollectTime { get; set; } }这个模型对应数据库里的一张表。为了把注意力集中在 WebApi 本身先用静态类模拟仓库public static class RecordStore { private static readonly ListDeviceRecord _records; static RecordStore() { _records new ListDeviceRecord { new DeviceRecord { Id 1, DeviceName PLC-01, Temperature 36.5, Humidity 42.1, CollectTime DateTime.Now.AddMinutes(-5) }, new DeviceRecord { Id 2, DeviceName PLC-02, Temperature 38.2, Humidity 40.5, CollectTime DateTime.Now.AddMinutes(-4) }, new DeviceRecord { Id 3, DeviceName PLC-03, Temperature 35.9, Humidity 41.0, CollectTime DateTime.Now.AddMinutes(-3) } }; } // 按 ID 查一条记录找不到时返回 null public static DeviceRecord GetById(int id) { return _records.FirstOrDefault(r r.Id id); } // 简单自增 ID 后写入列表 public static void Add(DeviceRecord record) { record.Id _records.Max(r r.Id) 1; _records.Add(record); } }FirstOrDefault查不到时返回nullAdd里用Max生成自增 ID。换成数据库时这两个方法内部改成 EF Core 或 Dapper 查询即可Controller 层写法完全不变。并发上报时ListT不是线程安全的Demo 阶段可以忽略生产环境换数据库天然解决。3.2 查询接口GET /api/record/{id}按 ID 找一条记录并把字段返回对应的需求就是热词里常出现的显示查找一条记录字段数据。Controller 里写[HttpGet({id})] public IActionResult GetRecord(int id) { var record RecordStore.GetById(id); if (record null) { return NotFound(new { code 404, message $记录 {id} 不存在 }); } return Ok(record); }new { code 404, message ... }是匿名对象序列化后变成{code:404,message:记录 100 不存在}。查不到返回 404 而不是null加 200原因前面说过状态码要真实表达结果。Ok(record)会把DeviceRecord序列化成 JSON字段名默认是 camelCase也就是deviceName、temperature这是 .NET 6 模板对前端友好的默认行为第 5 章再讲怎么关。用 curl 验证curl http://localhost:5000/api/record/1 curl http://localhost:5000/api/record/999第一条返回记录 JSON第二条返回 404。端口号取决于launchSettings.json模板默认可能是 5000 到 7000 之间的某个值以启动日志里实际监听的端口为准。3.3 写入接口POST 接收 application/x-www-form-urlencoded 设备上报老设备、PLC 网关、WinForm 程序上报数据时最常用的格式就是application/x-www-form-urlencoded也就是浏览器表单那种keyvaluekey2value2。虽然 RESTful 风格更推崇 JSON但兼容现场旧设备时表单格式必须支持。定义输入模型public class DeviceRecordInput { public string DeviceName { get; set; } public double Temperature { get; set; } public double Humidity { get; set; } public DateTime? CollectTime { get; set; } }DateTime?表示采集时间可选上位机没传就用服务器当前时间这是一种常见容错。Action 写成[HttpPost] public IActionResult CreateRecord([FromForm] DeviceRecordInput input) { if (string.IsNullOrWhiteSpace(input.DeviceName)) { return BadRequest(new { code 400, message DeviceName 不能为空 }); } var record new DeviceRecord { DeviceName input.DeviceName, Temperature input.Temperature, Humidity input.Humidity, CollectTime input.CollectTime ?? DateTime.Now // 可选时间用当前时间兜底 }; RecordStore.Add(record); return CreatedAtAction(nameof(GetRecord), new { id record.Id }, record); }[FromForm]从表单体解析参数Content-Type 必须和这个特性匹配。CreatedAtAction有三个参数第一个是 Action 名GetRecord第二个是路由参数填充GetRecord里的{id}第三个是响应体。它生成的状态码是 201 Created响应头里带Location: /api/record/4。对持续上报的设备来说201 和 200 都能用但按语义写 201 能在排查数据到底进没进库时少一步猜测。curl 模拟一次上报curl -X POST http://localhost:5000/api/record \ -d DeviceNamePLC-04Temperature37.1Humidity43.0curl -d不指定 Content-Type 时默认就是application/x-www-form-urlencoded和[FromForm]正好对上。再执行一次查询接口能看到新记录 ID 是 4collectTime已被自动填充。3.4 状态码语义和错误返回别让上位机去猜接口变多以后状态码语义必须统一不然上位机同事会来来回回问你这个返回到底算不算成功。长期维护下来我用这套规则场景方法状态码返回体示例查询到记录GET200完整记录 JSON记录不存在GET404{code:404,message:记录 100 不存在}参数非法GET/POST400具体错误原因新建成功POST201新记录 JSON Location 头服务端异常任意500统一错误体第 5 章实现注意[ApiController]会在参数绑定失败比如 Temperature 传了 abc时自动返回 400但业务规则校验DeviceName 为空不会自动执行必须自己写。错误体里的 code 字段和 HTTP 状态码保持一一对应上位机先看状态码再看 JSON 定位问题不用解析双重错误结构。4. 发布 C# WebApi 到服务器再用 WinForm 把接口调通4.1 C# WebApi 的发布dotnet publish 与 IIS 部署要点Demo 在本地跑通只是第一步。工控项目的常见形态是开发机上是 WinForm 或 WPF部署时把 WebApi 单独发布到服务器或工控机上位机通过局域网 IP 访问。发布命令dotnet publish -c Release -o ./publish-c Release指定 Release 配置-o ./publish指定输出目录。发布完成后publish 文件夹里包含程序集、依赖和web.config。如果目标服务器没装 .NET 运行时可以用自包含模式dotnet publish -c Release -r win-x64 --self-contained true -o ./publish-r win-x64指定目标平台--self-contained true把运行时一起打进去。自包含包体积大不少但工控机离线部署时最省事。部署到 IIS 时把 publish 文件夹指到站点目录还要在服务器安装对应版本的 .NET Hosting Bundle否则 IIS 返回 502 或 500.31 这类错误。老项目读者要特别注意.NET Framework 4.0 时代创建的 WebApi 项目不能直接跑在 .NET 6 运行时上需要迁移或重建。没有 IIS 的环境直接用 Kestrel 跑./WebApiDemo.exe --urls http://0.0.0.0:8080--urls指定监听地址0.0.0.0:8080表示监听本机所有网卡的 8080 端口。只写http://localhost:8080的话局域网其他机器访问不到这是发布后别人连不上的第一个检查点。4.2 发布后的验证顺序curl、局域网 IP、防火墙我排障的顺序固定三步本机、局域网、防火墙。第一步在本机确认服务活着curl http://localhost:8080/api/record/1第二步换成服务器局域网 IPcurl http://192.168.1.50:8080/api/record/1localhost 通而 IP 不通优先查监听地址再查防火墙。Windows 防火墙放行入站规则的命令netsh advfirewall firewall add rule nameWebApiDemo 8080 dirin actionallow protocolTCP localport8080dirin是入站方向protocolTCP localport8080放行 TCP 8080 端口执行需要管理员权限。如果现场网络还有交换机或安全组策略还要在网络设备层面放行一次。这三步走完发布后的基本连通性就有保障了。Swagger 在发布后访问http://192.168.1.50:8080/swagger也能打开调试阶段保留它是很方便的生产环境建议限制内网访问。4.3 WinForm 测试 C# WebApi 接口HttpClient 消费 GET 和 POST对应 winform 中测试 webapi 接口 的需求可以在 WinForm 里写一个简易调试工具核心是HttpClient。GET 查询一条记录并显示字段private static readonly HttpClient http new HttpClient { Timeout TimeSpan.FromSeconds(5) // 5 秒超时避免界面长时间卡住 }; private async void btnQuery_Click(object sender, EventArgs e) { try { string url $http://192.168.1.50:8080/api/record/{txtId.Text}; string json await http.GetStringAsync(url); // 按不区分大小写的方式反序列化兼容 camelCase 返回 var record JsonSerializer.DeserializeDeviceRecord(json, new JsonSerializerOptions { PropertyNameCaseInsensitive true }); lblDevice.Text $设备{record.DeviceName}; lblTemp.Text $温度{record.Temperature}℃; lblHumidity.Text $湿度{record.Humidity}%; } catch (HttpRequestException ex) { MessageBox.Show($请求失败{ex.Message}); } }PropertyNameCaseInsensitive true是因为 WebApi 返回的 JSON 字段是 camelCase而 C# 模型属性是 PascalCase打开这个开关才能正确对上。WinForm 项目里如果提示找不到JsonSerializer通过 NuGet 安装System.Text.Json包即可。HttpClient建议做成静态字段每次新建会堆积 TIME_WAIT 连接长时间运行必然出问题。POST 上报用FormUrlEncodedContent构造 urlencoded 请求体private async void btnUpload_Click(object sender, EventArgs e) { var form new FormUrlEncodedContent(new Dictionarystring, string { [DeviceName] txtDeviceName.Text, [Temperature] txtTemperature.Text, [Humidity] txtHumidity.Text }); HttpResponseMessage resp await http.PostAsync(http://192.168.1.50:8080/api/record, form); string result await resp.Content.ReadAsStringAsync(); if (resp.IsSuccessStatusCode) MessageBox.Show($上报成功{result}); else MessageBox.Show($失败({(int)resp.StatusCode}){result}); }FormUrlEncodedContent会自动把字典转成DeviceNamePLC-04Temperature37.1并设置Content-Type: application/x-www-form-urlencoded和 3.3 节[FromForm]的期望完全一致。这是老上位机接 WebApi 最省事的路径不需要引第三方库。事件处理器用async void是 WinForm 里的常见写法UI 线程上下文会自动回到界面线程更新控件不用手动 Invoke。只要严格按async void try/catch 内部全部 await来写就不会有死锁问题。4.4 发布后常见连接问题对照表把高频问题整理成一张表现场排查时对照着看现象优先检查对应位置本机连接超时服务没启动、端口被占用启动日志、--urls局域网连不上监听地址是否为 0.0.0.0、防火墙规则4.1、4.2404路径少了api/前缀或控制器名拼错2.1 路由405HTTP 动词不对POST 发给了 GET 接口2.1[HttpGet]415Content-Type 和参数绑定特性不匹配2.2 表格502 / 500.31IIS 缺少 Hosting Bundle 或应用池运行时版本不匹配4.1中文乱码老设备用 GBK 编码提交服务端按 UTF-8 解析客户端统一为 UTF-8 或服务端做编码转换中文乱码这条在工控现场出现的概率很高因为部分老设备固件写死了 GBK 编码。最可靠的做法是现场设备侧统一改为 UTF-8改不了的话客户端在构造请求时换成自定义ByteArrayContent并指定编码。5. WebApi 上线前的三个配置异步、CORS、全局异常5.1 用 async/await 把查询接口改成异步上面所有 Action 都是同步的。真实项目里查询数据库、调用 PLC 通信都是 IO 操作用异步可以让线程在等待期间回线程池处理其他请求[HttpGet({id})] public async TaskIActionResult GetRecordAsync(int id) { // 示例用 Task.Delay 模拟数据库或 PLC 通信的 IO 等待 await Task.Delay(10); var record RecordStore.GetById(id); if (record null) return NotFound(new { code 404, message $记录 {id} 不存在 }); return Ok(record); }方法的返回类型从IActionResult改成TaskIActionResult内部 await 真正的异步方法这就是全部改动。注意不要为了异步而异步用Task.Run包一个同步方法没有收益反而多一次线程切换。以后接数据库时把ToList()换成ToListAsync()把GetById换成 EF Core 的异步版本自然就完整了。5.2 给网页端开 CORS浏览器页面也能调用 WebApi如果 MES 看板是一个 Vue 页面浏览器里直接 fetchhttp://192.168.1.50:8080/api/record/1会撞上同源策略被拦截。在Program.cs配置 CORSbuilder.Services.AddCors(options { options.AddPolicy(AllowWebApp, policy policy.WithOrigins(http://localhost:5173) // 只允许这个来源 .AllowAnyHeader() .AllowAnyMethod()); }); var app builder.Build(); app.UseCors(AllowWebApp);WithOrigins是明确的来源白名单生产环境不要把前端地址写错或放开成AllowAnyOrigin()。如果跨域请求还带 Token需要额外的AllowCredentials()配合此时更不能用通配符来源。5.3 全局异常与字段命名策略各花十分钟配置就位最后两个配置能在后续对接里省很多时间。第一个是全局异常处理保证任何未捕获异常都返回统一 JSON不把堆栈裸抛给调用方app.UseExceptionHandler(errorApp { errorApp.Run(async context { var ex context.Features.GetIExceptionHandlerFeature()?.Error; context.Response.StatusCode 500; await context.Response.WriteAsJsonAsync(new { code 500, message 服务端内部错误 }); }); });注册这段之后Controller 抛出的异常也会被拦截并统一序列化具体堆栈在服务端日志里看。第二个是字段命名策略。System.Text.Json 默认把DeviceName输出成deviceName如果你的上位机反序列化时没开不区分大小写会取不到值。关闭 camelCase 转换builder.Services.AddControllers() .AddJsonOptions(options { options.JsonSerializerOptions.PropertyNamingPolicy null; });null表示不转换命名JSON 字段直接输出DeviceName。上下位机双方事先约定好字段大小写规则比事后到处改解析代码省事得多。这两个配置都是改一次长期受益建议在 Demo 阶段就放进模板。本文还有配套的精品资源点击获取
返回列表