大家好我是码农刚子。上周帮朋友看一个跑了五年的老项目打开 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 UIBenchmarkDotNet性能基准测试你要对性能下判断而不是靠感觉我每个新项目固定装的 8 个包按解决问题分类1. Serilog先说日志因为它是唯一一个你上线第一周就会后悔没用的包。内置 ILogger 写出来的是纯文本。线上出问题你想查「某个用户 5 分钟内所有的支付日志」只能 grep 字符串还得祈祷日志格式没变过。Serilog 把日志变成结构化数据。每个字段单独存可以按字段查、按字段聚合不用再做正则。dotnet add package Serilog.AspNetCorebuilder.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.DependencyInjectionExtensionspublic class OrderValidator : AbstractValidatorOrderDto { 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 .AddValidatorsFromAssemblyContainingOrderValidator();什么时候别用。三五个字段的简单表单DataAnnotations 完全够引 FluentValidation 属于杀鸡用牛刀。规则一旦开始分场景、分角色、分业务流程再换。3. Polly只要你的系统会调别人的接口就一定会遇到这三种情况。对方偶发 502对方响应慢到十秒对方整体挂掉还在被你的代码猛打。手写 try catch 加重试是很多项目的标准做法。问题是重试次数、退避策略、超时、并发隔离这四件事手写下来基本都会漏一个。Polly 这几年最大的变化是不用再手搓策略了。.NET 生态里现在有官方封装的弹性处理管道一行注册重试、超时、熔断、限流全给你配好。dotnet add package Microsoft.Extensions.Http.Resiliencebuilder.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.Toolconfig.NewConfigOrder, OrderDto() .Map(d d.Total, s s.Items.Sum(i i.Price)); var dto order.AdaptOrderDto();什么时候别用。映射逻辑不复杂、字段还老在变的时候手写一个扩展方法反而更清楚。有时候多写十行代码比多学一个包的映射语法便宜。5. Dapper用不用 ORM是 .NET 圈吵了十年也没结论的话题。我的答案是两个都要。写数据用 EF Core改数据用它的事务和变更跟踪很舒服。但报表、多表联合查询、需要精确控制 SQL 的场景EF 生成的语句经常不是你想要的。Dapper 只有几百行核心代码。它不生成 SQL它只负责把 SQL 结果映射成对象。你可以和 EF Core 共存同一个连接上查询走 Dapper写入走 EF。dotnet add package Dapperconst 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.QueryAsyncOrderRow( sql, new { from start });参数化查询是它的默认写法字符串拼接永远不要碰。什么时候别用。如果整个项目都是标准 CRUD、领域模型还有聚合根和值对象那 EF Core 就够了再引一层 Dapper 只会让团队不知道该在哪写。6. Hangfire定时任务这件事我见过太多项目在 Startup 里起一个 Timer然后一部署多实例就跑到重。Hangfire 把任务存进数据库。任务不会因为进程重启而丢多实例部署会自动抢锁只有一个执行执行失败了自动重试还有一个 Web 界面能看见每个任务的执行历史。凌晨三点对账失败了第二天早上你能打开界面看见它失败了三次、报什么错、什么时候会再试。dotnet add package Hangfire.AspNetCore dotnet add package Hangfire.SqlServerbuilder.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.AspNetCorebuilder.Services.AddOpenApi(); app.MapOpenApi(); app.MapScalarApiReference();什么时候别用。项目还在 .NET 6 或 7Swagger 已经跑得好好的没有升级计划那就别动。换个文档 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 个它们解决的都不是你的业务问题是基础设施问题。这类问题别人已经踩了十年坑重写一遍只是把坑再踩一次。工具的克制比工具的数量更能说明一个工程师的水平。阅读原文这 8 个 NuGet 包项目直接起飞 - 码录集