ARTICLE DETAIL

资讯详情

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

C# WebApi连接SQL Server实战:从建库到接口调试全流程

C# WebApi连接SQL Server实战:从建库到接口调试全流程 写一个能打开SQL Server并返回数据的WebApi是C#入门者绕不开的一道坎。我在实际带新人和回答社区提问时发现大多数人不是不会写SQL也不是不懂HTTP而是卡在怎么把这几件事串起来——项目模板选哪个、连接字符串放哪里、数据访问层怎么组织、接口写完怎么调试。这篇文章就把从建库到接口跑通的完整链路走一遍代码以简单、能跑通为优先适合做练习、课程设计和个人小工具真实项目还得在结构、异常处理和安全性上继续打磨。1. 这套组合的适用场景什么需求值得用WebApi SqlServer1.1 WebApi在这个架构里扮演什么角色很多人第一次接触WebApi时心里有个疑问我直接用WinForms连数据库不行吗为什么中间非要加一层WebApi答案是解耦。WinForms直接连数据库意味着每个客户端都要拥有数据库账号密码连接串一旦泄露整个库都暴露了。而且多个客户端直连同一个库连接数会迅速上升数据库压力很大。WebApi相当于在客户端和数据库之间加了一道门卫——客户端只发HTTP请求给门卫门卫去数据库取数据再把结果包装成JSON返回。这样数据库账号密码只存在于服务器端客户端完全不知道数据库的存在。另一个好处是跨平台。只要接口按HTTP标准写不管是WinForms、WPF、网页、手机App还是Python脚本都能通过同样的URL拿到数据。这就是C#上位机和webapi经常被同时提及的原因——工业上位机采集到的数据要展示给多个终端看中间放一个WebApi是最顺手的方案。1.2 练习代码和真实项目的分界线在哪里标题里写得很清楚代码适用于简单练习真实开发需自行完善这句话不是谦虚是实话。练习代码追求的是链路完整、思路可读把SqlConnection、Command、DataReader这几个核心对象用明白真实项目则要引入异步、依赖注入、仓储模式、日志、缓存、鉴权、分页、异常处理中间件等等。我用一张表把分界线画清楚方便你判断自己手头的代码处于哪个阶段维度练习代码阶段真实项目要求数据库访问ExecuteReader顺序读取异步方法、连接池复用、Dapper/EF Core连接串管理硬编码在appsettings.json环境变量、配置中心、密钥托管异常处理try-catch返回报错信息全局异常中间件统一状态码安全性无鉴权JWT/OAuth2、接口限流、SQL防注入数据校验前端/调用方自觉模型验证DataAnnotations FluentValidation日志靠Debug输出Serilog/NLog全链路追踪初学者直接上真实项目那套东西很容易被各种抽象概念淹没连最基本的数据到底怎么从数据库到前端都没搞清楚。所以本文先用一个最小可用的实现把主链路走通最后再告诉你要往哪些方向补课。2. 动手前的环境准备从SQL Server安装到建库建表2.1 开发环境清单我建议的最小环境配置如下Visual Studio 2022 Community版免费安装时勾选ASP.NET和Web开发工作负载SQL Server 2019或2022 Developer版免费非生产授权SQL Server Management StudioSSMS19.x用于管理数据库浏览器里能打开Swagger页面即可不强制装Postman如果你只是想跑通代码不打算做界面以上就够。如果电脑配置一般SQL Server Express版也能胜任练习Express的LocalDB甚至不需要单独安装服务但连接字符串写法略有不同后文会提到。有一个细节很多人栽过安装SQL Server时选了默认实例连接字符串里的服务器名就是localhost如果安装时指定了命名实例比如SQLEXPRESS服务器名要写成localhost\SQLEXPRESS。这个反斜杠在C#字符串里要写成\\否则编译直接报错后面我会给标准写法。2.2 建一个练习用的数据库和表在SSMS中新建查询执行下面的SQL脚本CREATE DATABASE DemoDb; GO USE DemoDb; GO CREATE TABLE dbo.Student ( Id INT IDENTITY(1,1) PRIMARY KEY, Name NVARCHAR(50) NOT NULL, Age INT NOT NULL, ClassName NVARCHAR(50) NULL, CreateTime DATETIME DEFAULT GETDATE() ); GO INSERT INTO dbo.Student (Name, Age, ClassName) VALUES (N张三, 20, N计算机1班), (N李四, 21, N计算机2班), (N王五, 22, N软件工程1班); GO我故意把Class设为可空、CreateTime给默认值就是为了让你在后面演示插入时哪些字段必须给值、哪些可以不给。这张表结构简单但五脏俱全主键自增、非空约束、默认值都有了。2.3 连接字符串的几种写法连接字符串是整套代码里最容易被忽略、又最容易出问题的地方。最常见的坑有几个服务器名写错、用户名密码错、数据库名拼错、没有启用SQL Server身份验证。标准的SQL Server身份验证连接串是这样的{ ConnectionStrings: { DefaultConnection: Serverlocalhost;DatabaseDemoDb;User Idsa;PasswordYourStrongPassword;TrustServerCertificateTrue; } }如果你用的是Windows身份验证把User Id和Password替换为Trusted_ConnectionTrue{ ConnectionStrings: { DefaultConnection: Serverlocalhost;DatabaseDemoDb;Trusted_ConnectionTrue;TrustServerCertificateTrue; } }TrustServerCertificateTrue这个参数在SQL Server 2022和较新版本里相当关键。新版驱动默认不信任自签名证书不加这个参数会报证书链是由不受信任的颁发机构颁发的。老教程里没有它是因为旧版驱动默认行为不同。还有一个人经常踩的坑用LocalDB时服务器名是(localdb)\MSSQLLocalDB注意这里不需要写数据库服务密码因为它跑在你当前用户权限下。练习阶段如果不想折腾账号权限配置LocalDB是最快的起步选择但要注意LocalDB是进程内数据库重启后服务可能不会自动启动需要手动执行sqllocaldb start。3. 创建WebApi项目从模板到项目结构说明3.1 项目模板选择与创建步骤打开Visual Studio 2022选择创建新项目在模板列表里找到ASP.NET Core Web API注意别选成ASP.NET Core Web应用模型视图控制器——那是有视图的MVC模板我们只需要纯接口。创建过程中有一个附加信息页面里面有几个选项容易让人犯迷糊框架选.NET 6或.NET 8。.NET 6已停止官方支持新项目建议.NET 8 LTS身份验证类型选无勾选配置HTTPS勾选使用控制器新版模板默认生成的是极简API写法选控制器更接近经典项目结构不勾选启用OpenAPI支持的话Swagger后面要手动加包练习阶段建议直接勾上创建完默认会带一个WeatherForecast示例控制器这个先留着别删等我们自己的接口跑通了再清理不然中途漏了哪步都不好定位问题。3.2 配置文件里放什么项目根目录的appsettings.json是我们的主战场。把连接字符串写进去示例控制器用不到它但后续数据访问层要读。{ Logging: { LogLevel: { Default: Information, Microsoft.AspNetCore: Warning } }, AllowedHosts: *, ConnectionStrings: { DefaultConnection: Serverlocalhost;DatabaseDemoDb;Trusted_ConnectionTrue;TrustServerCertificateTrue; } }有些教程会把连接字符串直接写在代码里我不建议这么干因为项目发布后改连接串要重新编译而写在配置文件里只需要改appsettings.json再重启服务即可。真实项目里这个文件还可能被环境变量覆盖养成习惯从第一天刻意练习配置与代码分离会让你受益很多。3.3 Program.cs的核心作用创建完项目默认的Program.cs是这样的var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();这个文件很多人不知道每一行在干嘛。简单说builder.Services这一段是往服务容器里注册各种组件后面控制器的构造函数才能要什么就给什么app.UseXXX这一段是搭建HTTP请求的处理管道顺序不能乱——比如UseAuthorization必须在MapControllers之前。我们暂时不需要往Program.cs里加自定义服务注册.NET 8自带的AddControllers已经够用。等到了第八章我会演示怎么把数据访问类注册进去让控制器通过构造函数拿到它那才是真实项目惯用的依赖注入写法。4. 数据访问层连接、查询、增删改的完整代码4.1 用SqlConnection打开数据库现在进入本文的核心。我先创建一个专门的数据库帮助类把所有操作封装起来。新建一个文件夹叫Data在里面创建DbHelper.csusing Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; namespace WebApiDemo.Data { public class DbHelper { private readonly string _connectionString; public DbHelper(IConfiguration configuration) { _connectionString configuration.GetConnectionString(DefaultConnection); } public SqlConnection GetConnection() { var conn new SqlConnection(_connectionString); conn.Open(); return conn; } } }注意这里用的是Microsoft.Data.SqlClient不是老旧的System.Data.SqlClient。.NET 6之后官方都在推前者Visual Studio 2022创建的模板默认引用它不需要额外装包。如果项目报找不到命名空间在NuGet包管理器里搜索Microsoft.Data.SqlClient安装最新稳定版即可。4.2 查询单条记录SqlCommand SqlDataReader以按Id查学生为例完整的查询方法我写成这样public Student GetStudentById(int id) { using var conn GetConnection(); using var cmd conn.CreateCommand(); cmd.CommandText SELECT Id, Name, Age, ClassName, CreateTime FROM dbo.Student WHERE Id Id; cmd.Parameters.AddWithValue(Id, id); using var reader cmd.ExecuteReader(); if (reader.Read()) { return new Student { Id reader.GetInt32(0), Name reader.GetString(1), Age reader.GetInt32(2), ClassName reader.IsDBNull(3) ? null : reader.GetString(3), CreateTime reader.GetDateTime(4) }; } return null; }这里有两个细节值得反复琢磨。第一个是using关键字——SqlConnection和SqlCommand都实现了IDisposable用using包起来可以保证用完自动释放连接。数据库连接是很宝贵的资源释放不及时会把连接池耗尽表现出来就是服务运行一段时间后突然特别慢。这是初学者最容易犯的错误我今天写这个代码时特意没偷懒省略using。第二个细节是IsDBNull判断。数据库里ClassName是NULL时reader.GetString(3)会抛InvalidCastException。必须先判断字段是否为NULL再取值。这个坑在真实项目里几乎每天都会出现因为数据库设计要求很多字段允许为空。4.3 查询列表DataTable还是List查询多条记录最简单的方式是用DataTable拉住数据再遍历public ListStudent GetStudentList() { var result new ListStudent(); using var conn GetConnection(); using var cmd conn.CreateCommand(); cmd.CommandText SELECT Id, Name, Age, ClassName, CreateTime FROM dbo.Student ORDER BY Id; using var reader cmd.ExecuteReader(); while (reader.Read()) { result.Add(new Student { Id reader.GetInt32(0), Name reader.GetString(1), Age reader.GetInt32(2), ClassName reader.IsDBNull(3) ? null : reader.GetString(3), CreateTime reader.GetDateTime(4) }); } return result; }代码结构和单条查询几乎一样只是while循环。这里我想讲清楚一个原理ExecuteReader是流式读取它每次只从数据库拿一行数据到内存处理完再拿下一行。如果一次性把整个表都装进DataTable里大表场景下内存占用会很难看。但练习代码用DataTable或List 都没有性能顾虑以上这种手写映射的方式虽然啰嗦但逻辑透明方便你理解整个过程。4.4 新增、修改、删除ExecuteNonQuery查询用的是ExecuteReader增删改用ExecuteNonQuery后者返回受影响的行数public int AddStudent(Student student) { using var conn GetConnection(); using var cmd conn.CreateCommand(); cmd.CommandText INSERT INTO dbo.Student (Name, Age, ClassName) VALUES (Name, Age, ClassName); SELECT SCOPE_IDENTITY();; cmd.Parameters.AddWithValue(Name, student.Name); cmd.Parameters.AddWithValue(Age, student.Age); cmd.Parameters.AddWithValue(ClassName, (object)student.ClassName ?? DBNull.Value); var result cmd.ExecuteScalar(); return Convert.ToInt32(result); }这里有几个值得说道的点。一是AddWithValue的三个参数里第三个特别讲究——当ClassName是null时不能直接把null传给参数要写成DBNull.Value否则SQL Server不知道该往数据库里放NULL还是报错。二是末尾的SELECT SCOPE_IDENTITY()。插入后立刻把自增主键拿出来返回给调用方这在真实业务里太常见了比如插入订单后马上要用订单号去插明细。ExecuteScalar专门用来拿单行单列的结果。如果你只需要知道插入是否成功改成ExecuteNonQuery然后判断返回值大于0即可。修改和删除大同小异我直接给代码public int UpdateStudent(Student student) { using var conn GetConnection(); using var cmd conn.CreateCommand(); cmd.CommandText UPDATE dbo.Student SET Name Name, Age Age, ClassName ClassName WHERE Id Id; cmd.Parameters.AddWithValue(Id, student.Id); cmd.Parameters.AddWithValue(Name, student.Name); cmd.Parameters.AddWithValue(Age, student.Age); cmd.Parameters.AddWithValue(ClassName, (object)student.ClassName ?? DBNull.Value); return cmd.ExecuteNonQuery(); } public int DeleteStudent(int id) { using var conn GetConnection(); using var cmd conn.CreateCommand(); cmd.CommandText DELETE FROM dbo.Student WHERE Id Id; cmd.Parameters.AddWithValue(Id, id); return cmd.ExecuteNonQuery(); }4.5 参数化查询为什么是底线我把这一点单独拿出来讲因为它是从会写代码到写安全的代码最重要的一道分水岭。最原始的做法是字符串拼接SQL// 千万别这样写 cmd.CommandText SELECT * FROM dbo.Student WHERE Name name ;一旦name传入的是;DROP TABLE Student;--你的表就没了。这就是SQL注入。参数化查询的SQL语句里用Name占位再通过Parameters.AddWithValue把值单独传给驱动驱动会在协议层面把数据当成值而不是可执行的SQL片段发送给服务器。从我经验来看新手真正把这点刻进骨子里不是因为看了一篇教程而是因为遇到了一次真实的事故或者身边人的血泪教训。本文的练习代码从第一行起就坚持参数化请你务必养成这个习惯。5. 控制器层写接口、配路由、返回统一格式5.1 Controller的基本模板数据访问方法准备好后新建一个StudentController。右键Controllers文件夹添加-控制器-API控制器-空然后写using Microsoft.AspNetCore.Mvc; using WebApiDemo.Data; using WebApiDemo.Models; namespace WebApiDemo.Controllers { [Route(api/[controller])] [ApiController] public class StudentController : ControllerBase { private readonly DbHelper _db; public StudentController(DbHelper db) { _db db; } [HttpGet({id:int})] public ActionResultStudent GetById(int id) { var student _db.GetStudentById(id); if (student null) { return NotFound(new { message $未找到Id为{id}的学生 }); } return Ok(student); } } }注意看两点。构造函数里要的是DbHelper这是依赖注入的体现。但我们现在还没在Program.cs里注册DbHelper这时启动项目运行请求一进来就会报错提示无法解析类型为DbHelper的服务。你需要回头在Program.cs里加上builder.Services.AddScopedDbHelper();AddScoped的意思是每次HTTP请求范围内容器都复用同一个DbHelper实例。这是最贴合数据库连接使用模式的生命周期——连接本身在方法内用using管理而DbHelper这个能提供连接的工具人在整个请求期间存活即可。5.2 路由设计REST风格怎么定上面代码里[Route(api/[controller])]的[controller]会被替换成控制器名去掉Controller后缀即Student所以这个控制器的基地址是/api/Student。方法上[HttpGet({id:int})]的意思是GET /api/Student/3这样的请求会命中它参数id必须是整数如果传/api/Student/abc框架直接返回404连方法体都不会进。这个约束写法很实用避免你自己在方法里判断是不是数字。完整的REST接口设计我建议按这个对照表来定HTTP方法路由作用GET/api/Student获取列表GET/api/Student/{id}获取单条POST/api/Student新增PUT/api/Student/{id}完整更新DELETE/api/Student/{id}删除列表查询和新增方法补全[HttpGet] public ActionResultListStudent GetAll() { var list _db.GetStudentList(); return Ok(list); } [HttpPost] public ActionResultStudent Create(Student student) { var newId _db.AddStudent(student); student.Id newId; return CreatedAtAction(nameof(GetById), new { id newId }, student); }POST方法返回CreatedAtAction是有讲究的——HTTP协议规定201 Created应该带上Location头告诉客户端新资源在哪CreatedAtAction会帮你生成/api/Student/{newId}这样的地址。练习阶段可能不太在意这个细节但如果你后面用RestClient或者对接前端框架这个标准行为会省很多沟通成本。5.3 实体类Student的定义Models文件夹下新建Student.csnamespace WebApiDemo.Models { public class Student { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public string ClassName { get; set; } public DateTime CreateTime { get; set; } } } }用来让JSON序列化默认的驼峰命名会在输出时把Id变成idClassName变成className。前端如果是JavaScript收到的是小写开头属性正好符合JS习惯如果是其他C#客户端反序列化默认忽略大小写也能对上。这个细节不用刻意处理知道有这回事即可。6. 接口测试实战从Swagger到Postman再到WinForms6.1 用Swagger做第一轮冒烟测试按F5启动项目浏览器自动打开Swagger页面。这里有个小提醒如果打开报错HTTP 500先看控制台窗口的异常信息八成是连接字符串有问题。把Swagger当成第一个测试工具有三个好处每个接口方法直接列出来路由一目了然参数输入框自动生成不用记JSON格式点击Execute发请求状态码和响应体直接显示在GET /api/Student接口右边点Execute如果返回一个数组里面有三个学生对象说明从数据库到接口的主链路已经通了。这是整个练习最重要的一刻——数据库、数据访问层、控制器、路由、序列化全部串起来了。6.2 用Postman验证异常场景Swagger适合快速摸路但真实项目中Postman更常用原因是它能保存请求历史、构造更复杂的请求头、做环境变量切换。我建议你用Postman至少验证这几种情况GET /api/Student/1预期返回200和一条学生记录GET /api/Student/999预期返回404和自定义messagePOST /api/StudentBody里填JSON{name:赵六,age:23,className:测试班}预期返回201再GET一次列表看新增的数据是否在列POST请求有一个很经典的坑Body的JSON里属性名要和小写驼峰对上比如className不能写成ClassName。ASP.NET Core的模型绑定默认不区分大小写但为了稳妥请求体里的字段名建议按JSON标准小写开头写。如果你POST后进入Create方法但student对象是null或者属性全是null先检查Content-Type头是不是application/json。6.3 在WinForms中测试WebApi热搜词里winform 中 测试webapi接口出现频率很高确实很多练习者最终是winfrom上位机需要调接口。核心思路是使用HttpClient类发GET/POST请求然后反序列化JSON。using System.Net.Http; using System.Text.Json; public class ApiClient { private readonly HttpClient _http new HttpClient(); private readonly string _baseUrl http://localhost:5000; public async TaskListStudent GetStudentsAsync() { var json await _http.GetStringAsync(${_baseUrl}/api/Student); return JsonSerializer.DeserializeListStudent(json, new JsonSerializerOptions { PropertyNameCaseInsensitive true }); } public async TaskStudent AddStudentAsync(Student student) { var jsonContent new StringContent( JsonSerializer.Serialize(student), System.Text.Encoding.UTF8, application/json); var resp await _http.PostAsync(${_baseUrl}/api/Student, jsonContent); resp.EnsureSuccessStatusCode(); var resultJson await resp.Content.ReadAsStringAsync(); return JsonSerializer.DeserializeStudent(resultJson); } }上面的PropertyNameCaseInsensitive true就是用来解决大小写适配的服务端返回的是小写开头属性本地Student类是PascalCase开启忽略大小写之后能顺利映射。在WinForms按钮事件里调用时记得用async void或者async Task并在方法里await否则UI线程会被阻塞窗口看起来像死机一样。有一个测验过很多次的注意事项WebApi项目的launchSettings.json里默认配置了两个端口一个是http一个是https你发布部署或者局域网里跑时要用http那个端口因为自签名https证书在别的设备上默认不受信任请求大概率被拦。练习阶段建议直接用http地址省心。7. 常见报错与排查思路照着这个链路来定位7.1 SQL Server连接类报错这类报错很典型我把高频场景按出错位置列出来报错信息根本原因解决办法在建立与服务器的连接时出错。在连接到SQL Server时默认设置下SQL Server不允许进行远程连接服务器属性里未启用TCP/IP协议打开SQL Server配置管理器启用Named Pipes和TCP/IP协议用户sa登录失败SQL Server认证模式问题服务器属性-安全性-选中SQL Server和Windows身份验证模式并重新设置sa密码证书链是由不受信任的颁发机构颁发的证书信任问题连接串加TrustServerCertificateTrue在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误服务器名写错或服务未启动在SSMS里用同样的名字测试能否连上默认设置下SQL Server不允许进行远程连接SQL Server Browser服务未启动启动SQL Server Browser服务然后防火墙放行1433端口排错顺序很重要先确认SQL Server服务本身能连用SSMS测再看是不是代码连接串的问题。很多人在代码里反复折腾半天最后发现是数据库服务没启动这个低级错误我用三分钟才定位。7.2 编译和运行时异常练习项目最常见的编译错误有当前上下文中不存在名称xxx——多半是实体类没建或者using没引入类型或命名空间名称SqlClient不存在——NuGet包没装上缺少using指令或程序集引用——检查文件顶部using运行时最常见的报错是DataReader在using块之外使用或者用连接时遇到连接已关闭。这通常是因为你手动了conn.Open()又把using写在前面连接被提前释放。建议始终遵循本文示例的写法连接、命令、Reader都用using嵌套保证释放顺序是从内到外。7.3 Swagger能打开但接口请求失败Swagger页面本身能打开说明服务已经跑起来。请求失败要分情况看返回404路由不对。检查控制器的Route特性以及HTTP方法特性是否匹配返回400模型绑定失败。用Postman看响应体里ModelState的错误具体内容返回500方法体抛异常。在Visual Studio的调试输出里找根本原因建议在数据访问方法里先catch(Exception ex)然后Console.WriteLine(ex.ToString())别只写exc.Message堆栈信息才是定位的关键CORS跨域报错前端网页跨端口请求时会遇到。Program.cs里加Cors服务注册然后UseCors允许指定来源builder.Services.AddCors(options { options.AddPolicy(AllowAll, policy policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader()); }); // app.Build() 之后 app.UseCors(AllowAll);注意UseCors要放在UseAuthorization之前放在MapControllers之前这点顺序错了会报运行时警告。7.4 发布后接口404的经典原因发布出来的WebApi在本机能跑换台机器或者用IIS托管就404是IIS主机名和项目启动路径不匹配导致的。解决办法在Program.cs里设置BasePathvar builder WebApplication.CreateBuilder(new WebApplicationOptions { Args args, ContentRootPath AppContext.BaseDirectory });另外发布时注意选择框架依赖还是独立独立发布能免去目标机器装.NET运行时的麻烦。发布后先本机用cmd跑到项目目录执行exe验证一遍再挂到IIS上。这一步能帮你在发布环节少踩一半的坑。8. 从练习到真实开发这三个方向必须补课8.1 异步化别让数据库操作阻塞线程练习代码里所有数据库操作都是同步的真实项目在请求量上来后会出问题。ASP.NET Core的线程池线程数量有限每个线程处理一个请求如果线程在等数据库返回时被阻塞线程池很快就满了后续请求只能排队。改成异步并不难核心就是把数据库调用的API从同步换成Async版本using var conn GetConnection(); using var cmd conn.CreateCommand(); cmd.CommandText SELECT Id, Name, Age, ClassName, CreateTime FROM dbo.Student WHERE Id Id; cmd.Parameters.AddWithValue(Id, id); using var reader await cmd.ExecuteReaderAsync(); if (await reader.ReadAsync()) { return new Student { Id reader.GetInt32(0), Name reader.GetString(1), Age reader.GetInt32(2), ClassName reader.IsDBNull(3) ? null : reader.GetString(3), CreateTime reader.GetDateTime(4) }; }方法签名上把public Student改成public async Task 控制器里的调用加上await。第一次改可能会警惕Task 的返回类型多写几次就习惯了。8.2 引入ORM从写SQL到关注业务手写SqlCommand的缺点是每张表都要重复写映射代码表结构一变就要改好几处。真实项目里大多用Dapper或EF Core。Dapper是轻量级微ORM它不隐藏SQL只帮你把DataReader映射到实体using Dapper; public ListStudent GetStudentList() { using var conn GetConnection(); return conn.QueryStudent(SELECT Id, Name, Age, ClassName, CreateTime FROM dbo.Student).ToList(); }四五行就能替代前面十几行的Reader映射。EF Core则是重量级ORM你直接操作DbSet 框架负责生成SQL。从本文的手写SQL切换到Dapper几乎没有学习曲线我更推荐你先上Dapper把ORM解决什么问题想明白之后再按项目复杂度决定要不要上EF Core。8.3 安全与健壮性从能跑到可靠最后这点必须说透。练习代码能跑通不等于能上线。以下几个方向是真实项目的最低安全底线JWT鉴权接口不能裸奔至少登录接口不做鉴权其他接口带Token参数校验POST/PUT方法里给Student类加[Required]、[Range]特性让框架自动返回400全局异常处理注册一个ExceptionHandler中间件统一返回{code:500,message:服务器繁忙}而不是把堆栈信息直接抛给客户端日志记录至少要做到每个请求记录日志SQL执行超过阈值要告警密码绝不存明文使用PBKDF2或者BCrypt做哈希数据库泄露也不至于直接暴露用户密码我给你一个真实经验作为收尾我见过不少人拿着练习代码直接改改就部署到生产环境第一周没问题等到某个表数据量过十万、某个接口被脚本频繁调用时各种问题集中爆发——连接池耗尽、线程池被打满、SQL执行计划混乱。到那时候再补课成本极高。所以如果你打算把这个练习项目继续往前推进我的建议很明确先补异步和Dapper再加鉴权最后加日志与全局异常这一套走完你的代码就不是练习水平了。
返回列表