资讯动态

.NET 8 Web API源码设计:分层架构与性能优化实战

发布时间:2026/9/11 1:39:10 来源:尧图企业网站定制
简介这是一套基于 .NET 8 平台搭建的 Web API 项目设计源码面向中小型团队和个人开发者采用经典三层架构并融入简化版 DDD 模式整合 Sqlsugar、Autofac、Serilog、CSRedis 等主流组件帮助解决快速交付与后续扩展的平衡问题。资源压缩包共 68 个文件包含 52 个 C# 源码文件、4 个工程文件、3 个配置文件、Dockerfile 及许可证等整体约 573KB结构覆盖 Domain 领域层、Infrastructure 基础设施层、Api 接口层并配套实体映射、仓储、控制器、过滤器与事件订阅等通用模块。目前已有 753 人学习下载。具体来看代码中包含用户、支付、操作日志、微信工具、Excel 导入导出、Redis 事件存储等功能的实现既可作为新项目脚手架也能为现有系统重构提供参考适合有一定 C# 基础、希望掌握 .NET 8 Web API 分层设计的开发者深入学习。1. 基于.NET 8的Web API项目设计源码到底在设计什么dotnet new webapi一条命令能生成一个能跑的项目但生产环境的代码没人直接用那个裸模板。标题里的「设计源码」不是在讲如何写 Controller而是指动手前先决定骨架Controller 层、Service 层、仓储层和 DTO 之间的边界画在哪里认证与 Swagger 在中间件管线里的挂载顺序配置项放哪个文件才能在换环境时不触发重新编译。.NET 8 把最小 API、原生 AOT 和新的指标 API 带到了稳定版本这些能力会反过来影响你对源码组织方式的直觉。这篇文章写给准备用 .NET 8 搭后端的人从项目形态选型讲到分层落地再到可观测性配置每一步都给出能直接粘贴的命令和参数目标是让你的源码不是「能跑」而是「敢上线」。2. .NET 8下选对Web API项目形态最小API还是控制器2.1 为什么模板同时保留了两套写法.NET 8 的 webapi 模板默认包含Program.cs里的映射示例和Controllers目录这是因为微软在执行dotnet new webapi时提供--use-controllers参数来切换生成形态。模板这样做不是让你二选一而是承认两种风格都有真实场景Controller 适合资源式接口和团队约定管理最小 API 适合短接口和边缘服务。.NET 8 里最小 API 已经补齐了IEndpointFilter、模型绑定和从容器解析服务的[FromKeyedServices]此前「最小 API 不适合复杂项目」的说法不再成立。选型判断标准要看三点接口是否需要统一的路由前缀和版本号团队是否依赖[ApiController]隐式绑定和模型验证行为端点是否有大量可复用的横切逻辑。如果三个答案都是「是」用 Controller如果接口偏向网关转发、健康检查、状态查询最小 API 的样板代码更少。一个解决方案里混用两种形态完全合法只要路由表不冲突。2.2 用dotnet CLI创建骨架源码新建项目时我习惯一次性把项目结构定下来避免后面手动搬文件dotnet new webapi -n OrderApi --use-controllers -o src/OrderApi cd src/OrderApi dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Swashbuckle.AspNetCore dotnet new sln -n OrderApi dotnet sln add src/OrderApi参数含义如下-n指定项目名--use-controllers让模板生成 Controller 目录并保留AddControllers注册不带这个参数时模板生成的是最小 API 样例WeatherForecast。Swashbuckle.AspNetCore在 .NET 8 模板里已内置这里显式添加是为了保证版本与后续代码匹配。最后两步创建解决方案文件并把项目挂进去后续加Application、Infrastructure项目时直接dotnet sln add即可。模板生成后要做的第一件事是删掉WeatherForecastController和WeatherForecast.cs这两个文件是模板演示用的留在源码里会让新人对项目边界产生误解。接着检查Properties/launchSettings.json把applicationUrl改成你实际要用的端口否则团队里每个人都带着不同的本地端口提交代码联调时全是地址冲突。2.3 两种形态的选择标准与混用判断点最小 APIController路由组织分散在 Program.cs按控制器集中管理模型验证需要显式调用ValidationResult[ApiController]自动 400过滤器IEndpointFilterIAsyncActionFilter路由约束语法糖较少完整的[Route]、[HttpGet]约定适合场景BFF、Ping、短查询领域资源、版本化接口混用时要注意中间件注册顺序。常见做法是先把健康检查和短查询映射成最小 API再调用app.MapControllers()两者共用同一条管道var health app.MapGroup(/api/health); health.MapGet(/liveness, () Results.Ok(new { status up })); app.MapControllers();MapControllers()放在后面对最小 API 没有影响因为 Controller 路由表在UseRouting阶段已经构建完成注册顺序只影响端点在内存中的排列不影响匹配优先级。真正需要小心的是同一个 URL 前缀被两种风格同时命中比如MapGet(/api/orders/{id})和[HttpGet({id})]撞车运行时不会报错只会按路由模板优先级选择一个这种隐患很难排查所以混用时建议用MapGroup给最小 API 单独划前缀。3. 分层源码结构把业务逻辑从Controller里拆出来3.1 推荐的解决方案源码目录布局「设计源码」最直观的体现就是目录结构。一个可维护的 .NET 8 分层源码通常是这样OrderApi.sln ├─ src/ │ ├─ OrderApi.Api/ # Controller、Filter、Middleware、DTO 映射 │ ├─ OrderApi.Application/ # 用例、接口定义、DTO、验证器 │ ├─ OrderApi.Domain/ # 实体、枚举、领域异常 │ └─ OrderApi.Infrastructure/ # EF Core、仓储实现、外部客户端 └─ tests/ └─ OrderApi.Tests/ # xUnit 测试依赖方向是单向的Application引用DomainInfrastructure引用ApplicationApi引用Application和Infrastructure。一旦出现Infrastructure引用Api说明某个工具类或者常量放错了层。我把连接字符串和配置模型放在Api的appsettings.json但读取它们的选项类放在Application接口定义放在Application实现放在Infrastructure这样换数据库或换第三方服务时可以只替换Infrastructure项目。3.2 仓储模式与IQueryable的边界仓储接口放在Application实现放在Infrastructure这是最容易理解的拆分方式。接口设计上要警惕把IQueryable暴露出去一旦调用方拿到IQueryable他就能在 Controller 里写_repo.Orders().Where(x x.Price 100).ToListAsync()查询逻辑散落到各个 ControllerDbContext 的语义也被泄露到上层。public interface IOrderRepository { TaskOrder? GetByIdAsync(Guid id, CancellationToken ct); TaskIReadOnlyListOrder GetByCustomerAsync(Guid customerId, int page, int size, CancellationToken ct); void Add(Order order); Taskint SaveChangesAsync(CancellationToken ct); } // Infrastructure 项目实现 public sealed class OrderRepository(AppDbContext db) : IOrderRepository { public async TaskOrder? GetByIdAsync(Guid id, CancellationToken ct) await db.Orders.AsNoTracking() .FirstOrDefaultAsync(o o.Id id, ct); public async TaskIReadOnlyListOrder GetByCustomerAsync(Guid customerId, int page, int size, CancellationToken ct) await db.Orders.AsNoTracking() .Where(o o.CustomerId customerId) .OrderByDescending(o o.CreatedAt) .Skip((page - 1) * size) .Take(size) .ToListAsync(ct); }代码逻辑说明GetByIdAsync使用AsNoTracking()避免只读查询被上下文跟踪减少内存占用GetByCustomerAsync把分页参数放到接口方法签名里调用方无法再拼接Skip/Take组合避免超大页码导致全表扫描。返回类型用IReadOnlyList而不是IEnumerable明确告诉调用方数据已经落库。3.3 DTO与ApiContractController 永远不返回Order实体而是返回OrderResponse。实体里通常有导航属性和领域方法直接序列化会泄漏内部状态遇到循环引用还会让 Newtonsoft 或 System.Text.Json 抛异常。DTO 定义放在Application的Contracts目录下public sealed record OrderResponse( Guid Id, string OrderNo, decimal TotalAmount, string Status, DateTime CreatedAt); public sealed record CreateOrderRequest( Guid CustomerId, ListOrderItemRequest Items);选record是因为不可变性和基于值的相等性适合 DTOJSON 序列化时不需要写一堆 getter/setter。映射用Mapster或手写扩展方法都可以我倾向用手写扩展方法避免引入AutoMapper在 .NET 8 里额外的IQueryable.ProjectTo配置成本项目里 DTO 只有十几个时手写比配置快。3.4 依赖注入生命周期与源码可测试性生命周期实例数量典型对象Transient每次请求全新轻量服务、HttpClient 之外的短生命周期处理器Scoped同一次请求内共享DbContext、仓储、业务服务Singleton进程内唯一日志器、缓存、配置选项DbContext 必须是 Scoped这是 EF Core 自 .NET Core 时期就定死的约束。把仓储注册成 Scoped 并让它们共享同一个AppDbContext能保证一次 HTTP 请求里所有仓储操作落在同一个事务上下文SaveChangesAsync才能聚合多次修改一次性提交。Singleton 服务里不能注入 Scoped 服务否则DbContext被单例持有会伴随内存泄漏和连接池耗尽.NET 8 容器在这种情况下直接抛异常而不是静默容忍。测试层面Application项目不引用任何基础设施包所以单元测试可以只引用Application和Domain两个项目用NSubstitute模拟IOrderRepository就能覆盖用例逻辑。我在tests/OrderApi.Tests里按「用例名 返回值」组织测试方法例如CreateOrder_WithEmptyItems_ThrowsDomainException源码里看到测试名就能反推业务规则。4. 基础设施落地EF Core迁移、JWT认证与Swagger配置4.1 EF Core连接数据库并执行迁移Infrastructure项目里放AppDbContext然后通过OnModelCreating配置实体映射。连接字符串写在Api的appsettings.Development.json运行时通过AddDbContext注入builder.Services.AddDbContextAppDbContext(options options.UseSqlServer( builder.Configuration.GetConnectionString(DefaultConnection), sql sql.EnableRetryOnFailure(3)));EnableRetryOnFailure(3)表示连接失败时立即重试 3 次适用于 SQL Server 短暂故障切换的场景但重试次数不要调太大否则数据库宕机时请求会堆积在重试等待上。第一次拿到代码后执行迁移dotnet ef migrations add InitSchema -p src/OrderApi.Infrastructure -s src/OrderApi.Api dotnet ef database update -p src/OrderApi.Infrastructure -s src/OrderApi.Api-p指定包含 DbContext 的项目-s指定启动项目EF 工具会从启动项目的Program.cs解析连接字符串。团队多人协作时新成员拉取源码后应先跑dotnet ef database update不要直接复制数据库备份迁移文件会保证库结构一致。如果公司规定禁止开发人员直接访问生产库就把database update换成生成 SQL 脚本dotnet ef migrations script -p src/OrderApi.Infrastructure -s src/OrderApi.Api -o migration.sql4.2 JWT认证的注册与中间件顺序JWT 的典型配置参数如下表配置项推荐值说明Issuerhttps://auth.example.com签发者标识Audienceorder-api接收方标识SecurityKey至少 32 字节随机数放环境变量禁止提交到源码Expires60 分钟短 token 刷新 token注册代码var key Encoding.UTF8.GetBytes(builder.Configuration[Jwt:Key]!); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options { options.TokenValidationParameters new TokenValidationParameters { ValidateIssuer true, ValidateAudience true, ValidateLifetime true, ValidIssuer builder.Configuration[Jwt:Issuer], ValidAudience builder.Configuration[Jwt:Audience], IssuerSigningKey new SymmetricSecurityKey(key) }; }); var app builder.Build(); app.UseAuthentication(); // 先认证 app.UseAuthorization(); // 再授权中间件顺序是这套源码最容易踩的坑UseAuthentication必须出现在UseAuthorization之前否则[Authorize]在管道里拿不到已认证的User对象请求会在授权阶段被拒绝。JWT Key 不要硬编码在appsettings.json用环境变量或密钥管理服务注入Jwt:Key至少 32 字节不然SymmetricSecurityKey在 .NET 8 里会直接提示密钥强度不足。4.3 Swagger分组与鉴权按钮.NET 8 项目内置 Swashbuckle但默认 Swagger 看不到需要 JWT 的接口。在Program.cs里配置builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options { options.SwaggerDoc(v1, new OpenApiInfo { Title Order API, Version v1 }); options.AddSecurityDefinition(Bearer, new OpenApiSecurityScheme { In ParameterLocation.Header, Name Authorization, Type SecuritySchemeType.Http, Scheme bearer }); options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference new OpenApiReference { Type ReferenceType.SecurityScheme, Id Bearer } }, Array.Emptystring() } }); });SecuritySchemeType.Http配合Scheme bearer会生成带Bearer前缀的授权请求。注意AddEndpointsApiExplorer不能漏最小 API 的文档生成依赖它。开发环境中如果不想每次手动输入 token可以在 Swagger 的try it out里粘贴 JWTAddSecurityRequirement会把这个 token 加到每个受保护接口的请求头上。4.4 全局异常处理替换默认日志默认模板对未处理异常只返回空白 500前端拿到的不是结构化错误。加一个全局异常中间件放在 Pipeline 最前面app.UseExceptionHandler(errApp { errApp.Run(async context { var ex context.Features.GetIExceptionHandlerFeature()?.Error; context.Response.StatusCode StatusCodes.Status500InternalServerError; context.Response.ContentType application/problemjson; await context.Response.WriteAsJsonAsync(new { traceId Activity.Current?.Id ?? context.TraceIdentifier, message ex?.Message }); }); });application/problemjson是 RFC 7807 标准格式前端可以统一按问题详情解析错误。生产环境不要返回ex.Message换成固定文案并记录完整堆栈到日志。安全团队检查源码时最关注这里堆栈信息一旦泄露到响应体等于把攻击面画给调用方。5. 用.NET 8原生特性压榨这套Web API的性能5.1 用OpenTelemetry定位真实耗时.NET 8 内置的System.Diagnostics.Metrics能让 API 源码直接暴露请求延迟、活跃连接数和错误率不用额外装 agent。AddOpenTelemetry包可以接 Prometheus但最轻量的验证方式是加一个简单的指标端点var meter new Meter(OrderApi, 1.0.0); var requestCounter meter.CreateCounterint(orderapi.requests.total); app.Use(async (context, next) { requestCounter.Add(1, new KeyValuePairstring, object?(path, context.Request.Path)); await next(); });CreateCounterint的path标签会按路由拆分计数配合 Grafana 的 sum by 查询能看到每个端点的调用量。先部署这套再去看数据库慢查询别一上来就调 EF Core 参数。5.2 对纯计算端点启用AOT发布.NET 8 的发布命令支持PublishAot但 Web API 项目只有在不依赖反射、不使用动态 LINQ 时才能顺利裁剪。判断方法项目里如果出现ConfigurationManager、Reflection.Emit或dynamicAOT 大概率直接编译失败。适合 AOT 的是那些纯计算、输入输出 DTO 固定的端点。发布命令dotnet publish -c Release -r linux-x64 -p:PublishAottrue裁剪后启动时间能从秒级降到毫秒级容器镜像体积也明显变小。如果 AOT 编译报错别挣扎关掉PublishAot改用ReadyToRun编译收益虽然小一半但零风险。5.3 验证API健康状态与依赖连通性最后给源码加一个带依赖检查的健康端点builder.Services.AddHealthChecks() .AddDbContextCheckAppDbContext(db) .AddUrlGroup(new Uri(https://external-service.example.com), external); app.MapHealthChecks(/healthz);AddDbContextCheck默认执行SELECT 1验证数据库连通性AddUrlGroup请求外部服务。K8s 部署时把/healthz配成 liveness把依赖全部正常时才有返回 200 的单独端点配成 readiness避免流量打到数据库已断的后端。确认这两个指标落库后这套源码才算真正进入了可交付状态。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价