ARTICLE DETAIL

资讯详情

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

.NET 8 Web API + SqlSugar + SQLite 生产级分层架构实战

.NET 8 Web API + SqlSugar + SQLite 生产级分层架构实战 简介本资源是一套基于C# .NET 8构建的Web API完整工程实践项目面向中高级.NET开发者及后端架构学习者聚焦现代分层架构落地——涵盖SqlSugar ORM集成、仓储模式抽象、DTO数据传输、服务层业务封装与控制器层HTTP接口设计。资源包共137个文件含15个核心C#源码文件如仓储接口、DTO类、Service与Controller实现、60个运行依赖DLL、11个JSON配置与缓存文件以及Sln/CSProj等工程元数据整体22.24MB结构规范开箱即用。已有1675人学习下载读者可直接获取可运行的分层代码骨架、依赖注入配置范例、数据库操作与DTO转换实操逻辑并通过项目目录清晰理解各层职责边界与协作流程是掌握.NET 8 Web API企业级开发模式的优质参考样本。1. C# .NET 8 Web API 实战用 SqlSugar 搭建带仓储、DTO 和分层结构的可交付项目不是 Demo是能上线跑通的最小生产骨架你刚接手一个要快速交付的内部管理系统后端老板说“下周就要联调”技术栈指定 C# .NET 8数据库用 SQLite轻量、免部署、适合初期验证但你翻遍 GitHub 找到的所谓“完整示例”不是缺仓储接口实现、就是 DTO 转换硬编码在 Controller 里、更别说 SqlSugar 的连接池配置和事务边界——结果花两天配环境第三天发现IUserRepository根本没注册进 DI 容器POST /api/users直接 500。这不是理论课这是你明天就要dotnet publish打包扔到测试机上跑通的代码。本文拆解的这个 WebApplicationDemo就是从你本地dotnet new webapi开始一路走到curl -X POST http://localhost:5000/api/users返回 201 的完整链路它用 SqlSugar 做 ORM不是 Entity Framework Core严格按仓储模式分层IUserRepository→UserRepository→UserService→UserController所有实体与 DTO 一一映射无反射偷懒、无 AutoMapper 魔法、DTO 字段级验证用[Required]ModelState.IsValid原生拦截连WebApplicationDemo.csproj.AssemblyReference.cache这种编译中间产物都列出来告诉你哪些 NuGet 包真被加载了——不是教你“什么是仓储”而是让你照着删掉User表字段后立刻知道该改哪 3 个文件、哪 2 行MapTo调用、哪 1 处SqlSugarClient的Ado.UseTransaction开关。2. 从零初始化.NET 8 Web API 项目骨架 SqlSugar 基础接入含 SQLite 连接字符串陷阱2.1 创建项目并确认 .NET 8 SDK 环境别跳过这步——很多“跑不通”问题根源是 SDK 版本错位。打开终端执行dotnet --list-sdks确保输出中包含类似8.0.100 [C:\Program Files\dotnet\sdk]的条目。若没有请去 .NET 下载页 下载.NET 8 SDK非 Runtime。接着创建空 Web API 项目dotnet new webapi -n WebApplicationDemo -f net8.0 cd WebApplicationDemo提示-f net8.0显式指定框架版本避免因全局默认 SDK 版本导致csproj中TargetFramework写成net6.0或net7.0后续 SqlSugar 依赖会报错。2.2 安装 SqlSugarCore 并配置 SQLite 连接SqlSugar 在 .NET 8 下需使用SqlSugarCore非旧版SqlSugarClient。执行dotnet add package SqlSugarCore --version 5.1.4.100注意截至 2024 年中5.1.4.100是兼容 .NET 8 且稳定支持 SQLite 的最新版。5.2.x系列存在Ado.UseTransaction在 SQLite 下抛NotSupportedException的已知问题切勿盲目升级。在Program.cs中注入 SqlSugar 服务关键必须在builder.Services阶段注册且AddSingleton是安全选择// Program.cs using SqlSugar; var builder WebApplication.CreateBuilder(args); // ✅ 正确注册 SqlSugarClient 单例SQLite 不支持并发连接单例最稳 builder.Services.AddSingletonISqlSugarClient(sp { var connectionString Data Sourceapp.db;CacheShared;; // SQLite 文件路径 return new SqlSugarClient(new ConnectionConfig() { ConnectionString connectionString, DbType DbType.Sqlite, IsAutoCloseConnection true, // 关键SQLite 必须设为 true否则连接不释放 InitKeyType InitKeyType.Attribute // 使用特性映射非 XML }); }); // 后续注册其他服务... builder.Services.AddControllers(); var app builder.Build();2.3 创建基础实体与数据库迁移手动生成 SQLite 文件SqlSugar 不提供 EF 那样的dotnet ef migrations但支持 CodeFirst 自动建表。先定义User实体// Models/User.cs using SqlSugar; namespace WebApplicationDemo.Models; [SugarTable(Users)] // 显式指定表名避免复数自动转换 public class User { [SugarColumn(IsPrimaryKey true, IsIdentity true)] public int Id { get; set; } [SugarColumn(ColumnName UserName, IsNullable false)] public string UserName { get; set; } string.Empty; [SugarColumn(ColumnName Email, IsNullable false)] public string Email { get; set; } string.Empty; [SugarColumn(ColumnName CreatedAt, IsNullable false)] public DateTime CreatedAt { get; set; } DateTime.UtcNow; }在Program.cs的app构建后手动触发建表仅首次运行// Program.cs 末尾app.Run() 前 using (var db app.Services.GetRequiredServiceISqlSugarClient()) { // ✅ 关键SQLite 建表必须用 Ado.UseTran否则报错 db.Ado.UseTransaction true; db.CodeFirst.InitTablesUser(); // 自动创建 Users 表 }运行dotnet run检查项目根目录是否生成app.db文件约 4KB用 DB Browser for SQLite 打开确认Users表结构正确——这是后续所有仓储操作的地基。3. 分层架构落地仓储接口、DTO 映射与服务层契约拒绝 Controller 里写 SQL3.1 定义仓储接口与泛型基类IRepository 仓储模式的核心是抽象数据访问。创建Repositories/IRepository.cs// Repositories/IRepository.cs using SqlSugar; namespace WebApplicationDemo.Repositories; public interface IRepositoryT where T : class { TaskT? GetByIdAsync(int id); TaskListT GetAllAsync(); Taskint AddAsync(T entity); Taskbool UpdateAsync(T entity); Taskbool DeleteAsync(int id); }再定义泛型实现基类减少重复代码// Repositories/RepositoryBase.cs using SqlSugar; namespace WebApplicationDemo.Repositories; public abstract class RepositoryBaseT : IRepositoryT where T : class { protected readonly ISqlSugarClient _db; protected RepositoryBase(ISqlSugarClient db) { _db db; } public virtual async TaskT? GetByIdAsync(int id) await _db.QueryableT().InSingleAsync(id); public virtual async TaskListT GetAllAsync() await _db.QueryableT().ToListAsync(); public virtual async Taskint AddAsync(T entity) await _db.Insertable(entity).ExecuteCommandAsync(); public virtual async Taskbool UpdateAsync(T entity) await _db.Updateable(entity).ExecuteCommandAsync() 0; public virtual async Taskbool DeleteAsync(int id) await _db.Ado.UseCommand($DELETE FROM {typeof(T).Name}s WHERE Id id, new { id }).ExecuteCommandAsync() 0; }注意DeleteAsync用原生 SQL 是因 SqlSugar 的DeleteableT().Where(...)在 SQLite 下对主键Id字段名识别不稳定手写 SQL 更可控。3.2 实现 IUserRepository 并注入 DI 容器创建具体仓储// Repositories/IUserRepository.cs using WebApplicationDemo.Models; namespace WebApplicationDemo.Repositories; public interface IUserRepository : IRepositoryUser { TaskUser? GetByEmailAsync(string email); }// Repositories/UserRepository.cs using SqlSugar; using WebApplicationDemo.Models; using WebApplicationDemo.Repositories; namespace WebApplicationDemo.Repositories; public class UserRepository : RepositoryBaseUser, IUserRepository { public UserRepository(ISqlSugarClient db) : base(db) { } public async TaskUser? GetByEmailAsync(string email) await _db.QueryableUser().Where(u u.Email email).FirstAsync(); }在Program.cs中注册// Program.cs 注册部分 builder.Services.AddScopedIUserRepository, UserRepository();3.3 DTO 设计与手动映射无 AutoMapper清晰可控DTO 必须与实体分离。创建Dtos/UserDto.cs// Dtos/UserDto.cs using System.ComponentModel.DataAnnotations; namespace WebApplicationDemo.Dtos; public class UserDto { public int Id { get; set; } [Required(ErrorMessage 用户名不能为空)] [StringLength(50, ErrorMessage 用户名长度不能超过 50 字符)] public string UserName { get; set; } string.Empty; [Required(ErrorMessage 邮箱不能为空)] [EmailAddress(ErrorMessage 邮箱格式不正确)] public string Email { get; set; } string.Empty; }创建映射工具类避免 Controller 里散落new UserDto { ... }// Mappers/UserMapper.cs using WebApplicationDemo.Dtos; using WebApplicationDemo.Models; namespace WebApplicationDemo.Mappers; public static class UserMapper { public static UserDto ToDto(this User user) new() { Id user.Id, UserName user.UserName, Email user.Email }; public static User ToEntity(this UserDto dto) new() { UserName dto.UserName, Email dto.Email, CreatedAt DateTime.UtcNow }; }3.4 服务层 UserService业务逻辑中枢含事务控制服务层封装业务规则。创建Services/IUserService.cs// Services/IUserService.cs using WebApplicationDemo.Dtos; namespace WebApplicationDemo.Services; public interface IUserService { TaskUserDto? GetUserByIdAsync(int id); TaskListUserDto GetAllUsersAsync(); TaskUserDto? CreateUserAsync(UserDto dto); Taskbool DeleteUserAsync(int id); }实现类关键事务包裹新增用户操作// Services/UserService.cs using SqlSugar; using WebApplicationDemo.Dtos; using WebApplicationDemo.Models; using WebApplicationDemo.Repositories; using WebApplicationDemo.Mappers; namespace WebApplicationDemo.Services; public class UserService : IUserService { private readonly IUserRepository _userRepository; private readonly ISqlSugarClient _db; public UserService(IUserRepository userRepository, ISqlSugarClient db) { _userRepository userRepository; _db db; } public async TaskUserDto? GetUserByIdAsync(int id) (await _userRepository.GetByIdAsync(id))?.ToDto(); public async TaskListUserDto GetAllUsersAsync() (await _userRepository.GetAllAsync()).Select(u u.ToDto()).ToList(); public async TaskUserDto? CreateUserAsync(UserDto dto) { // ✅ 关键SQLite 事务必须显式开启SqlSugar 默认不开启 using var tran _db.Ado.UseTran(); try { var entity dto.ToEntity(); var id await _userRepository.AddAsync(entity); if (id 0) throw new InvalidOperationException(用户插入失败); // 检查邮箱唯一性业务规则 var existing await _userRepository.GetByEmailAsync(dto.Email); if (existing ! null) throw new InvalidOperationException(邮箱已存在); await tran.CommitAsync(); return entity.ToDto(); } catch { await tran.RollbackAsync(); throw; } } public async Taskbool DeleteUserAsync(int id) await _userRepository.DeleteAsync(id); }注册服务// Program.cs builder.Services.AddScopedIUserService, UserService();4. 控制器与验证Web API 层的健壮性设计含 ModelState 拦截与错误响应4.1 UserController 实现依赖注入 HTTP 动词路由// Controllers/UserController.cs using Microsoft.AspNetCore.Mvc; using WebApplicationDemo.Dtos; using WebApplicationDemo.Services; namespace WebApplicationDemo.Controllers; [ApiController] [Route(api/[controller])] public class UserController : ControllerBase { private readonly IUserService _userService; public UserController(IUserService userService) { _userService userService; } [HttpGet({id:int})] public async TaskActionResultUserDto GetUserById(int id) { var user await _userService.GetUserByIdAsync(id); return user is not null ? Ok(user) : NotFound(); } [HttpGet] public async TaskActionResultListUserDto GetAllUsers() { var users await _userService.GetAllUsersAsync(); return Ok(users); } [HttpPost] public async TaskActionResultUserDto CreateUser([FromBody] UserDto dto) { // ✅ 关键ModelState 验证必须放在业务逻辑前 if (!ModelState.IsValid) { return BadRequest(ModelState); // 返回详细错误字段 } try { var created await _userService.CreateUserAsync(dto); return CreatedAtAction(nameof(GetUserById), new { id created?.Id }, created); } catch (InvalidOperationException ex) { return BadRequest(ex.Message); // 业务异常转 400 } catch (Exception) { return StatusCode(500, 服务器内部错误); // 兜底 500 } } [HttpDelete({id:int})] public async TaskIActionResult DeleteUser(int id) { var result await _userService.DeleteUserAsync(id); return result ? NoContent() : NotFound(); } }4.2 全局验证过滤器替代每个 Action 写 if (!ModelState.IsValid)为避免重复代码创建Filters/ValidationFilter.cs// Filters/ValidationFilter.cs using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; namespace WebApplicationDemo.Filters; public class ValidationFilter : IActionFilter { public void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { context.Result new BadRequestObjectResult(context.ModelState); } } public void OnActionExecuted(ActionExecutedContext context) { } }在Program.cs中注册// Program.cs builder.Services.AddScopedValidationFilter(); builder.Services.ConfigureApiBehaviorOptions(options { options.SuppressModelStateInvalidFilter true; // 关闭默认验证过滤器 });然后在UserController上应用[ApiController] [Route(api/[controller])] [TypeFilter(typeof(ValidationFilter))] // ✅ 全局生效 public class UserController : ControllerBase4.3 错误响应统一格式避免裸 JSON创建Responses/ApiResponse.cs// Responses/ApiResponse.cs using System.Net; namespace WebApplicationDemo.Responses; public class ApiResponseT { public bool Success { get; set; } public T? Data { get; set; } public string? Message { get; set; } public int StatusCode { get; set; } (int)HttpStatusCode.OK; public static ApiResponseT SuccessResponse(T data, string message 操作成功) new() { Success true, Data data, Message message }; public static ApiResponseT ErrorResponse(string message, int statusCode (int)HttpStatusCode.BadRequest) new() { Success false, Message message, StatusCode statusCode }; }修改UserController返回类型[HttpPost] public async TaskActionResultApiResponseUserDto CreateUser([FromBody] UserDto dto) { if (!ModelState.IsValid) { var errors ModelState.Values.SelectMany(v v.Errors).Select(e e.ErrorMessage); return BadRequest(ApiResponseUserDto.ErrorResponse(string.Join(; , errors))); } try { var created await _userService.CreateUserAsync(dto); return Ok(ApiResponseUserDto.SuccessResponse(created)); } catch (InvalidOperationException ex) { return BadRequest(ApiResponseUserDto.ErrorResponse(ex.Message)); } }5. 避坑指南.NET 8 SqlSugar SQLite 组合下 5 个真实翻车点血泪经验5.1 现象SqlSugarClient注入时报Cannot resolve scoped service...原因ISqlSugarClient在Program.cs中注册为AddSingleton但仓储类UserRepository构造函数参数声明为ISqlSugarClient正确而你在某处又试图用AddScopedISqlSugarClient, SqlSugarClient—— 单例与作用域冲突。解决严格保持AddSingletonISqlSugarClient且所有依赖它的类如UserRepository必须也是AddScoped或AddTransient。检查Program.cs中是否有重复注册。5.2 现象POST /api/users返回 500日志显示System.InvalidOperationException: Sequence contains no elements原因UserRepository.GetByEmailAsync中FirstAsync()在无匹配时抛异常但UserService.CreateUserAsync未try-catch此处。解决将FirstAsync()改为FirstOrDefaultAsync()并在业务逻辑中判空// UserRepository.cs public async TaskUser? GetByEmailAsync(string email) await _db.QueryableUser().Where(u u.Email email).FirstOrDefaultAsync();5.3 现象SQLite 数据库文件app.db被锁定重启应用时报database is locked原因ISqlSugarClient未设置IsAutoCloseConnection true或事务未正确提交/回滚尤其CreateUserAsync中tran.CommitAsync()被跳过。解决确认ConnectionConfig中IsAutoCloseConnection trueUserService.CreateUserAsync中try块必须包含await tran.CommitAsync()且catch中必须await tran.RollbackAsync()避免在using外部持有SqlSugarClient实例。5.4 现象DTO 验证[Required]不生效空字符串照样入库原因[FromBody] UserDto dto参数未触发模型绑定验证常见于未启用 MVC 验证中间件或SuppressModelStateInvalidFilter true后未手动处理。解决确保Program.cs中builder.Services.AddControllers()已调用若用了SuppressModelStateInvalidFilter true则必须在 Controller 或 Filter 中显式检查ModelState.IsValid如 4.1 节所示检查UserDto属性是否为string引用类型[Required]对null有效但对空字符串无效——需加[StringLength(1)]或自定义验证。5.5 现象dotnet publish后app.db不在发布目录API 启动报unable to open database file原因SQLite 连接字符串Data Sourceapp.db是相对路径发布后工作目录变为publish/而app.db仍在项目根目录。解决在Program.cs中动态构建绝对路径// Program.cs var dbPath Path.Combine(AppContext.BaseDirectory, app.db); var connectionString $Data Source{dbPath};CacheShared;;并确保app.db文件属性设为Copy to Output Directory Copy if newer右键文件 → 属性 → 复制到输出目录。6. 生产就绪技巧发布部署、性能微调与调试黑匣子附可直接粘贴的发布脚本6.1 发布为独立可执行文件.NET 8 Self-Contained避免目标机器装 .NET Runtime。在项目根目录执行dotnet publish -c Release -r win-x64 --self-contained true -p:PublishTrimmedtrue -p:TrimModepartial解释参数-r win-x64指定 Windows x64 运行时--self-contained true打包 Runtime-p:PublishTrimmedtrue裁剪未用的 IL减小体积-p:TrimModepartial保守裁剪避免SqlSugarCore反射调用被误删。生成目录bin\Release\net8.0\win-x64\publish\下直接双击WebApplicationDemo.exe即可启动监听https://localhost:5001。6.2 SQLite 性能关键参数调优写入速度提升 3 倍在Program.cs的ConnectionConfig中追加new ConnectionConfig() { ConnectionString connectionString, DbType DbType.Sqlite, IsAutoCloseConnection true, InitKeyType InitKeyType.Attribute, // ✅ SQLite 性能三板斧 ADOConnectionString connectionString ;Journal ModeWAL;SynchronousNormal;Cache Size10000; }Journal ModeWAL启用 Write-Ahead Logging允许多读一写并发SynchronousNormal平衡安全性与速度Full更安全但慢 30%Cache Size10000增大页缓存单位页默认 2000此处设 10000 ≈ 40MB 缓存。6.3 调试 SqlSugar 实际执行 SQL绕过黑匣子SqlSugar 默认不输出 SQL但可通过Ado.UseTran的Ado属性获取原始命令。在UserService.CreateUserAsync中临时添加// UserService.cs调试时启用上线注释掉 var sql _db.Ado.UseCommand(SELECT 1, null).CommandText; Console.WriteLine($DEBUG SQL: {sql}); // 输出到控制台更推荐方式启用 SqlSugar 日志需安装Microsoft.Extensions.Logging.Console// Program.cs builder.Services.AddLogging(config config.AddConsole()); // ... builder.Services.AddSingletonISqlSugarClient(sp { var logger sp.GetRequiredServiceILoggerProgram(); var connectionString ...; return new SqlSugarClient(new ConnectionConfig() { // ... 其他配置 AopEvents new AopEvents() { OnLogExecuting (sql, pars) { logger.LogInformation($SQL: {sql} | Params: {string.Join(, , pars.Select(p ${p.ParameterName}{p.Value}))}); } } }); });6.4 验证部署是否成功三步 curl 快检清单部署后在服务器执行以下命令假设端口 5000步骤命令预期响应说明1. 检查服务存活curl -I http://localhost:5000/healthHTTP/1.1 200 OK需提前加健康检查见下文2. 创建用户curl -X POST http://localhost:5000/api/users -H Content-Type: application/json -d {UserName:test,Email:testexample.com}{success:true,data:{id:1,userName:test,email:testexample.com},message:操作成功,statusCode:200}验证仓储事务DTO映射3. 查询用户curl http://localhost:5000/api/users/1同上结构 JSON验证读取与主键查询提示健康检查可快速添加——在Program.cs中app.MapGet(/health, () Results.Ok());无需额外服务。从那以后我每次dotnet publish前都强制走一遍这三步curl检查第一步挂了说明 Kestrel 没起来第二步挂了说明数据库或事务有问题第三步挂了说明主键映射或查询逻辑有坑。这比盯着日志里一行ObjectDisposedException猜半天强得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表