最近很多人在跟 .NET 10 Web API 的全套课程,方向是 Clean Architecture、EF Core,再加上 AI 能力接入。这套组合最值得关注的不是某个单点技术,而是把架构设计、数据持久化和 AI 服务串成一条完整链路的思路。适合正在做 .NET 后端开发、想从单体 Controller 堆代码转向分层架构,或者想给现有 Web API 接入大模型能力的开发者。下面按我实际跑完一轮 Demo 后的经验,把环境、分层、EF Core、AI 接入和踩坑点完整拆一遍。
1. 这类全栈课程真正值得学的东西,是把架构和 AI 组合成一条可落地的链路
很多人看到“Clean Architecture + EF Core + AI”第一反应是知识点太多,不知道该从哪入手。实际上这套组合的核心不是让你背分层理论,而是帮你解决一个非常具体的问题:当 Web API 开始接入 AI 能力时,代码结构怎么组织才不会乱。
1.1 为什么 Clean Architecture 在这里是基础
如果只是写一个简单的接口,Controller 里直接写业务逻辑也能跑。但一旦项目里同时存在数据库访问、第三方 AI 服务调用、请求参数校验、日志记录,Controller 就会越来越臃肿。Clean Architecture 的做法是把系统按依赖方向分层:Domain 放核心业务,Application 放用例逻辑,Infrastructure 放数据库和外部服务,WebAPI 只负责接收请求和返回响应。
这样做的好处是,AI 服务或者数据库实现未来都可以替换,业务层不需要跟着改。课程里把这一点作为主线,后面的 EF Core 和 AI 集成都是在为这条主线服务。
1.2 EF Core 和 AI 在这条链路里分别承担什么
EF Core 解决的是数据持久化问题。在 Clean Architecture 里,DbContext、实体映射、迁移脚本都属于 Infrastructure 层,Application 层只依赖抽象的仓储接口或查询接口。
AI 部分则分成两个维度:
- 开发过程中用 Copilot Agent 辅助写代码、测试、查报错;
- 业务功能中通过接口接入 Gemini 这类大模型服务。
这两个维度不能混为一谈。Copilot Agent 是开发工具,不进入运行时;Gemini 服务调用则是业务代码的一部分,需要按依赖方向封装。把这两件事分开,整条链路才清晰。
2. 环境准备:先把开发链跑通,再谈分层和 AI
我建议先把本地环境准备好,不要一上来就建解决方案。环境问题不解决,后面每一步都会卡。
2.1 开发机需要准备什么
最低配置和推荐配置可以看下面这张表:
| 项目 | 最低要求 | 推荐配置 |
|---|---|---|
| 操作系统 | Windows 10 / 11,或 macOS 13+ | 同左 |
| .NET SDK | .NET 10 SDK | .NET 10 SDK + 最新补丁 |
| IDE | Visual Studio 2022 或 VS Code | Visual Studio 2022 或 Rider |
| 数据库 | SQL Server LocalDB | SQL Server / PostgreSQL / SQLite |
| 内存 | 8 GB | 16 GB 以上 |
| 网络 | 能访问 NuGet 和 AI 服务 | 稳定网络,避免超时 |
这里有个细节:.NET 10 的某些功能在旧版 SDK 里没有,比如新增的 API 模板选项和默认配置行为。如果你用的是预览版或者刚发布的正式版,一定要确认 SDK 版本和 NuGet 包版本匹配。原始课程材料没有给出具体版本号,所以落地时先跑一次dotnet --info确认环境。
2.2 用命令行建出解决方案结构
我习惯用命令行创建项目,比在 IDE 里点模板更快,也更清楚每个项目的依赖关系。
dotnet new sln -n CleanArchDemo dotnet new webapi -n CleanArchDemo.WebApi dotnet new classlib -n CleanArchDemo.Domain dotnet new classlib -n CleanArchDemo.Application dotnet new classlib -n CleanArchDemo.Infrastructure dotnet sln add CleanArchDemo.WebApi CleanArchDemo.Domain CleanArchDemo.Application CleanArchDemo.Infrastructure这里要注意,.NET 10 的 Web API 模板默认会包含一些最小 API 示例和天气接口。建完项目后先把示例代码删掉,再开始写自己的代码,避免后面测试时被默认接口干扰。
2.3 项目引用关系是分层架构的关键
只建项目不设引用,等于没有分层。Clean Architecture 的核心是依赖方向必须从外向内:
cd CleanArchDemo.WebApi dotnet add reference ../CleanArchDemo.Application dotnet add reference ../CleanArchDemo.Infrastructure cd ../CleanArchDemo.Application dotnet add reference ../CleanArchDemo.Domain cd ../CleanArchDemo.Infrastructure dotnet add reference ../CleanArchDemo.Application dotnet add reference ../CleanArchDemo.Domain注意 WebApi 层可以同时引用 Application 和 Infrastructure,因为启动项目本身负责组装依赖注入。但 Application 层不能引用 Infrastructure 层,这是最容易搞反的地方。如果项目里有“循环依赖”的报错,先检查是不是引用方向错了。
3. Clean Architecture 分层:谁该放哪,依赖方向怎么控制
分层不是把文件放进不同文件夹就完事了,而是要让每一层只依赖它下面那层。下面按实际项目里的常见划分拆开说。
3.1 Domain 层只放核心业务实体
Domain 层在四个项目里处于最中心,它不应该引用任何其他项目。这里放的是业务实体和核心业务规则,比如订单、产品、用户这类概念,以及它们自身的行为。
namespace CleanArchDemo.Domain.Entities; public class Product { public Guid Id { get; set; } public string Name { get; set; } = string.Empty; public decimal Price { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; }在这个示例里,Product 只是一个简单的实体。实际项目中,领域实体可能包含状态流转、业务校验、事件发布等逻辑。关键判断标准是:这个对象不依赖数据库、不依赖框架、不依赖外部服务,它能独立存在。
3.2 Application 层处理用例
Application 层是业务用例的编排层。它定义接口,不关心接口怎么实现。比如要做一个“根据提示词生成内容”的功能,Application 层先定义一个IAiService接口,再定义一个用例类去调用这个接口。
using CleanArchDemo.Domain.Entities; namespace CleanArchDemo.Application.Interfaces; public interface IAiService { Task<string> CompleteAsync(string prompt, CancellationToken cancellationToken); }using CleanArchDemo.Application.Interfaces; namespace CleanArchDemo.Application.UseCases; public class GenerateContentUseCase { private readonly IAiService _aiService; public GenerateContentUseCase(IAiService aiService) { _aiService = aiService; } public async Task<string> ExecuteAsync(string prompt, CancellationToken cancellationToken) { return await _aiService.CompleteAsync(prompt, cancellationToken); } }为什么要这样做?因为控制反转之后,测试时可以替换成 Mock,未来换服务商时也不需要改业务代码。
3.3 Infrastructure 层放 EF Core 和外部服务
Infrastructure 层负责把 Application 层定义的接口变成实现。EF Core 的DbContext、实体配置、仓储实现、AI 服务的具体调用都放在这里。
这一层是整个架构里代码量最多的部分,也是最容易出现依赖混乱的地方。记住一个原则:Infrastructure 可以引用 Application 和 Domain,但 Application 永远不能引用 Infrastructure。
3.4 WebAPI 层只是入口
WebAPI 层包含 Controller、Program.cs、中间件和依赖注入注册。Controller 的逻辑应该尽可能薄,只做三件事:接收参数、调用 Application 层的用例、返回结果。
using CleanArchDemo.Application.UseCases; using Microsoft.AspNetCore.Mvc; namespace CleanArchDemo.WebApi.Controllers; [ApiController] [Route("api/[controller]")] public class AiController : ControllerBase { private readonly GenerateContentUseCase _useCase; public AiController(GenerateContentUseCase useCase) { _useCase = useCase; } [HttpPost("generate")] public async Task<IActionResult> Generate([FromBody] GenerateRequest request, CancellationToken cancellationToken) { var result = await _useCase.ExecuteAsync(request.Prompt, cancellationToken); return Ok(new { Result = result }); } }如果 Controller 里出现了数据库直接调用、AI 客户端实例化、日志手写拼接这些代码,说明分层已经失效了。
4. EF Core 在 Clean Architecture 中的实践
EF Core 本身不难,难的是放在哪、怎么迁移、仓储要不要包一层。这部分我多说几句。
4.1 DbContext 放在 Infrastructure 还是 Persistence
很多模板会单独建一个 Persistence 项目,把 DbContext 放进去。这个方案可以,但对大多数项目来说没有必要。直接在 Infrastructure 下建一个Persistence文件夹,里面放AppDbContext和实体配置就够了。项目多了反而增加维护成本。
using CleanArchDemo.Domain.Entities; using Microsoft.EntityFrameworkCore; namespace CleanArchDemo.Infrastructure.Persistence; public class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public DbSet<Product> Products => Set<Product>(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly); base.OnModelCreating(modelBuilder); } }ApplyConfigurationsFromAssembly会自动加载当前程序集里所有的IEntityTypeConfiguration实现,不用逐个注册,省事很多。
4.2 迁移操作的顺序
迁移是 EF Core 里比较容易出错的环节。主要是顺序问题。先安装工具,再添加迁移,最后更新数据库:
dotnet tool install --global dotnet-ef dotnet ef migrations add InitialCreate --project CleanArchDemo.Infrastructure --startup-project CleanArchDemo.WebApi dotnet ef database update --project CleanArchDemo.Infrastructure --startup-project CleanArchDemo.WebApi这里最容易踩的坑是:不指定--project和--startup-project,工具默认在当前目录找项目,结果要么报“没有找到 DbContext”,要么把迁移生成到了错误的项目。如果报错说找不到连接字符串,多半是startup-project没指向 WebApi 项目,因为连接字符串一般配置在启动项目里。
4.3 Repository 要不要包一层
关于 Repository 模式,我的建议是:简单查询不要包。直接用DbContext或者IDbContextFactory反而更清晰。EF Core 本身就是 UnitOfWork 和数据访问抽象的集合体,再包一层透明的 Repository 只会增加代码量,却没有带来实际好处。
什么时候才需要 Repository?当你有多个持久化方案需要切换,或者在测试中需要大量模拟数据访问层时,才值得包一层接口。如果只是做普通的 CRUD,Application 层通过构造函数注入AppDbContext或者一个更具体的仓储接口就够了。
有一个边界要注意:EF Core 支持不等于所有查询性能都好。比如连表查询、嵌套 Include、分页大表、N+1 查询,这些都需要单独检查生成的 SQL。不能因为框架帮你做了,就忽略查询效率。
5. AI 能力接入:Copilot Agent 在开发中的角色,以及 Gemini 的集成方式
这个课程的标题里有 Copilot Agent,又出现了 Gem,实际落地时通常就是两个方向:用 Copilot Agent 辅助开发,以及接入 Gemini 一类大模型服务。下面分别说。
5.1 Copilot Agent 是开发助手,不是业务组件
Copilot Agent 解决的问题是在写代码、查报错、补测试时减少重复劳动。比如我建完项目后会让它帮我生成一个符合 Clean Architecture 的 Product CRUD 例子,它能在几秒内给出 Domain、Application、Infrastructure 三层的基础代码。
但它不能替代你做架构决策。Agent 生成的代码经常只是“能用”,不一定符合你项目的具体规则。比如它会默认把业务校验写在 Controller 里,或者在 Application 层直接用 EF Core,这些都需要你按分层原则手动修正。我一般会让它先生成,再按架构规则逐层检查。
5.2 在 Application 层定义 AI 服务接口
接入 Gemini 这类模型时,先在 Application 层定义接口,然后在 Infrastructure 层实现真实请求。上面第 3 节里已经写了IAiService接口,下面看具体实现。
5.3 Infrastructure 层实现 Gemini 调用
using System.Net.Http.Json; using CleanArchDemo.Application.Interfaces; namespace CleanArchDemo.Infrastructure.Services; public class GeminiAiService : IAiService { private readonly HttpClient _httpClient; private readonly string _apiKey; private readonly string _endpoint; public GeminiAiService(HttpClient httpClient, IConfiguration configuration) { _httpClient = httpClient; _apiKey = configuration["AI:ApiKey"] ?? throw new InvalidOperationException("AI:ApiKey is not configured."); _endpoint = configuration["AI:Endpoint"] ?? "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent"; } public async Task<string> CompleteAsync(string prompt, CancellationToken cancellationToken) { var request = new { contents = new[] { new { parts = new[] { new { text = prompt } } } } }; var url = $"{_endpoint}?key={_apiKey}"; var response = await _httpClient.PostAsJsonAsync(url, request, cancellationToken); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync<GeminiResponse>(cancellationToken: cancellationToken); return result?.Candidates?.FirstOrDefault()?.Content?.Parts?.FirstOrDefault()?.Text ?? string.Empty; } } public class GeminiResponse { public List<GeminiCandidate>? Candidates { get; set; } } public class GeminiCandidate { public GeminiContent? Content { get; set; } } public class GeminiContent { public List<GeminiPart>? Parts { get; set; } } public class GeminiPart { public string? Text { get; set; } }这段代码里,API Key 从配置读取,而不是硬编码。还要用HttpClientFactory来管理 HttpClient,避免套接字耗尽。如果你要做流式输出,接口需要改成IAsyncEnumerable<string>,并在实现里处理 SSE 流。
这里最容易忽略的是超时和重试。AI 服务响应时间不稳定,单一超时配置可能不够。建议加一个重试策略,比如指数退避,同时设置合理的超时时间。具体的重试次数和超时时间要根据你的业务容忍度来定,没有统一答案。
6. 运行与验证:从单条请求到批量调用的判断标准
代码写完不是结束,能跑通并且稳定才是真正目的。这部分从最小链路开始,逐步扩展到更复杂的调用。
6.1 先跑最小链路
无论项目多复杂,第一步永远是启动 WebAPI,确认 Swagger 能打开,然后用一个最简单的接口验证。比如先调用传统的 Product 接口,再调用 AI 接口。这里有一个很实用的顺序:
- 启动项目,确认端口正常监听;
- 打开 Swagger 页面,确认接口枚举正常;
- 先用不带 AI 的普通接口测试数据库读写;
- 再测 AI 接口,输入一个短提示词,确认返回不为空。
不要一上来就测试长文本、多并发、流式输出。先把最小链路跑通,后面所有问题都更容易定位。
6.2 日志和异常处理
日志是整个链路里最容易被低估的部分。没有日志,你只能靠猜。建议在 Program.cs 里配置结构化日志,并且在 AI 服务实现里记录请求耗时和状态码。
builder.Services.AddHttpClient<GeminiAiService>(client => { client.Timeout = TimeSpan.FromSeconds(60); });在业务代码里,对 AI 服务的异常要单独处理。网络超时、API Key 失效、模型返回空内容、输入被内容审核拦截,这些都是不同的错误,不能混在一个 catch 里。
6.3 超时、并发、输出完整性
判断 AI 接口是否稳定,不能只看一次调用是否成功。我建议关注这几个指标:
| 指标 | 判断标准 | 排查方向 |
|---|---|---|
| 单次耗时 | 普通短文本应在 5 到 30 秒内 | 网络、模型大小、请求内容长度 |
| 成功率 | 连续 20 次调用,失败次数应为 0 | API Key、超时、限流 |
| 输出完整性 | 返回内容不截断、不丢失 | 流式处理是否正确、长度限制 |
| 并发稳定性 | 同时 5 个请求不报错 | HttpClient 管理、连接池、限流 |
如果你要做批量调用,比如一次处理 10 条文本,不要用并行度拉满的方式。先用SemaphoreSlim限制并发数,比如 3 到 5 个,然后逐条记录结果。批量任务失败时还要考虑重试和跳过,不能因为一条失败就中断整个队列。
7. 常见踩坑和排查顺序
最后这部分是我最想写的。很多问题看起来像框架或 AI 服务的问题,实际往往是环境、配置或输入格式的问题。
7.1 编译和运行时的高频问题
先列几个容易遇到的现象和排查方向:
| 现象 | 常见原因 | 排查顺序 |
|---|---|---|
| 迁移生成到错误项目 | 未指定--project | 先看命令参数,再看项目结构 |
| 连接字符串找不到 | 配置写在非启动项目 | 确认startup-project,确认环境变量 |
| 依赖注入报错 | 服务未注册或生命周期不匹配 | 看 Program.cs 注册代码,确认 AddScoped 还是 AddTransient |
| NuGet 包版本冲突 | 不同项目引用了不同 EF Core 版本 | 统一 SDK 版本和包版本 |
| API Key 为空 | 配置未加载或环境变量名不一致 | 检查 appsettings.json、launchSettings.json、环境变量 |
这里有个常见误区:构建时通过不代表运行时没有问题。E F Core 的查询可能在编译期完全正常,但运行时因为数据库表不存在、字段类型不匹配、迁移未应用而崩溃。所以每次改动实体后,优先跑迁移,再启动项目。
7.2 AI 接口调用的排查链路
AI 接口一旦报错,很多人第一反应是“模型是不是挂了”,实际上更多的原因是请求格式、认证或网络。
我一般按这个顺序排查:
- 先看报错信息里的状态码。401 是 Key 问题,429 是限流,500 是服务端问题;
- 再检查请求体格式。Gemini 的
contents结构不能写错,字段名大小写要严格; - 检查网络是否能正常访问目标地址。这里只判断你的服务器和目标服务之间的连通性,不涉及任何代理工具;
- 检查超时配置。如果 HttpClient 的 Timeout 设置过短,长文本请求会被提前中断;
- 最后看返回内容。有时候请求成功但内容为空,可能是输入提示词被过滤,或者模型返回了空 parts。
如果报错信息里有SSL或证书相关字样,先确认目标机器的证书链是否完整。这类问题通常跟代码逻辑无关。
7.3 我的建议
这套课程里最值得模仿的不是某个类怎么写,而是它的组织方式和验证节奏。我们先从最小案例跑起,再逐步加深,不要让 AI 功能绑架了整个架构设计。如果你只是学习,用 SQLite 加一个短文本示例就够了;如果要上生产,就需要把日志、超时、重试、配额管理、内容格式校验都提前设计好。
踩过几次之后我发现,很多问题不是框架能力不够,而是前置环境和输入材料没有处理干净。项目引用方向错了,连接字符串没有放对位置,API Key 没有生效,这些占了排查时间的一大部分。先把这些基础环节做扎实,Clean Architecture、EF Core 和 AI 集成这套方案才能真正给你带来长期收益。