
在 SQL Server 上构建 JSON 原生 REST APIsql-server-samples 中 ASP.NET Core Todo 服务全解析【免费下载链接】sql-server-samplesAzure Data SQL Samples - Official Microsoft GitHub Repository containing code samples for SQL Server, Azure SQL, Azure Synapse, and Azure SQL Edge项目地址: https://gitcode.com/gh_mirrors/sq/sql-server-samples本文以 sql-server-samples 仓库中 samples/features/json/todo-app/dotnet-rest-api 示例为骨架系统讲解如何利用 SQL Server 2016或更高版本与 Azure SQL Database 内建的 JSON 功能FOR JSON 与 OPENJSON在既有数据库表之上快速搭建 ASP.NET Core REST API。读完本文你将掌握用一条 FOR JSON 查询把关系表直接输出为 JSON 响应、用 OPENJSON 把 HTTP 请求体中的 JSON 反序列化写入数据库、以及如何通过注入式数据访问组件Belgrade.Sql.Client写出零 ORM 的 CRUD 服务。About this sample示例定位与技术要点本示例来自 sql-server-samples 仓库的 JSON 功能示例集完整示例索引见 samples/features/json/readme.md其中包含 Todo、Product Catalog、Comments 等多个基于同一思路的实现。本示例的核心信息如下适用版本SQL Server 2016或更高、Azure SQL Database关键技术SQL Server 2016 / Azure SQL Database 的 JSON 函数 ——FOR JSON与OPENJSON编程语言C#ASP.NET Core、T-SQL作者Jovan Popovic数据访问方式通过 Belgrade.Sql.Client 库以注入方式提供IQueryPipe查询流式管道与ICommand命令执行两个服务示例演示了一个非常务实的场景不引入 ORM、不引入 Repository 模式直接在既有 Todo 表上以最小的代码量完成 GET / POST / PUT / PATCH / DELETE 全套 CRUD 操作。JSON 序列化与反序列化全部由数据库引擎完成应用程序层几乎不写 JSON 处理代码。Before you begin运行前提软件前提SQL Server 2016或更高版本或一个 Azure SQL DatabaseVisual Studio 2015或更高版本带 ASP.NET Core RC2或更高支持也可以直接使用 .NET Core 命令行工具dotnetCLI。Azure 前提拥有可创建 Azure SQL Database 的订阅权限。需要说明的是项目工程文件 dotnet-rest-api.csproj 中目标框架为netcoreapp1.0;net46依赖包版本如 Microsoft.AspNetCore.Mvc 1.0.4、System.Data.SqlClient 4.8.6 等也对应 ASP.NET Core 1.0 时代的构建方式这是该示例发布时的历史快照。在现代环境中运行可能需要先升级项目到新版 .NET SDK 并更新包版本但其中 SQL/JSON 交互的核心思路完全适用于当前版本的 SQL Server。Run this sample从建表到跑通服务的完整步骤第一步创建并填充 Todo 表使用 SQL Server Management StudioSSMS或 SQL Server Data Tools 连接到 SQL Server 2016 或 Azure SQL 数据库执行仓库中的建表脚本 setup/setup.sql。该脚本先删除旧表再创建 Todo 表并插入三条初始数据DROP TABLE IF EXISTS Todo GO CREATE TABLE Todo ( id int IDENTITY PRIMARY KEY, title nvarchar(30) NOT NULL, description nvarchar(4000), completed bit, dueDate datetime2 default (dateadd(day, 3, getdate())) ) GO INSERT INTO Todo (title, description, completed, dueDate) VALUES (Install SQL Server 2016,Install RTM version of SQL Server 2016, 0, 2016-06-01), (Get new samples,Go to github and download new samples, 0, 2016-06-02), (Try new samples,Install new Management Studio to try samples, 0, 2016-06-02)这个表结构是整个示例的数据模型基准id为自增主键title必填description、completed、dueDate均可为空且dueDate默认值为当前日期加 3 天。后续所有 SQL 语句中的列名title、description、completed、dueDate都与这里的列定义严格对应。第二步打开项目并还原依赖包在 Visual Studio 2015 中打开根目录下的TodoRestWebApi.xproj工程文件在解决方案资源管理器中右键项目选择Restore Packages还原 NuGet 包也可以在应用根目录执行命令行dotnet restore从 dotnet-rest-api.csproj 可以看到示例核心依赖包括Belgrade.Sql.Client0.3.0数据访问管道、Microsoft.AspNetCore.Mvc1.0.4、Kestrel / IISIntegration 服务器组件、System.Data.SqlClient4.8.6以及 Configuration、Logging 系列扩展包。第三步配置连接字符串在appsettings.json或appsettings.development.json中添加连接字符串。仓库自带的 appsettings.json 中默认配置为{ Logging: { IncludeScopes: false, LogLevel: { Default: Debug, System: Information, Microsoft: Information } }, ConnectionStrings: { TodoDb: Server.\\SQLEXPRESS;DatabaseTodoDb;Integrated SecurityTrue } }若数据库托管在 Azure 上可将连接字符串替换为注意数据库名要与建表脚本使用的库名一致README 中示例使用Todo{ ConnectionStrings: { TodoDb: ServerSERVER.database.windows.net;DatabaseTodo;User IdUSER;PasswordPASSWORD } }连接字符串的读取逻辑位于 Startup.cs程序使用ConfigurationBuilder依次叠加appsettings.json可选的、支持热重载、appsettings.{EnvironmentName}.json与系统环境变量最终通过Configuration[ConnectionStrings:TodoDb]取出连接串。这也意味着你可以通过环境变量覆盖连接配置无需改动代码。第四步构建并运行 REST 服务构建方式任选其一IDE 中按CtrlShiftB、右键项目选择 Build、菜单 Build/Build Solution或在应用根目录执行dotnet build然后按F5或CtrlF5运行也可以直接执行dotnet run程序的入口在 Program.cs它使用 Kestrel 作为 Web 服务器并通过UseStartupStartup()装配应用。启动后可进行如下验证访问/api/Todo—— 返回 Todo 表全部数据组成的 JSON 数组访问/api/Todo/1—— 返回 id 为 1 的单条 Todo 记录 JSON发送POST、PUT、PATCH或DELETEHTTP 请求增删改 Todo 表中的数据。Sample details源码级拆解 REST API 如何与 JSON 无缝协作本节深入 Controllers/TodoController.cs 中每个 Action 的实现理解“关系表 ↔ JSON”双向转换是如何被数据库引擎完成的。依赖注入IQueryPipe 与 ICommand控制器通过构造函数注入两个服务TodoController.cs#L15-L19public TodoController(ICommand sqlCommand, IQueryPipe sqlPipe) { this.SqlCommand sqlCommand; this.SqlPipe sqlPipe; }这两个接口来自 Belgrade.Sql.Client 包。在 Startup.cs 的ConfigureServices中它们被注册为瞬态Transient服务每个请求都会创建新的QueryPipe/Command实例并携带各自的SqlConnectionstring ConnString Configuration[ConnectionStrings:TodoDb]; services.AddTransientIQueryPipe( _ new QueryPipe(new SqlConnection(ConnString))); services.AddTransientICommand( _ new Command(new SqlConnection(ConnString)));IQueryPipe.Stream(...)把查询结果直接流式写入 HTTP 响应体是“查询结果 → JSON 响应”的核心通道ICommand.ExecuteNonQuery(...)执行 INSERT / UPDATE / DELETE 等非查询命令。注意 README 的 Disclaimers 部分也明确示例刻意不采用 Repository 等分层模式依赖注入只是为了演示用途并非硬性要求——你可以轻松改造以适配自己的架构。GET 列表FOR JSON PATH一行出 JSON// GET api/Todo [HttpGet] public async Task Get() { await SqlPipe.Stream(select * from Todo FOR JSON PATH, Response.Body, []); }这里的精髓在于 SQL Server 2016 的FOR JSON PATH子句它把查询结果集的每一行自动序列化为 JSON 对象并打包成一个 JSON 数组。SqlPipe.Stream的第三个参数[]是“空结果时的默认输出”当表为空时响应体直接返回[]从而保证 HTTP 响应永远是合法的 JSON——这正是很多手写 JSON 拼接方案容易出错的地方。GET 单条WITHOUT_ARRAY_WRAPPER返回单个对象// GET api/Todo/5 [HttpGet({id})] public async Task Get(int id) { var cmd new SqlCommand(select * from Todo where Id id FOR JSON PATH, WITHOUT_ARRAY_WRAPPER); cmd.Parameters.AddWithValue(id, id); await SqlPipe.Stream(cmd, Response.Body, {}); }WITHOUT_ARRAY_WRAPPER让查询结果不再包裹为数组而是输出单个 JSON 对象配合where Id id的参数化查询天然规避 SQL 注入。同样地{}作为空结果时的默认回退值保证未找到记录时返回{}而不是空响应。POST 新增OPENJSON把请求体写入表// POST api/Todo [HttpPost] public async Task Post() { string todo new StreamReader(Request.Body).ReadToEnd(); var cmd new SqlCommand( insert into Todo select * from OPENJSON(todo) WITH( title nvarchar(30), description nvarchar(4000), completed bit, dueDate datetime2)); cmd.Parameters.AddWithValue(todo, todo); await SqlCommand.ExecuteNonQuery(cmd); }OPENJSON是FOR JSON的逆操作它把 JSON 文本解析成关系行集WITH子句定义输出列的名称、类型与映射关系未在WITH中列出的 JSON 属性会被忽略。因此客户端发送的 JSON 请求体例如{title:Learn JSON,description:...,completed:false,dueDate:2016-06-05}被整体作为todo参数传入后数据库端一次解析即可完成insert ... select * from OPENJSON(...)应用层无需任何反序列化代码。dueDate与completed的类型转换datetime2、bit也由数据库负责若 JSON 中类型不合法会在此处直接报错。PUT 全量更新以 JSON 覆盖行// PUT api/Todo/5 [HttpPut({id})] public async Task Put(int id) { string todo new StreamReader(Request.Body).ReadToEnd(); var cmd new SqlCommand( update Todo set title json.title, description json.description, completed json.completed, dueDate json.dueDate from OPENJSON( todo ) WITH( title nvarchar(30), description nvarchar(4000), completed bit, dueDate datetime2) AS json where Id id); cmd.Parameters.AddWithValue(id, id); cmd.Parameters.AddWithValue(todo, todo); await SqlCommand.ExecuteNonQuery(cmd); }PUT 语义为“全量替换”通过from OPENJSON(todo) WITH(...) AS json将 JSON 解析为名为json的派生表再与 Todo 表按Id id关联把各列整体覆盖为 JSON 中提供的值。PATCH 局部更新ISNULL实现按需更新// PATCH api/Todo [HttpPatch] public async Task Patch(int id) { string todo new StreamReader(Request.Body).ReadToEnd(); var cmd new SqlCommand( update Todo set title ISNULL(json.title, title), description ISNULL(json.description, description), completed ISNULL(json.completed, completed), dueDate ISNULL(json.dueDate, dueDate) from OPENJSON(todo) WITH( title nvarchar(30), description nvarchar(4000), completed bit, dueDate datetime2) AS json where Id id); cmd.Parameters.AddWithValue(id, id); cmd.Parameters.AddWithValue(todo, todo); await SqlCommand.ExecuteNonQuery(cmd); }PATCH 实现的是“局部更新”语义ISNULL(json.column, column)表示只有当请求 JSON 中提供了该字段时才更新否则保留原值。这样客户端只需发送需要修改的字段例如{completed:true}即可只翻转完成状态而不影响其他列。DELETE 删除// DELETE api/Todo/5 [HttpDelete({id})] public async Task Delete(int id) { var cmd new SqlCommand(delete Todo where Id id); cmd.Parameters.AddWithValue(id, id); await SqlCommand.ExecuteNonQuery(cmd); }删除操作同样使用参数化查询无任何 JSON 参与保持最小化实现。路由与整体流程控制器类标有[Route(api/[controller])]因此全部端点以/api/Todo为前缀各 Action 通过[HttpGet]、[HttpGet({id})]、[HttpPost]、[HttpPut({id})]、[HttpPatch]、[HttpDelete({id})]特性完成 REST 语义映射。整条请求链为HTTP 请求 → ASP.NET Core 路由与模型绑定 → 控制器 Action → Belgrade.Sql.Client 执行 T-SQL含 FOR JSON / OPENJSON→ 数据库引擎完成 JSON 编解码 → 响应流式写回客户端。姊妹实现Node.js/Express 4 版本对照仓库的 todo-app 目录下还提供了完全同构的 Node.js 实现nodejs-express4-rest-api/README.md用 Express 4 Tedious 完成同样的 SQL/JSON CRUD适合希望对比不同技术栈的读者。其路由 routes/todo.js 中SQL 语句与 C# 版本几乎一一对应列表req.sql(select * from todo for json path).into(res, [])单条req.sql(select * from todo where id id for json path, without_array_wrapper).param(id, req.params.id, TYPES.Int).into(res, {})新增 / 更新调用存储过程createTodo todo/updateTodo id, todo通过TYPES.NVarChar参数传入 JSON 请求体删除req.sql(delete from todo where id id).param(...)可以看到无论 C# 还是 Node.js“FOR JSON 输出 OPENJSON 输入”这一数据库端 JSON 能力都是完全共享的这正是该示例想传达的核心价值JSON 编解码下沉到数据库层后各语言后端只需关注 HTTP 语义与 SQL 语句本身。Node 版本使用 Tedious 时还可在连接配置中设置encrypt: true以适配 Azure SQL 的加密连接要求。Disclaimers 与注意事项本示例不是通用架构与开发模式的最佳实践指南它刻意保持最小代码量未使用 Repository 等模式内置的 ASP.NET Core 依赖注入机制也仅为演示非强制要求。代码可直接修改以适配你自己的应用架构。示例发布于 ASP.NET Core 1.0 时代工程文件为 .xproj依赖包版本偏低在现代 .NET SDK 上运行时需要迁移项目格式并升级 NuGet 包但 SQL/JSON 交互逻辑与 T-SQL 语句完全可复用。仓库 license.txt 表明示例基于 MIT 许可发布可自由参考与改造。Related Links本示例完整代码samples/features/json/todo-app/dotnet-rest-apiJSON 功能示例总览samples/features/json/readme.md同思路的 Product Catalog 与 Comments 示例见总览页链接更多 JSON 实战示例可参考仓库 samples/features/json 目录下的其他子项目Dapper-Orm、Entity-Framework、reactjs、angularjs、geo-json 等【免费下载链接】sql-server-samplesAzure Data SQL Samples - Official Microsoft GitHub Repository containing code samples for SQL Server, Azure SQL, Azure Synapse, and Azure SQL Edge项目地址: https://gitcode.com/gh_mirrors/sq/sql-server-samples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考