- 示例工程
【免费下载链接】practical-aspnetcore
Practical samples of ASP.NET Core 11, 10, 9, 8.0, 7.0, 6.0, 5.0, 3.1, 2.2, and 2.1,projects you can use. Readme contains explanations on all projects.
本篇技术指南围绕 practical-aspnetcore 仓库入门路径(Pathway 1)中的第二个练习展开:把上一个练习中创建的菜谱(Recipe)控制台应用,拆分为「REST API 后端 + 控制台客户端」两个独立程序。你将掌握如何使用 ASP.NET Core Minimal API 搭建 JSON REST 服务、如何让后端统一承载 JSON 读写与业务逻辑,以及如何用IHttpClientFactory让控制台通过 HTTP 与后端通信——这正是从单机程序迈向典型前后端分离架构的关键一步。
练习目标与架构定位
本练习(exercises/pathway-1/exercise-2.md)明确要求:
Take the recipe console application you have created and split it into two, one for backend with REST API and another is for the console.
也就是说,你需要在 exercise-1 完成的「单机菜谱管理控制台」基础之上,做一次架构层面的拆分,而不是重新实现业务。拆分后的形态是:
- 后端(Backend):一个 Web API 程序,负责所有业务逻辑,包括 JSON 文件的读取/写入、新建菜谱等;
- 控制台(Console):一个客户端程序,本身不再直接触碰数据文件,只通过HTTP 调用后端暴露的REST API完成操作;
- 通信协议:前后端之间统一使用JSON传递数据。
这一拆分蕴含了三个重要工程观念:
- 关注点分离:数据持久化、业务规则集中放在服务端,客户端只负责发起请求、呈现结果;
- 接口契约化:控制台与后端之间以 REST 端点 + JSON 载荷为契约,任何一端都可以独立演进、替换甚至换语言重写;
- 真实 HTTP 语义:练习要求你「先读透 HTTP 协议、理解如何设计 Web API」,因为 CRUD 操作(列出、新增、编辑菜谱)会映射为
GET、POST、PUT等不同的 HTTP 动词。
仓库中与本练习直接相关的资源有两组,正是后面两个章节的素材来源:projects/httpclientfactory(控制台侧的 HTTP 客户端方案)与 projects/minimal-api(后端 Web API 的最简写法)。
后端:用 Minimal API 搭建菜谱 REST 服务
练习允许使用 ASP.NET Core 6 的 Minimal API(WebApplication风格)来搭建后端。仓库 minimal-api/hello-world 展示了最精简的入口形态:
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Hosting; WebApplication app = WebApplication.Create(); app.Run(async context => { await context.Response.WriteAsync("Hello world .NET 6. Make sure you run this app using 'dotnet watch run'."); }); await app.RunAsync();端点映射:从 Map 到 MapGet / MapPost
Minimal API 的核心是把「HTTP 端点」直接映射到方法。仓库 minimal-api/map 给出了最朴素的写法:
IResult Index() => Results.Json(new Greeting("Hello World")); IResult About() => Results.Json(new { about = "me" }); var app = WebApplication.Create(); app.Map("/", Index); app.Map("/about", About); app.Run(); public record Greeting(string Message);注意这里出现了两个贯穿全文的关键元素:
Results.Json(...):把对象序列化为 JSON 响应,这正是「JSON 通信」在后端侧的出口;IResult:Minimal API 的统一返回类型,让端点可以灵活返回 JSON、状态码等内容。
对于菜谱场景,建议按 HTTP 动词精确映射,使用MapGet/MapPost/MapPut(仓库 minimal-api/map-post 展示了MapPost配合 JSON 模型绑定的完整写法):
IResult Greet([FromBody] Greeting greet) => Results.Json(new { greet.Message }); var app = WebApplication.Create(); app.MapPost("/greet", Greet); app.Run(); public record Greeting(string Message);把这段代码套到菜谱模型上,一个最小可运行的后端骨架就是:
using Microsoft.AspNetCore.Mvc; var app = WebApplication.Create(); // 菜谱数据模型(继承自 exercise-1 的定义) public record Recipe(string Title, string[] Ingredients, string Instructions, string[] Categories); // 存储与业务逻辑统一收拢到后端 var recipes = new List<Recipe>(); app.MapGet("/recipes", () => Results.Json(recipes)); // 列出全部菜谱 app.MapGet("/recipes/{title}", (string title) => recipes.FirstOrDefault(r => r.Title == title) is { } r ? Results.Json(r) : Results.NotFound()); // 按标题查找 app.MapPost("/recipes", ([FromBody] Recipe recipe) => { recipes.Add(recipe); return Results.Created($"/recipes/{recipe.Title}", recipe); // 新建菜谱 }); app.Run();业务逻辑与 JSON 持久化收归后端
练习反复强调一个原则:Backend handles all the business logic including the JSON reading/writing, creating a new recipe, etc.也就是 JSON 文件的读写、菜谱的新增与编辑,都必须发生在后端进程中,控制台不再碰磁盘文件。持久化层可以直接使用System.Text.Json(exercise-1 中已经要求掌握)把List<Recipe>序列化写入磁盘、启动时读回内存,例如:
using System.Text.Json; var file = Path.Combine(AppContext.BaseDirectory, "recipes.json"); // 启动时读取 List<Recipe> recipes = File.Exists(file) ? JsonSerializer.Deserialize<List<Recipe>>(await File.ReadAllTextAsync(file)) ?? [] : []; // 每次变更后落盘 await File.WriteAllTextAsync(file, JsonSerializer.Serialize(recipes, new JsonSerializerOptions { WriteIndented = true }));仓库 projects/json/json/Program.cs 展示了Results.Json序列化复杂对象(含字典、可空值、DateTimeOffset)的行为,可以作为设计菜谱 JSON 载荷时的参考。
用 MapGroup 组织端点
当端点增多时,可以用MapGroup统一前缀并抽取扩展方法(见仓库 minimal-api/map-group-1):
app.MapGroup("/recipes").MapRecipesApi(); public static class RecipesApi { public static RouteGroupBuilder MapRecipesApi(this RouteGroupBuilder group) { group.MapGet("/", () => Results.Ok("all recipes")); group.MapGet("/{title}", (string title) => Results.Ok(title)); return group; } }这样GET /recipes/、GET /recipes/{title}等路由都被归并到同一前缀下,代码结构更清晰。
控制台:通过 HttpClient 与后端对话
后端就绪后,控制台程序的职责变成「发起 HTTP 请求、解析 JSON 响应、渲染结果」。练习给出的关键资源是 projects/httpclientfactory 下的四个示例,它们覆盖了从「最基础用法」到「类型化客户端」的完整演进路线。
第一步:IHttpClientFactory 集中管理 HttpClient
HttpClient是 .NET 中发起 HTTP 请求的核心类型,但直接new HttpClient()并长期持有会带来连接复用与 DNS 变更等问题。IHttpClientFactory正是为解决这些问题而生的集中管理方案。仓库 httpclientfactory-1 展示了最基础的接入方式:
var builder = WebApplication.CreateBuilder(); builder.Services.AddHttpClient(); // 注册 IHttpClientFactory 服务 var app = builder.Build(); app.Run(async context => { var httpClient = context.RequestServices.GetService<IHttpClientFactory>(); var client = httpClient.CreateClient(); // 每次创建一个受管理的 HttpClient var result = await client.GetStringAsync("http://scripting.com/rss.xml"); context.Response.Headers.Append("Content-Type", "application/rss+xml"); await context.Response.WriteAsync(result); }); app.Run();该示例在 Web 应用中演示,但AddHttpClient()+CreateClient()的模式对控制台同样适用——在控制台程序中,你只需先Host.CreateApplicationBuilder或WebApplication.CreateBuilder注册服务,再从 DI 容器取出IHttpClientFactory使用。
第二步:命名客户端,预配置请求头
仓库 httpclientfactory-2 展示了「命名客户端」:给客户端起一个名字,并集中配置默认请求头等参数:
var builder = WebApplication.CreateBuilder(); builder.Services.AddHttpClient("rss", c => { // Configure your http client here c.DefaultRequestHeaders.Add("Accept", "application/rss+xml"); }); var app = builder.Build(); app.Run(async context => { var httpClient = context.RequestServices.GetService<IHttpClientFactory>(); var client = httpClient.CreateClient("rss"); // 使用预配置的命名客户端 var result = await client.GetStringAsync("http://scripting.com/rss.xml"); ... });对于菜谱后端,你可以定义一个名为"recipes"的命名客户端,统一设置BaseAddress = "http://localhost:5000"与Accept: application/json,之后所有请求都从这一个配置源派生,避免在每处调用点重复写 URL 前缀。
第三步:类型化客户端,把 HTTP 封装成服务
命名客户端仍需要你手写CreateClient与 URL 拼接。仓库 httpclientfactory-3 展示了更面向对象的方式——类型化客户端(Typed Client):
var builder = WebApplication.CreateBuilder(); builder.Services.AddTransient<RssReader>(); builder.Services.AddHttpClient<RssReader>(); // 由 DI 把 HttpClient 注入进 RssReader var app = builder.Build(); app.Run(async context => { var rss = context.RequestServices.GetService<RssReader>(); var result = await rss.Get("http://scripting.com/rss.xml"); ... }); public class RssReader { readonly HttpClient _client; public RssReader(HttpClient client) { _client = client; } public Task<string> Get(string url) => _client.GetStringAsync(url); }把它映射到菜谱场景:创建一个RecipeService,把GetAllRecipes()、CreateRecipe(Recipe)等 API 调用全部收进类内,控制台的主流程只面向RecipeService编程,完全不感知 HTTP 细节:
public class RecipeService { private readonly HttpClient _client; public RecipeService(HttpClient client) { _client = client; _client.BaseAddress = new Uri("http://localhost:5000"); } public async Task<List<Recipe>> GetAllAsync() => await _client.GetFromJsonAsync<List<Recipe>>("/recipes") ?? []; public async Task<Recipe?> CreateAsync(Recipe recipe) { var response = await _client.PostAsJsonAsync("/recipes", recipe); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync<Recipe>(); } }这里用到的GetFromJsonAsync/PostAsJsonAsync/ReadFromJsonAsync是System.Net.Http.Json提供的扩展方法,会自动完成 JSON 序列化与反序列化,正好对上练习「用 JSON 通信」的要求。
第四步:接口化,解耦实现
仓库 httpclientfactory-4 进一步把类型化客户端背后的实现类替换为接口注入:
builder.Services.AddTransient<RssReader>(); builder.Services.AddHttpClient<IRss, RssReader>(); // 向接口注入实现 public interface IRss { Task<string> Get(string url); } public class RssReader : IRss { readonly HttpClient _client; public RssReader(HttpClient client) => _client = client; public Task<string> Get(string url) => _client.GetStringAsync(url); }对应菜谱控制台,你可以定义IRecipeService接口,再让RecipeService实现它。这样控制台程序依赖的是抽象接口,后端地址变更、通信方式调整时只需改动一处实现,也便于未来编写针对IRecipeService的单元测试(仓库 testing/nunit-1 展示了 .NET 测试工程的组织方式)。
JSON 数据契约:菜谱模型如何在前后端间流动
「Use JSON to communicate data between the backend and the console」是本练习的硬性要求,JSON 是整个系统的数据契约。来自 exercise-1 的菜谱实体定义如下:
- Title(标题)
- Ingredients(配料)
- Instructions(做法说明)
- Categories(一个或多个分类)
在前后端各写一份结构完全一致的 C# 类型(如record Recipe(string Title, string[] Ingredients, string Instructions, string[] Categories)),即可保证序列化后的 JSON 形状一致。后端返回的典型载荷形如:
{ "title": "Nasi Goreng", "ingredients": ["rice", "egg", "soy sauce"], "instructions": "Fry the rice with egg and soy sauce.", "categories": ["indonesian", "main"] }需要留意两侧的契约一致性:Ingredients与Categories用数组表达「多值」,Instructions用字符串表达「长文本」,前后端必须保持相同的属性名(或统一配置JsonSerializerOptions的命名策略),否则反序列化会得到空对象。
HTTP 语义与 REST API 设计要点
练习特别强调「Make sure you have read and understood how HTTP protocol works and how to design Web APIs」。把菜谱的增删改查映射为 REST 资源操作,是最直接的落地方式:
| 操作 | HTTP 方法 | 路径 | 语义 |
|---|---|---|---|
| 列出全部菜谱 | GET | /recipes | 幂等读取,返回 JSON 数组 |
| 查看单个菜谱 | GET | /recipes/{title} | 按标识读取,未找到返回404 |
| 新建菜谱 | POST | /recipes | 请求体携带菜谱 JSON,创建后返回201 Created |
| 编辑菜谱 | PUT | /recipes/{title} | 请求体携带更新后的完整菜谱 |
| 管理分类 | GET/POST/PUT | /categories | 分类作为独立资源维护 |
设计时把握三条原则:
- 动词交给 HTTP,路径只描述资源:不要出现
/recipes/create、/recipes/delete这类把动作写进路径的 URL,新建用POST、删除用DELETE; - 用状态码表达结果:成功创建返回
201 Created(对应Results.Created(...)),找不到资源返回404(对应Results.NotFound()),校验失败返回400;仓库 minimal-api/map 与 minimal-api/map-post 分别演示了Results.Json、Results.Ok、Results.Created的典型用法; - 载荷与模型解耦:客户端发送的 JSON 由
[FromBody]绑定为Recipe对象(见 minimal-api/map-post-2 的静态方法写法),后端校验通过后才落盘。
运行、验证与排错
推荐的联调方式是开两个终端:
# 终端一:启动后端 cd projects/minimal-api/map-post # 或你自己的 Recipe.Api 项目 dotnet run # 也可用 dotnet watch run 热重载 # 终端二:运行控制台客户端 cd path/to/your/Recipe.Console dotnet run验证路径有三层:
- 后端自测:用浏览器或
curl直接访问端点,确认GET /recipes返回 JSON、POST /recipes返回201; - 控制台自测:运行控制台,依次执行列出、新建、编辑操作,确认终端输出的内容与后端 JSON 文件中的内容一致;
- 边界检查:在控制台输入一个不存在的菜谱标题,确认后端返回
404且控制台能给出友好提示而非崩溃。
常见问题速查:
- 连接被拒绝:多为后端未启动,或控制台里
BaseAddress端口写错; - 返回 JSON 但反序列化出空对象:前后端属性名不一致,检查
JsonSerializerOptions命名策略是否统一; - POST 后数据没落盘:确认写文件逻辑放在后端(业务逻辑唯一归属),且每次变更后都调用序列化写入;
- 请求头不匹配:后端期望
Content-Type: application/json,客户端用PostAsJsonAsync时已自动设置,手写HttpClient.PostAsync时需手动补充。
延伸:与后续练习的衔接
本练习是整个 Pathway 1 的关键枢纽,后续练习全部建立在「JSON 后端」之上:
- exercise-3:保留 JSON 后端,新增基于 Razor Pages 的 HTML 界面,仍通过 HTTP Client 连接后端;
- exercise-4:后端调整 CORS 设置,前端改用 Fetch API + Alpine.js 的单页界面;
- exercise-5:把 JSON 后端替换为 gRPC 后端;
- exercise-6:为后端加入 JWT 认证并实现令牌刷新。
也就是说,只要在本练习中把「后端提供干净 REST API、客户端只走 HTTP + JSON」的边界划清楚,后续所有 UI 形态(Razor Pages、SPA、gRPC、JWT)都只是在替换这个边界的某一侧实现。本练习产出的Recipe数据模型与 REST 端点设计,将作为整条学习路径的公共地基被反复复用。
小结
本练习的核心交付物是一份可运行的「REST 后端 + 控制台客户端」两份程序:后端用 ASP.NET Core Minimal API(MapGet/MapPost/Results.Json,参考 projects/minimal-api)承载全部业务与 JSON 持久化;控制台通过IHttpClientFactory(参考 projects/httpclientfactory 的四个递进示例,从基础CreateClient到命名客户端、类型化客户端、接口化注入)发起 HTTP 调用并解析 JSON 响应。完成它,你就完成了从「单体控制台程序」到「前后端分离的分布式雏形」的第一次真实跨越。
- 示例工程
【免费下载链接】practical-aspnetcore
Practical samples of ASP.NET Core 11, 10, 9, 8.0, 7.0, 6.0, 5.0, 3.1, 2.2, and 2.1,projects you can use. Readme contains explanations on all projects.
相关推荐
Stable Diffusion社区精选案例:探索文本到图像生成技术的无限创意可能
Stable Diffusion社区精选案例:探索文本到图像生成技术的无限创意可能 Stable Diffusion作为一款强大的文本到图像生成工具,正在全球范
使用 ASP.NET Core 与 SQL Server FOR JSON 构建 ReactJS 评论应用:sql-server-samples 中的 JSON 前后端集成实战
使用 ASP.NET Core 与 SQL Server FOR JSON 构建 ReactJS 评论应用:sql server samples 中的 JSON
示例工程数据库教程后端如何轻松安装Linux Wacom驱动:让你的数位板在Linux系统完美工作✨
如何轻松安装Linux Wacom驱动:让你的数位板在Linux系统完美工作✨ 如果你是Linux用户,同时拥有Wacom数位板,那么 Linux Wacom驱
低代码前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考