大家好,我是码农刚子。上周帮朋友看一个跑了五年的老项目,打开 csproj,依赖列表干净得像刚建的一样。
不是他写得好,是他该用的第三方库一个都没用。日志靠 Console.WriteLine,参数校验在 Controller 里堆 if,定时任务拿 Task.Run 硬扛。
这种项目不是不能跑,是每天都在踩自己五年前埋的雷。
我把自己这几年每个新项目都会装的 8 个包整理了一遍。都是用了很久、经得起生产环境折腾的,不是 GitHub 上一时热闹的玩具。
先说清楚,这 8 个包解决的是 8 类不同的问题,不重复、不打架。按需取用,不用全装。
| 包 | 解决什么 | 什么时候该上 |
|---|---|---|
| Serilog | 结构化日志 | 日志要按字段检索、要长期留存 |
| FluentValidation | 校验规则与业务逻辑分离 | 字段多、规则会反复改 |
| Polly | 重试、熔断、超时 | 调外部接口、跨网络调用 |
| Mapster | 对象映射,编译期生成 | DTO 和实体之间频繁转换 |
| Dapper | 手写 SQL 的轻量 ORM | 复杂查询、报表、EF 写不动时 |
| Hangfire | 后台任务与定时调度 | 任务要持久化、要看得见历史 |
| Scalar | 交互式 API 文档 | .NET 9 以后,想要现代 OpenAPI UI |
| BenchmarkDotNet | 性能基准测试 | 你要对性能下判断,而不是靠感觉 |
我每个新项目固定装的 8 个包,按解决问题分类
1. Serilog
先说日志,因为它是唯一一个你上线第一周就会后悔没用的包。
内置 ILogger 写出来的是纯文本。线上出问题你想查「某个用户 5 分钟内所有的支付日志」,只能 grep 字符串,还得祈祷日志格式没变过。
Serilog 把日志变成结构化数据。每个字段单独存,可以按字段查、按字段聚合,不用再做正则。
bash
dotnet add package Serilog.AspNetCore
csharp
builder.Host.UseSerilog((ctx, cfg) => cfg
.ReadFrom.Configuration(ctx.Configuration)
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.Seq("http://localhost:5341"));
装完之后,日志就该这么写。
csharp
logger.LogInformation(
"订单 {OrderId} 已创建,金额 {Amount}", id, amount);
看着像字符串拼接,实际存下来是 OrderId 和 Amount 两个独立字段。
这就是结构化日志和文本日志的全部区别。
什么时候别用。如果只是个内部小工具,日志写完没人看,那内置的 ILogger 加个控制台输出就够了,不用为了仪式感引一套。
2. FluentValidation
参数校验这件事,写起来最没成就感,也最容易写成一团。
DataAnnotations 的问题是它长在实体上。一个 CreateOrderDto 和 UpdateOrderDto 校验规则差一点,你就得开两个类或者塞一堆 Groups。
FluentValidation 把规则从实体里拿出来,单独成类。校验规则是一等公民,能单测,能复用,能按场景组合。
bash
dotnet add package FluentValidation.DependencyInjectionExtensions
csharp
public class OrderValidator : AbstractValidator<OrderDto>
{
public OrderValidator()
{
RuleFor(x => x.Amount).GreaterThan(0);
RuleFor(x => x.Email).EmailAddress();
RuleFor(x => x.Phone).Matches(@"^1[3-9]\d{9}$")
.WithMessage("手机号格式不对");
}
}
注册一行就够了,它会扫整个程序集。
csharp
builder.Services
.AddValidatorsFromAssemblyContaining<OrderValidator>();
什么时候别用。三五个字段的简单表单,DataAnnotations 完全够,引 FluentValidation 属于杀鸡用牛刀。规则一旦开始分场景、分角色、分业务流程,再换。
3. Polly
只要你的系统会调别人的接口,就一定会遇到这三种情况。对方偶发 502,对方响应慢到十秒,对方整体挂掉还在被你的代码猛打。
手写 try catch 加重试,是很多项目的标准做法。问题是重试次数、退避策略、超时、并发隔离,这四件事手写下来基本都会漏一个。
Polly 这几年最大的变化是不用再手搓策略了。.NET 生态里现在有官方封装的弹性处理管道,一行注册,重试、超时、熔断、限流全给你配好。
bash
dotnet add package Microsoft.Extensions.Http.Resilience
csharp
builder.Services.AddHttpClient("pay")
.AddStandardResilienceHandler(o =>
{
o.Retry.MaxRetryAttempts = 3;
o.TotalRequestTimeout.Timeout =
TimeSpan.FromSeconds(10);
o.CircuitBreaker.FailureRatio = 0.5;
});
这段配置里最重要的不是重试次数,是那个总超时。它保证一个请求无论怎么重试,都不会把线程占超过十秒。
什么时候别用。同机房里两个服务互调,网络抖动的概率极低,加了重试反而在对方慢的时候把压力放大三倍。内网调用先想清楚再上。
4. Mapster
对象映射这个需求,只要你分了 DTO 和实体,就一定躲不开。
AutoMapper 是很多人的第一选择,但这几年它的定位越来越尴尬。运行时反射映射,性能有成本,出错要到运行期才知道,而且新版本对商业使用还有授权说明要读。
Mapster 的路线不一样,它走源生成器。映射代码在编译期就生成好了,运行时零反射,映射写错编译就报错。 这两种体验差距,用过一次就回不去。
bash
dotnet add package Mapster
dotnet add package Mapster.Tool
csharp
config.NewConfig<Order, OrderDto>()
.Map(d => d.Total,
s => s.Items.Sum(i => i.Price));
var dto = order.Adapt<OrderDto>();
什么时候别用。映射逻辑不复杂、字段还老在变的时候,手写一个扩展方法反而更清楚。有时候多写十行代码,比多学一个包的映射语法便宜。
5. Dapper
用不用 ORM,是 .NET 圈吵了十年也没结论的话题。我的答案是两个都要。
写数据用 EF Core,改数据用它的事务和变更跟踪,很舒服。但报表、多表联合查询、需要精确控制 SQL 的场景,EF 生成的语句经常不是你想要的。
Dapper 只有几百行核心代码。它不生成 SQL,它只负责把 SQL 结果映射成对象。 你可以和 EF Core 共存,同一个连接上查询走 Dapper,写入走 EF。
bash
dotnet add package Dapper
csharp
const string sql = @"
select o.Id, o.Amount, c.Name
from Orders o
join Customers c on c.Id = o.CustomerId
where o.CreatedAt >= @from";
var rows = await conn.QueryAsync<OrderRow>(
sql, new { from = start });
参数化查询是它的默认写法,字符串拼接永远不要碰。
什么时候别用。如果整个项目都是标准 CRUD、领域模型还有聚合根和值对象,那 EF Core 就够了,再引一层 Dapper 只会让团队不知道该在哪写。
6. Hangfire
定时任务这件事,我见过太多项目在 Startup 里起一个 Timer,然后一部署多实例就跑到重。
Hangfire 把任务存进数据库。任务不会因为进程重启而丢,多实例部署会自动抢锁只有一个执行,执行失败了自动重试,还有一个 Web 界面能看见每个任务的执行历史。
凌晨三点对账失败了,第二天早上你能打开界面看见它失败了三次、报什么错、什么时候会再试。
bash
dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.SqlServer
csharp
builder.Services.AddHangfire(c =>
c.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseSqlServerStorage(connStr));
app.UseHangfireDashboard("/jobs");
RecurringJob.AddOrUpdate(
"daily-report", () => Report.Send(), Cron.Daily);
什么时候别用。就一个进程、任务失败了重跑一次也无所谓,那用 BackgroundService 加一个简单循环就够。Hangfire 的价值在持久化和可观测,这两点用不上就别引。
7. Scalar
这个包跟前 6 个不一样,它替换的是你已经在用的东西。
Swashbuckle 撑了 .NET 的 API 文档很多年,但从 .NET 9 开始官方模板已经把 Swagger 那一套去掉了,换成内置的 OpenAPI 文档生成。文档数据的来源变了,界面就成了可选项。
Scalar 就是那个界面。同样吃 OpenAPI 文档,但支持内嵌请求示例、多语言代码生成、暗色主题,还不用自己维护 UI 静态资源。
bash
dotnet add package Scalar.AspNetCore
csharp
builder.Services.AddOpenApi();
app.MapOpenApi();
app.MapScalarApiReference();
什么时候别用。项目还在 .NET 6 或 7,Swagger 已经跑得好好的,没有升级计划,那就别动。换个文档 UI 不值得你承担一次框架升级。
8. BenchmarkDotNet
最后一个包不解决业务问题,它解决的是你和同事吵架的问题。
用 Stopwatch 测性能是通不过评审的。JIT 预热、GC 时机、CPU 频率、死代码消除,任何一个都能让你的数字偏出好几倍。
BenchmarkDotNet 会自己处理预热、多次迭代、统计显著性,还会告诉你每一轮分配了多少内存。
bash
dotnet add package BenchmarkDotNet
csharp
[MemoryDiagnoser]
public class SumBench
{
[Benchmark(Baseline = true)]
public int ForLoop() { /* ... */ }
[Benchmark]
public int LinqSum() => _data.Sum();
}
跑完它会给你一张表,谁快谁慢、差多少、各分配了多少字节,写得明明白白。有了这张表,性能讨论就从「我觉得」变成「数据显示」。
什么时候别用。它是测试项目依赖,别引进生产代码。而且每次跑要几分钟,别放进 CI 的每次提交里,独立跑或者按需跑。
9. 说回装包这件事
8 个包装进 csproj 加起来不到 20 行。真正花时间的不是学会它们,是判断什么时候不该装第九个。
每引一个包,你就多了一份升级责任。NuGet 上躺着的包,三年不更新的太多了。今天的省事,很可能是三年后那次框架升级里的一个坑。
所以我的习惯是,装之前先问一句。这个问题我手写要多少行,写完了会不会有人维护。答案是几十行就能解决、又要跟着业务一直改的,那就自己写。
反过来,像上面这 8 个,它们解决的都不是你的业务问题,是基础设施问题。这类问题别人已经踩了十年坑,重写一遍只是把坑再踩一次。
工具的克制,比工具的数量更能说明一个工程师的水平。