
1. 写在前面NancyFX 的退场把我们这些老项目架在了火堆上老实说看到“NancyFX 停止维护”这几个字的时候我第一反应不是惋惜而是想骂人。手里压着两个基于 Nancy 写的老 API 服务一个给内部运营系统用一个给小程序做数据接口。当年选 NancyFX 就是看重它轻量、无依赖、路由写起来舒服结果一觉醒来这个框架变成“只读状态”了。更头疼的是团队里新来的同事根本没见过 Nancy每次都要先补半个小时的背景知识才能上手改需求。不过骂归骂问题还是要解决。NancyFX 在 .NET 生态里扮演的角色很特殊它不像 ASP.NET MVC 那么“重”但又比原生的HttpListener方便得多。它把模块化、依赖注入、视图引擎这些概念揉在一起又保持了一个非常友好的开发体验。正因为这样很多中小项目、内部工具、快速原型都选了它。现在它停止维护意味着这些项目迟早要面对兼容性、安全性和依赖冲突的三重压力。这篇文章不是要劝你把所有 Nancy 项目立刻推倒重写而是分享我这次迁移到 PicoServer 的全过程为什么选它、怎么迁、中间踩了哪些坑、以及最终跑起来的效果。如果你手上也有类似的老项目或者你正在一个新的轻量级需求里选型这篇内容应该能帮你省不少时间。1.1 NancyFX 的辉煌与停止维护的真相NancyFX 最早火起来是因为在 .NET Framework 时代写一个 Web 服务往往意味着要忍受 ASP.NET WebForms 的繁琐。Nancy 提供一个极简的NancyModule编程模型你只需要继承一个类里面写Get、Post这样的方法就能把路由和逻辑绑在一起。这种“一个模块搞定一个资源”的方式比大型 MVC 项目更容易上手特别适合做微服务雏形和内部 API。但好景不长。随着微软在 ASP.NET Core 上押下重注整个 .NET 世界开始向跨平台、高性能、统一管道收敛。Nancy 社区的核心维护者逐渐精力不足项目长期没有大版本更新。到后来连 .NET Core 3.1 和 .NET 6 的兼容适配都变得很勉强。就在前几年项目被正式标记为归档状态不再接受新的功能 PR只保留一些安全修复的讨论。所有人都知道这个框架实际上已经退休了。停止维护这件事外行看是“以后没人更新了”内行看是三件事安全漏洞没人补、新版本 .NET 运行时兼容性无人验证、依赖的老包可能会在未来某个时候直接不可用。尤其是自定义宿主在非 Windows 环境跑的时候问题会越来越频繁。你没有做错任何事但生态系统已经不再等你。1.2 停止维护到底意味着什么我见过很多团队同时也维护着十几二十个老的 Nancy 服务一听到停止维护就慌了但真正动手迁移的却不多。原因很简单这些服务还在线上跑得好好的不是非动不可。但这里有一个“技术债”的窗口期问题。你不在框架还有余温的时候迁移等到 .NET 升级、依赖包冲突、或者安全审计要求强制整改的那一天你会在更大的压力下被迫重构那才是最被动的。从我这次的经验来看NancyFX 停止维护带来的直接问题包括无法在新项目里舒服地使用它、老项目升级 .NET 版本时总有几个包对不上、新同学的学习成本高。还有一个隐性成本当你去 GitHub 提 issue 的时候发现仓库已经 Archive心里特别没底。所以与其等系统出问题再想办法不如尽早选一个替代方案把“要不要换”变成“怎么换”。2. 替代方案选型为什么我最终选了 PicoServer轻量级 Web 框架的候选名单其实不少但真正适合接手 NancyFX 场景的并不算多。我列出来的候选包括ASP.NET Core 的 Minimal API、老牌的高性能框架 ServiceStack、以及几个 Nancy 社区 fork还有一个就是 PicoServer。每一项都有它适合的地方但放到“老项目迁移”这个具体场景里PicoServer 在我这边胜出了。2.1 候选方案横向扫一眼先说说 ASP.NET Core Minimal API。它是微软官方的轻量方案文档全、社区大性能也极好。但它和 Nancy 的编程模型差距比较大。Nancy 是“模块自动扫描注册”你写好NancyModule子类框架自己找到并挂载Minimal API 则要求你显式地把每个路由注册到WebApplication上。老代码迁过去基本上要重写路由层工作量不小。ServiceStack 性能强、功能全但它是商业授权对于公司内部项目License 成本是个绕不开的问题。Nancy 的社区 fork 虽然保留了部分兼容 API但活跃度普遍不高本质上还是在维护一个“没有官方支持”的框架。万一 fork 的维护者也跑路了你就得再换一次等于把风险延后而没有消除。PicoServer 吸引我的地方反而是它的“不起眼”。它没有庞大的功能集路由、参数绑定、请求上下文、响应辅助方法核心就几条命令。它不强行塞给你一套 MVC 或者模块机制而是让你用非常直白的方式定义 URL 和处理器的映射关系。对于迁移 Nancy 这种“自己本来就不复杂”的服务来说这种极简风格反而降低了理解成本。源码量小遇到问题可以自己读源码排查心里踏实。2.2 PicoServer 的设计思路把复杂藏到背后PicoServer 的核心思路是把 HTTP 服务器请求处理过程拆成三层监听层、路由层、处理器层。监听层负责绑定端口和支持 HTTP/1.1 的基础解析路由层负责把请求方法加路径匹配到对应的处理函数处理器层则完全留给你自己决定返回 JSON、静态文件、还是直接写字符串。这种设计跟 NancyFX 最大的不同是它没有“上下文魔法”。Nancy 里的Request、Response、ViewBag都会被包装成一套自己的对象用起来方便但调试的时候要绕。PicoServer 则更贴近底层很多操作其实就是对着HttpContext做增删改查。老手看着亲切新手学起来也不至于隔着一层纱。当然PicoServer 也不是没有代价。它默认不带视图引擎、不带认证授权、不带Session 管理。你要是想搭一个完整的 MVC 网站它并不合适。但如果你做的是 API、内部工具后端、IoT 网关、或者嵌入式设备的管理页面它反而刚刚好。Nancy 的典型使用场景恰恰就是这么一批小服务所以我们迁移的时候没有感到功能缺失。3. PicoServer 上手实操从一个能跑的 HelloWorld 到一个完整的 API在动手之前我的忠告是先花十分钟把官方的 README 通读一遍尤其是版本说明。PicoServer 这种微型框架版本之间 API 变动可能会很大。下面我写的示例基于我使用的 0.4.x 版本如果你拿到的包接口对不上就按 README 里的签名调整一下思路是一样的。3.1 环境准备与项目初始化我的环境是 Windows 11 .NET 8 SDK不过 PicoServer 是跨平台的Linux 容器里跑也没问题。新建项目很简单我直接在空目录里执行dotnet new console -n Picodemo cd Picodemo dotnet add package PicoServer这里有一点要注意因为 PicoServer 本质上是自托管的 HTTP 服务不需要 ASP.NET Core 的整套运行时所以控制台项目就够了。Program.cs里先写一个最简单的启动代码验证包引用和环境没问题using PicoServer; var app PicoServer.App.Create(args); app.Get(/, ctx ctx.Response.WriteAsync(Hello PicoServer!)); app.Run();跑起来之后浏览器打开http://localhost:8080能看到那行字就说明监听环境一切正常。这一步别跳过很多后面看起来玄乎的问题其实在第一步就埋下了比如端口被占用、防火墙拦截、甚至运行时版本不对。3.2 路由与参数绑定怎么写才顺手Nancy 的路由是模块化的PicoServer 则是一张“路由表”。迁移的时候我建议先把 Nancy 模块里的路由一个个列出来整理成一张表格再照着表去注册。这样做的好处是你不会漏掉那些藏在子模块里的路由。假设老代码里有一个这样的 Nancy 模块public class UserModule : NancyModule { public UserModule() : base(/api/users) { Get(/, _ GetAll()); Get(/{id:int}, parameters GetById(parameters.id)); Post(/, _ CreateUser(Request.Body)); } }迁移到 PicoServer 之后可以拆成这样app.Get(/api/users, ctx UserHandler.GetAll(ctx)); app.Get(/api/users/{id:int}, ctx UserHandler.GetById(ctx, ctx.RouteValues[id])); app.Post(/api/users, ctx UserHandler.Create(ctx));写的时候注意{id:int}这种约束写法。PicoServer 支持基本的类型约束但不同版本处理方式可能不同。我的经验是宁可自己在处理器里做类型转换和校验也别强依赖路由模板的隐式转换。比如app.Get(/api/users/{id}, ctx { if (!int.TryParse(ctx.RouteValues[id], out var id)) { ctx.Response.StatusCode 400; return ctx.Response.WriteAsync(Invalid id); } return UserHandler.GetById(ctx, id); });这样多写了三行但排查问题的时候会感谢自己的严谨。3.3 中间件机制给请求加“关卡”Nancy 里也可以用BeforeRequest、AfterRequest做请求前后处理。PicoServer 同样有中间件管道只是名字可能叫Use或者AddMiddleware取决于版本。我在项目里用中间件处理了三件事请求日志、接口鉴权、统一异常捕获。日志中间件最直接app.Use(async (ctx, next) { var sw System.Diagnostics.Stopwatch.StartNew(); await next(); sw.Stop(); Console.WriteLine(${ctx.Request.Method} {ctx.Request.Path} {ctx.Response.StatusCode} {sw.ElapsedMilliseconds}ms); });鉴权中间件要注意顺序必须在路由处理之前注册。否则请求都已经进到业务代码里了你再想拦已经晚了。我有一段时间把鉴权写在路由之后结果前端直接拿到了数据排查了很久才反应过来是顺序问题。异常捕获也很重要。Nancy 有一个默认的错误页面机制PicoServer 更简单如果你自己不处理异常可能直接让进程崩掉。所以我加了统一的 try-catch 中间件把未处理的异常转成 500 JSONapp.Use(async (ctx, next) { try { await next(); } catch (Exception ex) { ctx.Response.StatusCode 500; await ctx.Response.WriteAsJsonAsync(new { error ex.Message }); } });3.4 静态文件与前端资源支持有的老项目不止提供 API还兼着托管一些静态页面。Nancy 支持约定目录下的静态资源PicoServer 也有类似能力。我用的版本里需要在创建 App 的时候配置静态目录根路径var app PicoServer.App.Create(args, options { options.StaticFilesRoot wwwroot; options.DefaultFiles new[] { index.html }; });默认文件支持很贴心访问/时自动返回wwwroot/index.html。但有一个坑静态文件和 API 路由的匹配优先级不同版本表现不一样。如果发现某个路径明明写了 API 路由却返回了静态文件就去查一下是不是把同名目录放到了 wwwroot 下面。命名冲突最容易在这个环节出现。4. 迁移实战把 NancyFX 模块改写到 PicoServer这一节才是最硬核的部分。我会用一个虚拟的“待办事项 API”作为例子完整走一遍迁移过程。这个例子虽然简单但覆盖了路由、请求体读取、响应序列化、依赖注入四个最常见的场景。4.1 一个典型 Nancy 模块长什么样先看看迁移前的代码。这是很多 Nancy 项目里非常典型的一段public class TodoModule : NancyModule { private readonly ITodoService _service; public TodoModule(ITodoService service) : base(/api/todos) { _service service; Get(/, _ GetAll()); Get(/{id}, parameters GetById((int)parameters.id)); Post(/, _ Create(Request.Body)); Put(/{id}, parameters Update((int)parameters.id, Request.Body)); Delete(/{id}, parameters Delete((int)parameters.id)); } private Response GetAll() { var items _service.GetAll(); return Response.AsJson(items); } private Response GetById(int id) { var item _service.GetById(id); return item null ? HttpStatusCode.NotFound : Response.AsJson(item); } private Response Create(Stream body) { var item JsonConvert.DeserializeObjectTodoItem(new StreamReader(body).ReadToEnd()); var created _service.Create(item); return Response.AsJson(created).WithStatusCode(HttpStatusCode.Created); } }这段代码用到了构造函数注入、Request.Body的流读取、Response.AsJson序列化、以及状态码设置。迁移的关键就是把这四件事在 PicoServer 下重新落位。4.2 一步一步改写到 PicoServer第一步先定义处理类。我倾向用一个静态类按资源聚合方法这样代码结构清楚。public static class TodoHandler { public static async Task GetAll(HttpContext ctx, ITodoService service) { var items service.GetAll(); await ctx.Response.WriteAsJsonAsync(items); } public static async Task GetById(HttpContext ctx, ITodoService service) { if (!int.TryParse(ctx.RouteValues[id], out var id)) { ctx.Response.StatusCode 400; return; } var item service.GetById(id); if (item null) { ctx.Response.StatusCode 404; return; } await ctx.Response.WriteAsJsonAsync(item); } public static async Task Create(HttpContext ctx, ITodoService service) { var item await ctx.Request.ReadFromJsonAsyncTodoItem(); if (item null) { ctx.Response.StatusCode 400; return; } var created service.Create(item); ctx.Response.StatusCode 201; await ctx.Response.WriteAsJsonAsync(created); } }第二步在Program.cs里注册路由。如果项目用了依赖注入容器就通过容器解析ITodoService然后在路由处理里显式传入。PicoServer 对 DI 的支持比较朴素它不会像 Nancy 那样自动帮你把构造参数注入到处理器里所以建议你在路由注册的时候做一个简单的工厂方法var service app.Services.GetRequiredServiceITodoService(); app.MapGet(/api/todos, ctx TodoHandler.GetAll(ctx, service)); app.MapGet(/api/todos/{id}, ctx TodoHandler.GetById(ctx, service)); app.MapPost(/api/todos, ctx TodoHandler.Create(ctx, service));这样写的好处是依赖关系一目了然。新同事接代码的时候不需要去猜测框架在哪里偷偷调用了构造函数。第三步把原来的 JSON.NET 迁移成System.Text.Json。这一步可能要看项目复杂度。Nancy 默认用的就是 JSON.NETPicoServer 的响应辅助方法通常基于System.Text.Json。如果老项目里大量使用了Json.NET特有的特性比如[JsonProperty]、自定义 ContractResolver那就需要注意。简单场景下直接换注解public class TodoItem { public int Id { get; set; } public string Title { get; set; } string.Empty; public bool Completed { get; set; } }System.Text.Json默认序列化属性名是 camelCase你可以在创建 App 的时候配置options.JsonSerializerOptions.PropertyNamingPolicy System.Text.Json.JsonNamingPolicy.CamelCase;如果老接口已经用 PascalCase 输出客户那边不好改那就保持默认或者显式指定。这类细节要在上线前跟调用方确认好。4.3 差异对照与注意事项我把迁移过程中遇到的主要差异整理成了一个表格方便你对照检查关注点NancyFXPicoServer路由定义模块内声明自动扫描启动时手动注册参数注入构造器自动注入需要手动传依赖请求体读取Request.Body流ReadFromJsonAsync响应序列化Response.AsJsonWriteAsJsonAsync状态码设置.WithStatusCode()直接给HttpContext.Response.StatusCode赋值静态文件默认支持需要配置文件根目录异常处理内置错误页需要自己写中间件视图引擎支持多种默认不支持注意事项里有两点特别想强调。第一路由注册顺序很重要。如果同时存在/api/todos/{id}和/api/todos/active这种静态和动态路径尽量把静态路径注册在前面避免动态路由吞掉请求。第二ReadFromJsonAsync遇到空 body 会返回 null但不是所有版本都会抛异常所以处理逻辑里一定要判空不能假设请求体永远合法。5. 常见问题与排查技巧实录真正上线过程永远不会一帆风顺。这一节我把这次迁移中遇到的高频问题整理出来每一个都是我实际踩过的坑不是从文档里抄来的。5.1 启动失败、端口被占用和进程立即退出最典型的问题是端口被占用。Nancy 默认端口可能写死在配置里迁移后沿用老端口就很容易撞上别的进程。排查时先用命令看端口netstat -ano | findstr :8080拿到 PID 后再看是哪个进程占用的。如果确定没用就结束进程如果不想动它就改 PicoServer 的监听端口。配置文件里明确写Urls: http://localhost:8081或者启动时用命令行参数指定都能解决。还有一种情况是进程启动后闪光退出。这时候重点检查是不是运行时版本不对或者依赖包版本冲突。在控制台项目里用dotnet run直接跑能看到完整异常栈不要一上来就丢到 docker 里看日志先本地把最小复现跑通。5.2 路由匹配 404 和参数绑定失败404 问题多半出在路由前缀上。Nancy 模块的BasePath是区分大小写的PicoServer 的路由表默认可能不区分大小写但这并不意味着你就可以随手改大小写。接口的路径是给调用方看的一旦变更就是破环性修改。所以迁移的时候保持路径一致前缀不能丢。参数绑定失败最常见的是 int 类型。比如请求/api/todos/abc路由匹配到了{id}但解析abc成 int 失败。如果路由模板不做约束你的代码里就要自己 try-catch 或者 TryParse。我在迁移中统一封装了一个RouteValueReader工具方法专门处理类型转换比在每个处理器里写重复代码靠谱得多。5.3 HTTPS 和反向代理场景本地开发直接用 HTTP 没问题但生产环境基本都要挂 HTTPS。PicoServer 本身不自带证书管理我的做法是在前面放一个 Nginx 反向代理让 PicoServer 只监听回环地址的 HTTP。这样证书更新、TLS 终止都用 Nginx 来做应用层完全不感知。但这里有一个容易忽略的点如果 Nginx 转发到 PicoServer 时没有正确设置X-Forwarded-For和X-Forwarded-Proto头业务代码里如果依赖客户端 IP 或者判断请求是否 HTTPS就会出错。我建议在 PicoServer 里加一个简单中间件把反代头读进来覆盖请求上下文里的对应字段。否则日志里的 IP 永远是127.0.0.1排查问题会疯掉。5.4 性能调优的几点实测PicoServer 因为极简性能通常不错但默认参数不一定贴合你的场景。我实测下来有三个点对吞吐影响最大线程池最小线程数、HTTP 连接保持、以及 JSON 序列化配置。高并发下可以尝试在启动时调整线程池ThreadPool.SetMinThreads(200, 200);不要盲目调大最好配合压测看数据。连接保持方面确认客户端是否复用连接避免每次请求都重新建立 TCP 连接。JSON 序列化方面如果需要反复序列化同一个对象集合考虑使用源生成器或者缓存序列化结果能省不少 CPU。6. 迁移到 PicoServer 之后的几点个人体会最后说点“软”的东西可能比技术细节更有用。我在这次迁移中没有追求一步到位而是先把一个流量最小的内部服务迁过去跑了两周确认日志、监控、异常处理都正常之后才继续迁第二个、第三个。每一步都用 git 标签记录切换点出了一问题可以快速回滚。这个节奏看起来慢但实际非常稳。另一个体会是轻量级框架的“轻”不等于“简单”。PicoServer 没有强加给你一套架构这既是优点也是缺点。优点是你不会被框架绑住手脚缺点是你得有足够的自制力去建立一套自己的规范。我后来在项目里约定所有 API 返回值统一包成{ code, data, message }所有异常统一走同一个中间件所有路由注册集中在一个文件里不散落到各个类中。这些规矩弥补了框架的简单也让团队协作变得顺畅。如果你现在正面对一个 NancyFX 老项目我的建议是不要慌也不要急着全量重写。先梳理路由清单评估每一条依赖再做一个小范围的验证迁移。替换框架是手段真正要做的是让服务继续稳定可靠地跑下去。至于选不选 PicoServer看了上面的实践相信你已经有自己的判断了。