news 2026/11/2 3:03:08

菜谱控制台应用前后端分离改造:基于 ASP.NET Core Minimal API 与 IHttpClientFactory 的 JSON REST 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
菜谱控制台应用前后端分离改造:基于 ASP.NET Core Minimal API 与 IHttpClientFactory 的 JSON REST 实战
  • 示例工程

【免费下载链接】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.

项目地址:https://gitcode.com/gh_mirrors/pr/practical-aspnetcore
点击查看免费下载

本篇技术指南围绕 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传递数据。

这一拆分蕴含了三个重要工程观念:

  1. 关注点分离:数据持久化、业务规则集中放在服务端,客户端只负责发起请求、呈现结果;
  2. 接口契约化:控制台与后端之间以 REST 端点 + JSON 载荷为契约,任何一端都可以独立演进、替换甚至换语言重写;
  3. 真实 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分类作为独立资源维护

设计时把握三条原则:

  1. 动词交给 HTTP,路径只描述资源:不要出现/recipes/create、/recipes/delete这类把动作写进路径的 URL,新建用POST、删除用DELETE;
  2. 用状态码表达结果:成功创建返回201 Created(对应Results.Created(...)),找不到资源返回404(对应Results.NotFound()),校验失败返回400;仓库 minimal-api/map 与 minimal-api/map-post 分别演示了Results.Json、Results.Ok、Results.Created的典型用法;
  3. 载荷与模型解耦:客户端发送的 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

验证路径有三层:

  1. 后端自测:用浏览器或curl直接访问端点,确认GET /recipes返回 JSON、POST /recipes返回201;
  2. 控制台自测:运行控制台,依次执行列出、新建、编辑操作,确认终端输出的内容与后端 JSON 文件中的内容一致;
  3. 边界检查:在控制台输入一个不存在的菜谱标题,确认后端返回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.

项目地址:https://gitcode.com/gh_mirrors/pr/practical-aspnetcore
点击查看免费下载

相关推荐

上一篇:TrollInstallerX:iOS 14-16.6.1 TrollStore安装神器的高效部署指南
下一篇:三维场景智能对齐:Scan2CAD实现扫描数据与CAD模型精准匹配的7个核心方法

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 3:03:04

27. 数据产品-数据治理-9. DAMA 模型体系全景复盘实战推演

文章目录前言一、数据治理核心三问&#xff1a;所有企业数字化的底层必修课1. 为什么做&#xff1a;数据治理是数字化的前置地基&#xff0c;而非锦上添花2. 怎么做&#xff1a;治理定规则&#xff0c;管理做落地&#xff0c;双向迭代闭环3. 做什么&#xff1a;12大知识领域打底…

作者头像 李华
网站建设 2026/10/24 7:29:35

AIoT猫狗识别落地指南:喂食器、摄像头与智能门的实战解析

做AIoT落地这几年&#xff0c;猫狗识别一直是个绕不开的品类。智能喂食器要区分“来吃饭的是不是自家主子”&#xff0c;智能摄像头要在猫狗靠近时精准抓拍并过滤掉误报&#xff0c;智能门更得在“放自家猫进门”和“拦住流浪猫”之间快速做决定。这几个场景听上去都是“识别猫…

作者头像 李华
网站建设 2026/10/25 0:43:42

Flutter跨平台鸿蒙开发实战:情绪盲盒APP经验分享

1. 项目背景与核心价值去年接触鸿蒙开发时&#xff0c;发现很多开发者都在纠结一个问题&#xff1a;如何用最小成本同时覆盖鸿蒙和其他主流移动平台。正好团队需要开发一款心理健康类应用&#xff0c;我们决定尝试用Flutter框架来实现跨平台鸿蒙开发&#xff0c;最终完成了这款…

作者头像 李华
网站建设 2026/10/27 1:03:00

SpringBoot+EasyExcel实现高效Excel导入方案

1. 项目背景与核心价值最近在重构公司财务系统时&#xff0c;遇到了一个棘手问题&#xff1a;业务部门每天需要导入上百份结构复杂的Excel报表&#xff0c;这些文件包含多级表头、动态列和跨sheet关联数据。传统POI解析方式需要为每种模板编写大量重复代码&#xff0c;维护成本…

作者头像 李华
网站建设 2026/10/24 5:30:39

智能体记忆体系实战:知识库、长期记忆与工作记忆分层设计

你有没有遇到过这种尴尬局面&#xff1a;智能体Demo明明调通了&#xff0c;结果换一个人来对话&#xff0c;它连对方上次说过什么都想不起来。你跟它说“按老方案来”&#xff0c;它反问“哪个老方案”。很多人第一反应是模型不够聪明&#xff0c;换个更强的模型&#xff0c;结…

作者头像 李华