1. 每天手工搭建WebAPI项目,到底浪费了多少时间
先抛一个我自己的数据:一个标准的企业级WebAPI项目,从新建解决方案开始,到把Swagger、JWT鉴权、EF Core、仓储模式、统一响应格式、日志、异常中间件一一配齐,熟练的话也要40分钟到1个小时。如果团队里每个人的习惯不一样,有人用Autofac、有人用内置DI,有人喜欢Controller直接写业务、有人坚持Service层,那代码风格更是五花八门,review的时候光是统一格式就得花不少精力。
这就是我做出Chet.WebAPI.Template.Generator的初衷。它不是一个框架,也不是一个运行时组件,而是一个项目生成器——一条命令,把一整套带好了基础架构、规范约束、常用中间件的WebAPI项目骨架给你生成出来。你拿到手之后,直接在这个基础上写业务代码就行。
这篇文章我会把工具的设计思路、使用方式、生成的项目结构、模板定制方法,以及我在开发和日常使用中踩过的坑,完整地写一遍。不管你是刚接触.NET的新手,还是被重复性搭建工作折磨了很久的老手,这篇文章应该都能给你一些可落地的参考。
先说明白这个工具能做什么:
- 基于dotnet CLI工作,跨平台可用,Windows、Linux、macOS都能跑
- 支持生成完整的分层解决方案,Controller层、Service层、Repository层、DTO、实体、扩展方法一应俱全
- 自动集成Swagger,并且按版本分组展示
- 自带了JWT认证的整套配置骨架,你只需要填入密钥和过期时间
- 统一响应格式、全局异常处理中间件、CORS策略、Serilog日志,这些都已经是"开箱即用"的状态
- 支持模板参数化,你可以通过编辑模板来定义自己的项目风格,团队规范可以直接固化进来
接下来我从实际使用的角度,把每一步拆开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 安装与第一条生成命令:从NuGet到解决方案落地
2.1 安装方式与前置条件
这个生成器以.NET Tool的形式分发,安装命令很简单:
bash复制dotnet tool install -g Chet.WebAPI.Template.Generator
安装之前确保本机已经装了.NET SDK(6.0或更高版本都可以)。你可以在终端里用下面的命令确认:
bash复制dotnet --version
如果显示的是类似8.0.100这样的版本号,说明SDK没问题。
装好之后,命令行工具名是chet-webapi。你可以直接输入这个命令查看帮助信息:
bash复制chet-webapi --help
看到Usage信息,说明安装成功了。
提示:如果你本机同时装了多个.NET SDK版本,建议在
global.json里固定一个版本,避免生成的模板引用到不兼容的目标框架。我一开始没注意这个,结果在不同机器上生成的csproj目标框架不一致,导致了后面不少麻烦。
2.2 生成项目的完整步骤
在你想创建项目的目录下,执行:
bash复制chet-webapi generate -n MyCompany.Orders.Api -o ./OrdersService
参数说明:
-n指定项目的名称,建议用公司前缀+业务模块的命名方式,比如Acme.Inventory.Api-o指定输出目录,如果不填,默认生成到当前目录下
执行之后,命令行会进入交互式问答,问你几个关键的配置选项:
- 目标框架:net8.0还是net6.0?这里建议直接用net8.0,除非你有特殊兼容需求。
- 是否启用JWT认证:输入y或n。如果选y,会自动生成JWT相关的配置类和appsettings占位节点。
- 数据库选型:EF Core + SQLServer、EF Core + PostgreSQL、还是Dapper?这个选择会影响生成的Repository层和数据访问代码。
- 是否启用API版本控制:建议启用,尤其是接口要对外提供服务的时候。
回答完这几个问题,工具会开始生成文件。你会在目标目录下看到这样的结构:
code复制OrdersService/
├── MyCompany.Orders.Api.sln
├── src/
│ ├── MyCompany.Orders.Api/
│ │ ├── Controllers/
│ │ ├── Middlewares/
│ │ ├── Extensions/
│ │ ├── appsettings.json
│ │ └── Program.cs
│ ├── MyCompany.Orders.Application/
│ │ ├── Services/
│ │ ├── DTOs/
│ │ └── Interfaces/
│ └── MyCompany.Orders.Domain/
│ ├── Entities/
│ ├── Enums/
│ └── Interfaces/
├── src/MyCompany.Orders.Infrastructure/
│ ├── Repositories/
│ ├── Data/
│ └── Migrations/
└── tests/
└── MyCompany.Orders.Api.Tests/
整个生成过程大概几秒钟,比手搭快太多了。
2.3 为什么推荐用生成器而不是直接复制旧项目
很多人搭建新项目的时候,第一反应是"把上一个项目复制一份,然后删掉业务代码"。这个方法有两个问题:
第一,复制过来的项目往往带着历史包袱。上一代项目的Hack代码、已经废弃的配置、某个项目特有的处理逻辑,很容易被无意识地带到新项目里。你删的时候未必能删干净,留着又是隐患。
第二,缺少一致性保障。每个人的"基础项目"版本都不一样,有人还停留在.NET Core 3.1,有人已经开始用.NET 8了。不同项目之间基础架构不统一,维护成本极高。
而项目生成器的思路是:模板是一个版本,生成逻辑是确定性的,同一个版本的生成器在任意机器上生成出来的结果是完全一样的。这就保证了团队里所有人起步的基础代码是一致的。
这其实是受到了Ruby on Rails和Spring Initializr的启发。早些年Rails能在开发效率上碾压一众技术栈,"脚手架+约定优于配置"功不可没。.NET生态里其实一直缺少一个好用的初始化工具——dotnet new自带的WebAPI模板太精简了,到生产可用之间还差着一大截。我写这个生成器就是想把那段"差着的路"铺好。
3. 生成后的项目骨架:每一层到底帮你做了什么
3.1 启动层(API层)的配置全解析
打开生成出来的Program.cs,你会发现它已经把常见的启动配置都组织好了。我挑几个关键的点说一下:
csharp复制var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
// 自定义扩展方法,见Extensions文件夹
builder.Services.AddJwtAuthentication(builder.Configuration);
builder.Services.AddSwagger(builder.Configuration);
builder.Services.AddCorsPolicy(builder.Configuration);
builder.Services.AddRepositories();
builder.Services.AddApplicationServices();
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
});
这里用了C#的扩展方法,把每个关注点的配置拆成了独立的文件:
AddJwtAuthentication:读取appsettings.json里的Jwt节点,配置TokenValidationParametersAddSwagger:注册Swagger服务,并按照版本号生成不同的文档分组AddCorsPolicy:从配置里读取允许的来源列表
整个启动层是可读的、可修改的,而不是一堆魔法。你拿到手后,每个文件的职责清清楚楚,想调整直接改对应文件即可。
3.2 中间件管线:异常处理、请求日志、认证授权
中间件这块,我花了比较多心思。默认生成的Program.cs中间件管线长这样:
csharp复制var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.UseSerilogRequestLogging();
app.UseHttpsRedirection();
app.UseCors("DefaultPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
顺序是有讲究的:
- 异常处理中间件要在最外层,这样任何下层抛出的异常都能被捕获并转换为统一格式的响应
UseSerilogRequestLogging()放在认证之前,意思是即使请求没有通过认证,也会记录请求日志,方便排查问题UseAuthentication()和UseAuthorization()必须严格按照先认证后授权的顺序
异常处理中间件的行为是这样的:捕获所有未处理异常,根据异常类型返回不同的HTTP状态码,响应体统一为{ "code": 500, "message": "...", "traceId": "..." }格式。这样前端对接的时候只用处理一套错误格式,不用再为各种奇奇怪怪的异常响应写兼容代码。
3.3 业务层与数据访问层:默认模式是"接口+实现"
生成的项目里,业务层和数据访问层都遵循"接口+实现"的模式:
csharp复制// Application层
public interface IOrderService
{
Task<PagedResult<OrderDto>> GetOrdersAsync(OrderQuery query);
Task<OrderDto> GetOrderByIdAsync(long id);
Task<long> CreateOrderAsync(CreateOrderCommand command);
}
public class OrderService : IOrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IMapper _mapper;
public OrderService(IOrderRepository orderRepository, IMapper mapper)
{
_orderRepository = orderRepository;
_mapper = mapper;
}
// 业务实现...
}
Repository层同理,也是先定义接口再实现,构造函数注入数据库上下文。
很多.NET开发者会纠结:一个小项目有必要搞这么多层吗?我的看法是——如果项目生命周期超过三个月,或者你预期它有可能演进,就值得分层。分层不是给架构师看的,是给三个月后的自己看的。那时候你可能已经不记得每个业务方法的具体逻辑,层与层之间的清晰边界能帮你快速定位代码。
当然,你也可以简化。生成器的Repository实现不是那种一刀切的"全项目统一的泛型仓储",而是每个聚合根一个仓储接口,这样业务复杂度上来之后不会出现"仓储接口里塞满了各种特定查询方法"的尴尬局面。
4. 那些容易被忽略但生产必备的默认配置
4.1 Swagger的实用增强:分组、注释、Token调试
我见过很多项目的Swagger就是裸的,点开只有一个Default分组,接口注释不显示,调试需要Token的接口还得自己想办法。
生成器默认做了三件增强:
第一,按API版本分组。Swagger UI里会显示v1、v2两个下拉选项,对应不同版本的接口。这样接口文档本身就是一份版本清晰的API清单。
第二,XML注释自动导入。生成的项目在csproj里启用了GenerateDocumentationFile,Swagger配置里读取了XML注释文件。你在控制器方法上写的/// <summary>注释会直接显示在Swagger文档里,这逼着你在写接口的时候就把注释写清楚,因为不写的话文档就是残缺的。
第三,JWT调试按钮。Swagger UI右上角会出现Authorize按钮,填入了Token之后,调试接口时请求头会自动带上Authorization: Bearer xxx。这个功能看似简单,但真的能省掉不少调试时间。
4.2 Serilog日志的配置思路
日志这块,生成器接的是Serilog,配置在appsettings.json里:
json复制"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"System": "Warning"
}
},
"WriteTo": [
{ "Name": "Console" }
]
}
默认写到了Console,适合容器化部署场景。如果你需要写到文件或者数据库,直接在这个配置节点下加WriteTo条目即可。
一个建议:生产环境不要上来就开Debug级别的日志,数据量会让你崩溃。一般先保持Information,遇到具体问题再临时调整特定命名空间的日志级别。
我在实际项目里经常遇到一个情况:线上出了问题,但日志级别太高,关键信息没记下来。所以我把Override里的Microsoft和System调成Warning,自己业务代码的日志级别保持Information,这样既不会刷屏,也不会漏掉业务关键日志。
4.3 CORS策略与JWT认证的默认值
CORS配置默认是"白名单模式"——你在appsettings.json的Cors:AllowedOrigins节点里配置允许的来源,默认情况下一个都不允许。这是安全考虑:与其默认全放行然后忘改,不如默认全禁止,按需添加。
json复制"Cors": {
"AllowedOrigins": [
"https://admin.example.com"
]
}
JWT配置也是同样的思路:
json复制"Jwt": {
"Issuer": "MyCompany.Api",
"Audience": "MyCompany.Client",
"SigningKey": "REPLACE_WITH_YOUR_SECRET_KEY",
"ExpireMinutes": 120
}
生成器会特意在SigningKey的位置放一个占位字符串,提醒你必须替换。之前在同事的机器上见过项目上线两个月了,JWT的SigningKey还是第一次生成的默认值,这意味着任何知道默认密钥的人都可以签发Token,这属于最高级别的安全事故。
4.4 健康检查与Dockerfile
生成的项目里包含了一个/healthz端点,用于健康检查。这在部署到Docker或者Kubernetes环境时几乎是必备的,检测探针直接指向这个地址。
同时,项目根目录生成了一个多阶段构建的Dockerfile:
dockerfile复制FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 80
ENTRYPOINT ["dotnet", "MyCompany.Orders.Api.dll"]
这个Dockerfile已经可以正常构建部署,不需要额外修改。你只需要准备一个数据库容器或者连接字符串,就能把整套服务跑起来。
5. 模板定制:把团队的代码风格固化到生成器里
5.1 模板文件为什么值得改
工具用了一段时间后,你会发现一个新的需求:团队内部想统一一些规范。比如:
- 公司内部的NuGet包源地址
- 统一的命名空间前缀
- 默认引用的内部公共库
- 代码分析规则集(
.editorconfig) - 统一的分页参数命名(PageIndex/PageSize还是Page/PageSize)
这些事情如果靠口头约定,很快就会走样。但如果能固化到模板里,每个新项目一出生就自带这些规范,那才是真正的"默认即正确"。
生成器的模板是开放可改的。在安装目录下,你会找到一个templates文件夹,里面是按类型划分的子文件夹:
bash复制~/.dotnet/tools/.store/chet-webapi.template.generator/*/tools/net8.0/any/templates/
每个子文件夹代表一种文件模板,使用Razor语法进行参数化。比如项目名称的占位是@Model.ProjectName,命名空间的占位是@Model.RootNamespace。
5.2 几个值得改的模板位置
以.editorconfig为例,生成器默认生成的文件里有这些配置:
ini复制root = true
[*]
charset = utf-8
end_of_line = crlf
insert_final_newline = true
[*.cs]
indent_style = space
indent_size = 4
csharp_new_line_before_open_brace = all
csharp_style_var_elsewhere = true:suggestion
你可以把公司规范里关于命名、缩进、空行的规则加进去。这样Visual Studio和Rider在打开项目的第一时间就会应用这些规则,不符合规范的代码直接出现波浪线提示。
还有Directory.Build.props这个文件,它可以让一个解决方案下所有项目共享MSBuild属性。你可以在这里统一配置LangVersion、Nullable、TreatWarningsAsErrors等属性,代码质量门槛在编译期就生效。
5.3 修改模板后如何验证
模板文件修改后,重新执行一次生成,对比输出结果是否符合预期。建议用一个小测试项目作为验证:不写任何业务代码,只跑构建,确保没有编译错误。
code复制
生成的测试项目里,默认包含一个示例控制器、一个示例Service和几个单元测试。
生成的测试项目里,默认包含一个示例控制器、一个示例Service和几个单元测试。这些内容就是最好的模板验证样本——如果它们能编译、能通过测试,那模板的改动就是安全的。
## 6. 踩坑记录与当前版本的边界
### 6.1 第一次使用时的SLN路径问题
刚开始使用这个工具的时候,我遇到过一个坑:生成的解决方案文件路径带上了时间戳目录,导致CI脚本里`dotnet build`找不到sln文件。
排查了一会发现,问题出在我执行生成命令时用了`-o`参数指定了子目录,但工具内部的sln生成路径没有同步拼接这个前缀。修复之后,工具会在所有相对路径计算中统一使用最终输出目录作为基准。
如果你也遇到类似问题,先确认一下:
1. 生成命令的输出目录是否正确
2. 解决方案文件名是否和你`-n`参数传入的名称一致
3. 打开sln文件,检查项目路径引用是否全部为相对路径
### 6.2 处理已有代码库时的兼容性策略
这个工具不适合直接拿来处理已有的老项目——它的定位是"从零创建新项目"。如果你想把老项目迁移到新的架构风格上,建议是:先手工对照模板差异评估成本,再决定值得不值得。
我在处理一个.NET Framework 4.7.2的老项目时,评估发现大部分改动集中在依赖注入和中间件配置上,但框架本身的差异(比如HttpContext的API不同)导致迁移成本远高于重建一个新项目再迁移业务代码。这种情况不如直接"老项目冻结,新需求用新架构起新服务"。
### 6.3 当前版本不做什么
这个版本的设计目标之一是保持简单,所以有意地不做这些事情:
**第一,不做全自动的数据库迁移**。生成器只生成DbContext和初始的Migrations骨架,但不会在你运行项目的瞬间自动建库。理由是生产环境的数据库变更应该走严格的迁移审批流程,而不是应用启动时自动执行。
如果你确实需要开发环境自动迁移,在`Program.cs`里加一段:
```csharp
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.Migrate();
}
第二,不做代码生成(Code Generator)功能。生成器只在项目初始化时生成一套基础代码,不会在你写了实体类后自动生成对应的Service。这一点是一个刻意的取舍,因为过度自动化的代码生成往往会导致大量无效代码。
第三,不绑定特定ORM。虽然生成器默认使用EF Core,但你完全可以在生成后替换成Dapper或者其它数据访问方案。模板里的Repository模式是对数据访问抽象的实现,更换ORM时只需要重写Infrastructure层即可。
6.4 后续版本的一些想法
目前这个工具已经能满足我日常80%以上的新项目启动场景了。后续我计划增加的方向有两个:
一个是对微服务场景的优化,比如生成完整的服务发现配置、统一配置中心接入代码等。不过这部分还在设计阶段,不想做成一个臃肿的"全家桶",要保证每个模块都是按需引入的。
另一个是开发一个模板市场——让团队可以把定制好的模板发布出去,其他人一条命令就能拉取使用。这样一来,团队内部的架构演进就变成了"模板的演进",新项目天然继承最新的架构决策。
7. 最终使用流程
最后整理一下我每天实际使用这个工具的方式。
新项目开工第一天,我会执行:
bash复制chet-webapi generate -n ClientName.ProjectName.Api -o ./ProjectName
cd ./ProjectName
dotnet restore
dotnet build
dotnet run --project src/ClientName.ProjectName.Api
浏览器打开Swagger页面,确认接口文档能正常展示,然后提交第一次代码。整个过程不超过十分钟。
之后在写业务代码的时候,我只需要在Application层加对应的Service接口和实现,在Domain层加实体,在Infrastructure层加仓储实现,在API层加Controller。每一层的目录都是现成的,关注点分离也是天然的,写完一个模块,代码在哪个位置、数据怎么流转,自己心里清清楚楚。
我个人的体会是:脚手架工具最重要的价值不是省那几十分钟的搭建时间,而是它在项目出生那一刻就立下了规矩。代码分层、异常处理、日志规范、配置管理,这些东西如果靠人写是靠不住的,靠文化和制度约束也是靠不住的,最靠谱的方式是在代码生成的那一刻就固化下来。
如果你也在维护多个.NET WebAPI项目,或者团队里经常有新项目启动,这个生成器应该能派上用场。安装命令在前面已经给过了,用起来有任何问题,也欢迎你在使用后交流你的定制经验。
