简介:本资源是一套完整的WebApi服务开发与调用实践项目,面向.NET初学者及中级开发者,聚焦RESTful接口设计、服务端实现与客户端通信两大核心能力。项目包含WebApi服务端(含控制器、路由、模型绑定)与独立访问客户端(基于HttpWebRequest封装),覆盖GET/POST等标准HTTP操作、JSON数据交互、异常处理及基础身份验证场景,适用于分布式系统入门学习与企业级API对接实训。压缩包共553个文件,15.96MB,以73个C#源码文件(cs)、119个运行依赖库(dll)、65个界面资源(png)、57个前端脚本(js)和35个样式文件(css)为主,辅以csproj工程文件、config配置及nupkg包管理文件,结构清晰,便于分层理解与模块调试。已有1467人学习下载,配套代码具备完整可运行性,含详细注释与典型错误处理示例,是掌握WebApi全链路开发的高实用性参考范例。
1. WebApi 和访问 WebApi:两个项目不是“配套安装包”,而是服务端与客户端的完整闭环实践
你手头拿到的“WebApi 和访问 WebApi 两个项目”,不是某个框架的示例模板,也不是教学视频附赠的压缩包——它是一组可独立编译、可本地调试、可真实交互的最小可行闭环:一个项目负责对外暴露 HTTP 接口(即 WebApi 服务端),另一个项目则作为调用方,通过 HttpClient、Postman 或前端 JS 发起请求(即访问 WebApi 客户端)。这种结构在企业级开发中极为常见,但新手常误以为“只要跑通服务端就算完成”,结果在联调阶段卡死在跨域、认证失败、JSON 序列化异常或 HTTPS 重定向上。这两个项目真正价值在于:它把 .NET 生态下 WebApi 的契约定义、路由控制、模型绑定、错误统一处理、客户端请求构造、响应解析、异常捕获全链路拆解成两份可运行代码,且不依赖云环境、不强制使用 IdentityServer 或 Swagger UI。适合刚学完 ASP.NET Core 基础、正准备接手内部系统对接、或需要快速搭建内部管理后台 API 层的开发者。如果你正在为“接口能跑但别人调不通”“返回 400 却没报错信息”“DateTime 格式前后端对不上”这类问题反复改配置,这份资源就是为你写的。
2. WebApi 项目:从空模板到可调试接口服务的五步落地
2.1 创建 Minimal API 项目并启用关键中间件
我们不从传统 Controller 模板开始,而是采用 .NET 6+ 推荐的 Minimal Hosting 模式,它更轻量、更贴近生产部署逻辑。新建项目时选择ASP.NET Core Web API,目标框架设为.NET 6.0或更高(避免 .NET 5 的已知 CORS 行为差异):
dotnet new webapi -n MyWebApi --framework net6.0 cd MyWebApi关键不是生成代码,而是修改Program.cs—— 这里决定了你的 API 是否具备生产就绪能力。以下是最小但完整的配置骨架:
var builder = WebApplication.CreateBuilder(args); // 1. 添加 JSON 序列化配置(解决 DateTime / null 问题) builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); // 2. 启用 CORS(必须在 AddControllers 之后、Build() 之前) builder.Services.AddCors(options => { options.AddPolicy("AllowAll", policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); var app = builder.Build(); // 3. 配置中间件顺序(CORS 必须在 UseRouting 之后、UseEndpoints 之前) if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); // 开发时可注释,但务必保留此行结构 app.UseCors("AllowAll"); // ← 此处是 CORS 生效的关键位置 app.UseAuthorization(); app.MapControllers(); app.Run();提示:中间件顺序不是建议,是硬性规则。
UseCors()放在UseAuthorization()之后会导致预检请求(OPTIONS)被拦截;放在MapControllers()之后则完全不生效。这是大量开发者翻车的第一步。
2.2 定义强类型模型与控制器:避免字符串魔法和隐式转换
不要用object或dynamic接收请求体,也不要手动JsonConvert.DeserializeObject。Minimal API 的优势在于编译期检查。以用户登录为例,先定义 DTO:
// Models/LoginRequest.cs public class LoginRequest { [Required(ErrorMessage = "用户名不能为空")] [StringLength(20, MinimumLength = 2, ErrorMessage = "用户名长度为2-20位")] public string Username { get; set; } = string.Empty; [Required(ErrorMessage = "密码不能为空")] [StringLength(32, MinimumLength = 6, ErrorMessage = "密码长度为6-32位")] public string Password { get; set; } = string.Empty; } // Models/LoginResponse.cs public class LoginResponse { public bool Success { get; set; } public string Token { get; set; } = string.Empty; public string Message { get; set; } = string.Empty; }再编写控制器(注意[ApiController]特性与[Route]的组合):
// Controllers/AuthController.cs [ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { [HttpPost("login")] public IActionResult Login([FromBody] LoginRequest request) { if (!ModelState.IsValid) { return BadRequest(new { success = false, message = "参数校验失败", errors = ModelState }); } // 模拟验证逻辑(实际应接入 Identity 或 JWT) if (request.Username == "admin" && request.Password == "123456") { var token = Guid.NewGuid().ToString("N"); return Ok(new LoginResponse { Success = true, Token = token, Message = "登录成功" }); } return Unauthorized(new LoginResponse { Success = false, Message = "用户名或密码错误" }); } }参数说明:
[FromBody]明确告诉模型绑定器从请求体反序列化,避免 GET 请求误用 POST Body;ModelState.IsValid是防御性编程核心,它会自动收集所有[Required]、[StringLength]等特性校验结果;- 返回
BadRequest(...)而非throw new Exception(...),确保 HTTP 状态码为 400,客户端可据此做统一错误处理。
2.3 启用详细错误响应:让开发期调试不再靠猜
默认情况下,UseDeveloperExceptionPage()只显示 HTML 错误页,对 API 调试无用。我们需要让 500 错误也返回结构化 JSON:
// Program.cs 中,在 app.UseHttpsRedirection() 后添加 app.Use(async (context, next) => { try { await next(); } catch (Exception ex) { context.Response.StatusCode = StatusCodes.Status500InternalServerError; context.Response.ContentType = "application/json"; var errorResponse = new { success = false, message = "服务器内部错误", detail = ex.Message, stackTrace = app.Environment.IsDevelopment() ? ex.StackTrace : null }; await context.Response.WriteAsJsonAsync(errorResponse); } });为什么这么做:当业务逻辑抛出未捕获异常(如数据库连接失败、空引用),前端只看到
500 Internal Server Error,无法定位是哪行代码崩了。加上这段中间件后,开发环境下会返回带stackTrace的 JSON,极大缩短排错时间。上线前删掉stackTrace字段即可,不影响功能。
3. 访问 WebApi 项目:三种调用方式的选型依据与实操细节
3.1 使用 HttpClient 封装通用请求类:避免每次都 new 实例
HttpClient不是线程安全的“一次一建”对象,而是应复用的长生命周期实例。直接在Program.cs中注册为单例:
// 在 builder.Services 部分添加 builder.Services.AddHttpClient<IApiClient, ApiClient>(client => { client.BaseAddress = new Uri("https://localhost:7290/"); // ← 改为你的 WebApi 地址 client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); });再定义接口与实现:
// Services/IApiClient.cs public interface IApiClient { Task<T> PostAsync<T>(string endpoint, object data); Task<T> GetAsync<T>(string endpoint); } // Services/ApiClient.cs public class ApiClient : IApiClient { private readonly HttpClient _httpClient; public ApiClient(HttpClient httpClient) { _httpClient = httpClient; } public async Task<T> PostAsync<T>(string endpoint, object data) { var json = JsonSerializer.Serialize(data); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync(endpoint, content); response.EnsureSuccessStatusCode(); // ← 抛出异常若状态码非 2xx var responseJson = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize<T>(responseJson); } public async Task<T> GetAsync<T>(string endpoint) { var response = await _httpClient.GetAsync(endpoint); response.EnsureSuccessStatusCode(); var responseJson = await response.Content.ReadAsStringAsync(); return JsonSerializer.Deserialize<T>(responseJson); } }关键点:
EnsureSuccessStatusCode()是血泪经验。没有它,401 Unauthorized会静默返回null,而不是抛异常,导致后续NullReferenceException,掩盖真实问题。
3.2 控制台调用演示:验证服务连通性最直接的方式
新建一个ConsoleApp项目,注入IApiClient并测试登录:
// Program.cs(控制台项目) var builder = Host.CreateDefaultBuilder(args); builder.ConfigureServices(services => { services.AddHttpClient<IApiClient, ApiClient>(client => { client.BaseAddress = new Uri("https://localhost:7290/"); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); }); }); var host = builder.Build(); var apiClient = host.Services.GetRequiredService<IApiClient>(); try { var loginReq = new { Username = "admin", Password = "123456" }; var result = await apiClient.PostAsync<LoginResponse>("api/auth/login", loginReq); Console.WriteLine($"Token: {result.Token}"); } catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) { Console.WriteLine("❌ 登录失败:用户名或密码错误"); } catch (HttpRequestException ex) { Console.WriteLine($"❌ 请求失败:{ex.StatusCode} - {ex.Message}"); } catch (Exception ex) { Console.WriteLine($"❌ 未知错误:{ex.GetType().Name} - {ex.Message}"); }为什么用控制台而非浏览器:浏览器只能发 GET,且无法设置
Content-Type: application/json头;Postman 虽好但需手动维护环境变量;而控制台代码可直接集成进 CI 流程,做自动化冒烟测试。
3.3 前端 JavaScript 调用:绕过跨域限制的两种真实路径
当你把 WebApi 部署到https://api.example.com,而前端运行在http://localhost:3000,浏览器会拦截请求。解决方案只有两个真实有效的:
方案一:开发期代理(推荐 Vue/React/Vite)
在vite.config.ts中配置:
export default defineConfig({ server: { proxy: { '/api': { target: 'https://localhost:7290', // ← 指向你的 WebApi changeOrigin: true, secure: false, // 若 WebApi 用自签名证书,设为 false rewrite: (path) => path.replace(/^\/api/, '') } } } })前端代码直接调用/api/auth/login,Vite 自动转发到后端,浏览器认为是同源请求。
方案二:服务端启用 CORS(生产唯一合法方案)
回到 WebApi 的Program.cs,将AllowAll策略升级为精确控制:
builder.Services.AddCors(options => { options.AddPolicy("FrontendPolicy", policy => { policy.WithOrigins("https://your-frontend-domain.com", "http://localhost:3000") .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials(); // ← 若需携带 Cookie 或 Authorization 头,必须开启 }); }); // 使用时 app.UseCors("FrontendPolicy");注意:
AllowCredentials()与AllowAnyOrigin()互斥。一旦开启凭据支持,WithOrigins()必须显式列出所有允许域名,不能写*,否则浏览器拒绝。
4. 避坑:五个高频翻车现场与对应解法
4.1 现象:POST 请求返回 400 Bad Request,但 ModelState.IsValid 为 true,且无任何错误信息
原因:请求头Content-Type缺失或错误。Minimal API 默认只接受application/json,若前端发的是text/plain或未设 header,模型绑定器直接放弃解析,返回空模型 + 400。
解决:前端必须显式设置headers: { 'Content-Type': 'application/json' };后端可在控制器方法上加[Consumes("application/json")]强制约束,并在全局异常中间件中打印原始请求体用于排查。
4.2 现象:DateTime 字段反序列化为0001-01-01T00:00:00,或序列化后变成/Date(1234567890000)/
原因:.NET 默认 JSON 序列化器对DateTime使用 Unix 时间戳格式(/Date(...)/),而 JavaScriptnew Date()无法直接解析;同时,若传入字符串"2023-01-01",默认绑定器可能因格式不匹配而设为默认值。
解决:统一使用System.Text.Json的JsonSerializerOptions配置:
options.JsonSerializerOptions.Converters.Add(new JsonDateTimeConverter("yyyy-MM-dd HH:mm:ss")); options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;并在前端用toISOString()发送,后端接收后转为DateTimeOffset类型字段。
4.3 现象:HTTPS 重定向导致前端请求失败,错误提示ERR_SSL_PROTOCOL_ERROR
原因:开发时 WebApi 启用了UseHttpsRedirection(),但前端(如 Vite)代理到http://localhost:7290(HTTP),而 WebApi 自动 307 重定向到https://localhost:7290,但该 HTTPS 端口未监听或证书无效。
解决:开发期临时注释app.UseHttpsRedirection();或为 WebApi 项目生成并信任本地开发证书:dotnet dev-certs https --trust;或在代理配置中强制走 HTTP(Vite 的secure: false已涵盖)。
4.4 现象:调用GetAsync<T>时抛出NotSupportedException: Deserialization of reference types without parameterless constructor
原因:泛型方法T是一个没有无参构造函数的类(如public class User { public User(string name) => Name = name; }),JsonSerializer.Deserialize<T>无法实例化。
解决:DTO 类必须有 public 无参构造函数;或改用JsonSerializer.Deserialize<JsonElement>先解析为元素树,再手动映射;或在JsonSerializerOptions中启用PropertyNameCaseInsensitive = true并确保字段名完全匹配。
4.5 现象:Swagger UI 页面空白,Network 显示Failed to fetch
原因:Swagger 依赖Swashbuckle.AspNetCore包,但 Minimal API 模板默认不包含app.UseSwagger()中间件,或MapSwagger()路由未正确注册。
解决:安装包dotnet add package Swashbuckle.AspNetCore,并在Program.cs中添加:
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // ... if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); }注意:UseSwaggerUI()必须在UseRouting()之后、MapControllers()之前,且仅在IsDevelopment()下启用。
5. 跨平台调试技巧:用 curl 和 PowerShell 验证接口契约,不依赖 IDE
5.1 用 curl 做原子级接口验证(Linux/macOS/WSL)
不要等前端写完才测接口。打开终端,用curl直接模拟请求,它是检验 API 契约是否清晰的终极手段:
# 测试登录(POST JSON) curl -X POST https://localhost:7290/api/auth/login \ -H "Content-Type: application/json" \ -d '{"Username":"admin","Password":"123456"}' \ -k # ← 忽略 SSL 证书错误(开发期) # 测试 GET(带查询参数) curl "https://localhost:7290/api/users?id=123" -k # 查看完整响应头与状态码 curl -I https://localhost:7290/health为什么比 Postman 更可靠:Postman 会自动处理重定向、Cookie、编码,掩盖真实问题;
curl是裸协议操作,返回什么就是什么。若curl能通,一定是你的 API 没问题;若不通,问题一定在服务端配置。
5.2 PowerShell 替代方案(Windows 原生环境)
Windows 用户无需安装额外工具,PowerShell 5.1+ 内置Invoke-RestMethod,功能与 curl 完全对等:
# 登录请求 $body = @{ Username = "admin"; Password = "123456" } | ConvertTo-Json $response = Invoke-RestMethod -Uri "https://localhost:7290/api/auth/login" ` -Method Post ` -ContentType "application/json" ` -Body $body ` -SkipCertificateCheck # ← 绕过自签名证书 Write-Host "Token: $($response.Token)" # 批量测试多个接口 $endpoints = @("/api/auth/login", "/api/users", "/health") foreach ($ep in $endpoints) { try { $r = Invoke-RestMethod "https://localhost:7290$ep" -SkipCertificateCheck Write-Host "✅ $ep -> $($r.success)" } catch { Write-Host "❌ $ep -> $($_.Exception.Response.StatusCode.value__)" } }玄学提示:
-SkipCertificateCheck参数在 PowerShell Core 6+ 中可用,若用旧版 PowerShell,改用-SessionVariable sess+ 手动忽略证书策略(较复杂,此处略)。关键是:别让证书问题阻断你的接口验证流。
5.3 构建可复用的测试脚本:把验证逻辑固化为工程资产
把上述 PowerShell 脚本保存为test-api.ps1,并加入项目根目录。再在MyWebApi.csproj中添加<Target>,实现每次dotnet build后自动运行健康检查:
<Target Name="RunApiTestsAfterBuild" AfterTargets="Build"> <Exec Command="powershell -ExecutionPolicy Bypass -File "$(MSBuildThisFileDirectory)test-api.ps1"" /> </Target>这样,只要编译通过,就代表基础接口契约可用。它比单元测试更轻量,比人工点击更快捷,是团队内快速同步接口状态的低成本方案。
从那以后我每次交付 WebApi 项目,都会把test-api.ps1和curl命令清单一起打包进docs/目录,新同事拉下来执行一遍,5 分钟内就能确认环境是否 ready。这比写 20 页文档管用得多。希望帮到你。
本文还有配套的精品资源,点击获取