上周帮朋友看一个跑了五年的老项目,打开 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 把日志变成结构化数据。每个字段单独存,可以按字段查、按字段聚合,不用再做正则。
dotnet add package Serilog.AspNetCore
builder.Host.UseSerilog((ctx, cfg) => cfg
.ReadFrom.Configuration(ctx.Configuration)
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.Seq("http://localhost:5341"));
装完之后,日志就该这么写。
logger.LogInformation(
"订单 {OrderId} 已创建,金额 {Amount}", id, amount);
看着像字符串拼接,实际存下来是 OrderId 和 Amount 两个独立字段。
这就是结构化日志和文本日志的全部区别。
什么时候别用。如果只是个内部小工具,日志写完没人看,那内置的 ILogger 加个控制台输出就够了,不用为了仪式感引一套。
2. FluentValidation
参数校验这件事,写起来最没成就感,也最容易写成一团。
DataAnnotations 的问题是它长在实体上。一个 CreateOrderDto 和 UpdateOrderDto 校验规则差一点,你就得开两个类或者塞一堆 Groups。
FluentValidation 把规则从实体里拿出来,单独成类。校验规则是一等公民,能单测,能复用,能按场景组合。
dotnet add package FluentValidation.DependencyInjectionExtensions
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("手机号格式不对");
}
}
注册一行就够了,它会扫整个程序集。
builder.Services
.AddValidatorsFromAssemblyContaining<OrderValidator>();
什么时候别用。三五个字段的简单表单,DataAnnotations 完全够,引 FluentValidation 属于杀鸡用牛刀。规则一旦开始分场景、分角色、分业务流程,再换。
3. Polly
只要你的系统会调别人的接口,就一定会遇到这三种情况。对方偶发 502,对方响应慢到十秒,对方整体挂掉还在被你的代码猛打。
手写 try catch 加重试,是很多项目的标准做法。问题是重试次数、退避策略、超时、并发隔离,这四件事手写下来基本都会漏一个。
Polly 这几年最大的变化是不用再手搓策略了。.NET 生态里现在有官方封装的弹性处理管道,一行注册,重试、超时、熔断、限流全给你配好。
dotnet add package Microsoft.Extensions.Http.Resilience
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 的路线不一样,它走源生成器。映射代码在编译期就生成好了,运行时零反射,映射写错编译就报错。 这两种体验差距,用过一次就回不去。
dotnet add package Mapster
dotnet add package Mapster.Tool
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。
dotnet add package Dapper
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 界面能看见每个任务的执行历史。
凌晨三点对账失败了,第二天早上你能打开界面看见它失败了三次、报什么错、什么时候会再试。
dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.SqlServer
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 静态资源。
dotnet add package Scalar.AspNetCore
builder.Services.AddOpenApi();
app.MapOpenApi();
app.MapScalarApiReference();
什么时候别用。项目还在 .NET 6 或 7,Swagger 已经跑得好好的,没有升级计划,那就别动。换个文档 UI 不值得你承担一次框架升级。
8. BenchmarkDotNet
最后一个包不解决业务问题,它解决的是你和同事吵架的问题。
用 Stopwatch 测性能是通不过评审的。JIT 预热、GC 时机、CPU 频率、死代码消除,任何一个都能让你的数字偏出好几倍。
BenchmarkDotNet 会自己处理预热、多次迭代、统计显著性,还会告诉你每一轮分配了多少内存。
dotnet add package BenchmarkDotNet
[MemoryDiagnoser]
public class SumBench
{
[Benchmark(Baseline = true)]
public int ForLoop() { /* ... */ }
[Benchmark]
public int LinqSum() => _data.Sum();
}
跑完它会给你一张表,谁快谁慢、差多少、各分配了多少字节,写得明明白白。有了这张表,性能讨论就从「我觉得」变成「数据显示」。
什么时候别用。它是测试项目依赖,别引进生产代码。而且每次跑要几分钟,别放进 CI 的每次提交里,独立跑或者按需跑。
9. 说回装包这件事
8 个包装进 csproj 加起来不到 20 行。真正花时间的不是学会它们,是判断什么时候不该装第九个。
每引一个包,你就多了一份升级责任。NuGet 上躺着的包,三年不更新的太多了。今天的省事,很可能是三年后那次框架升级里的一个坑。
所以我的习惯是,装之前先问一句。这个问题我手写要多少行,写完了会不会有人维护。答案是几十行就能解决、又要跟着业务一直改的,那就自己写。
反过来,像上面这 8 个,它们解决的都不是你的业务问题,是基础设施问题。这类问题别人已经踩了十年坑,重写一遍只是把坑再踩一次。
工具的克制,比工具的数量更能说明一个工程师的水平。
暂无评论,来抢沙发吧~