一、前言在 .NET 生态里官方早就给出过“前后端一把梭”的方案——Blazor Server、Blazor WebAssembly、ASP.NET Core 寄宿 IIS 等。但它们要么强依赖前端独立部署要么运行时拖家带口源码裸露、启动速度、跨域配置都是痛点。反观 Go、Rust 社区一个 app 文件就能跑完 HTTP 服务 静态站点拷贝即用编译完连源码影子都看不到。其实 C# 也能做到。今天这篇就把“单文件、AOT、前后端全打进 exe”的完整流程拆给你看。二、项目准备3 分钟搞定项目仍然是前后端分离架构前端可以用Vue、React任意框架实现然后在.Net中打包在一起前端代码只是嵌入。整体流程非常简单从零开始创建以NET10为例dotnet new webapi -n AotDocsify -aot创建一个ASP.NET Core Web API(native AOT)项目。项目根目录建wwwroot文件夹把前端编译产物或任何前端 dist整包拖进 wwwroot本例中用docsify项目纯前端markdown文档库将原nginx中html下的文件复制进来三、把静态文件“嵌”进 exe打开.csproj文件手动增加一行配置将wwwroot文件夹下所有文件全部设为嵌入的资源。编译器会把所有文件写进 PE 资源段AOT 后依旧可见零反射、零动态生成放心用。ItemGroup EmbeddedResource Includewwwroot\**\* / /ItemGroup四、让 WebApplication 认识这些“内嵌”文件原后端代码不变新增静态文件通过EmbeddedFileProvider将嵌入的资源配置为静态文件。如果在EmbeddedResource不重新更改文件逻辑名的话这里就需要填写完整的默认命名空间程序集名即AotDocsify.wwwroot。这样程序就能通过输入路径返回相应嵌入文件。app.UseStaticFiles(new StaticFileOptions { FileProvider new EmbeddedFileProvider(Assembly.GetExecutingAssembly(), AotDocsify.wwwroot), RequestPath });划重点Assembly.GetExecutingAssembly().GetManifestResourceStream在 NativeAOT 里并不是“真正的反射”它只是对 编译期就已知且被嵌入到 PE 的元数据表 做的一次 常量级查找只要你在 .csproj 里把 index.html 标记成 EmbeddedResourceNativeAOT 编译器就会把该资源连同它的名字一起写进最终映像并生成一段 无反射、无动态代码生成的存根代码。因此运行时既不会触发 “反射被裁剪” 的异常也不会出现 “找不到资源” 的错误。因此本代码可直接编译。五、页面路由配置根路径 / 和 SPA 的 404 兜底都要手动接当输入服务器根地址时比如localhost:5000服务器还是会返回404。这是因为我们还未对服务器根目录路由进行配置。传统方法配置仅针对于物理文件路径因此我们需要额外手动来创建一个根目录的路由。具体代码如下using var stream Assembly.GetExecutingAssembly() .GetManifestResourceStream(AotDocsify.wwwroot.index.html); if (stream is null) throw new Exception(找不到嵌入的 index.html); string indexHtml; using (var reader new StreamReader(stream)) indexHtml reader.ReadToEnd(); app.MapGet(/, () Results.Content(indexHtml, text/html)); app.MapFallback(() Results.Content(indexHtml, text/html));通过以下代码打印我们就能看到嵌入挂载的文件及路径var names Assembly.GetExecutingAssembly().GetManifestResourceNames(); Console.WriteLine( 嵌入资源列表 ); foreach (var n in names) Console.WriteLine(n); // 嵌入资源列表 // AotDocsify.wwwroot..nojekyll // AotDocsify.wwwroot.about.contributing.md // AotDocsify.wwwroot.about.project.md // AotDocsify.wwwroot.advanced.i18n.md // AotDocsify.wwwroot.advanced.plugins.md // AotDocsify.wwwroot.advanced.theme.md // AotDocsify.wwwroot.examples.code-highlight.md // AotDocsify.wwwroot.examples.markdown.md // AotDocsify.wwwroot.examples.math.md // AotDocsify.wwwroot.features.cover.md // AotDocsify.wwwroot.features.multipage.md // AotDocsify.wwwroot.features.navbar.md // AotDocsify.wwwroot.features.sidebar.md // AotDocsify.wwwroot.guide.basic-usage.md // AotDocsify.wwwroot.guide.installation.md // AotDocsify.wwwroot.guide.quickstart.md // AotDocsify.wwwroot.README.md // AotDocsify.wwwroot._sidebar.md // AotDocsify.wwwroot.index.html发布完后删除除exe外的所有文件包括wwwroot目录。因为此时所有前端文件均已嵌入打包本例aot生成的最终exe大小约10mb运行后前端后端均可正常工作且后端所有资源可供前端调用不存在跨域问题。打开http://localhost:5000出页面打开http://localhost:5000/todos出接口返回值。六、最后本文主要介绍了用最少的代码量把“前端 dist 后端 API” 压成了一个 AOT 可执行文件部署只剩“复制 → 运行”两步。
从零开始:C#单文件AOT打包前后端分离项目
发布时间:2026/5/24 3:02:27
一、前言在 .NET 生态里官方早就给出过“前后端一把梭”的方案——Blazor Server、Blazor WebAssembly、ASP.NET Core 寄宿 IIS 等。但它们要么强依赖前端独立部署要么运行时拖家带口源码裸露、启动速度、跨域配置都是痛点。反观 Go、Rust 社区一个 app 文件就能跑完 HTTP 服务 静态站点拷贝即用编译完连源码影子都看不到。其实 C# 也能做到。今天这篇就把“单文件、AOT、前后端全打进 exe”的完整流程拆给你看。二、项目准备3 分钟搞定项目仍然是前后端分离架构前端可以用Vue、React任意框架实现然后在.Net中打包在一起前端代码只是嵌入。整体流程非常简单从零开始创建以NET10为例dotnet new webapi -n AotDocsify -aot创建一个ASP.NET Core Web API(native AOT)项目。项目根目录建wwwroot文件夹把前端编译产物或任何前端 dist整包拖进 wwwroot本例中用docsify项目纯前端markdown文档库将原nginx中html下的文件复制进来三、把静态文件“嵌”进 exe打开.csproj文件手动增加一行配置将wwwroot文件夹下所有文件全部设为嵌入的资源。编译器会把所有文件写进 PE 资源段AOT 后依旧可见零反射、零动态生成放心用。ItemGroup EmbeddedResource Includewwwroot\**\* / /ItemGroup四、让 WebApplication 认识这些“内嵌”文件原后端代码不变新增静态文件通过EmbeddedFileProvider将嵌入的资源配置为静态文件。如果在EmbeddedResource不重新更改文件逻辑名的话这里就需要填写完整的默认命名空间程序集名即AotDocsify.wwwroot。这样程序就能通过输入路径返回相应嵌入文件。app.UseStaticFiles(new StaticFileOptions { FileProvider new EmbeddedFileProvider(Assembly.GetExecutingAssembly(), AotDocsify.wwwroot), RequestPath });划重点Assembly.GetExecutingAssembly().GetManifestResourceStream在 NativeAOT 里并不是“真正的反射”它只是对 编译期就已知且被嵌入到 PE 的元数据表 做的一次 常量级查找只要你在 .csproj 里把 index.html 标记成 EmbeddedResourceNativeAOT 编译器就会把该资源连同它的名字一起写进最终映像并生成一段 无反射、无动态代码生成的存根代码。因此运行时既不会触发 “反射被裁剪” 的异常也不会出现 “找不到资源” 的错误。因此本代码可直接编译。五、页面路由配置根路径 / 和 SPA 的 404 兜底都要手动接当输入服务器根地址时比如localhost:5000服务器还是会返回404。这是因为我们还未对服务器根目录路由进行配置。传统方法配置仅针对于物理文件路径因此我们需要额外手动来创建一个根目录的路由。具体代码如下using var stream Assembly.GetExecutingAssembly() .GetManifestResourceStream(AotDocsify.wwwroot.index.html); if (stream is null) throw new Exception(找不到嵌入的 index.html); string indexHtml; using (var reader new StreamReader(stream)) indexHtml reader.ReadToEnd(); app.MapGet(/, () Results.Content(indexHtml, text/html)); app.MapFallback(() Results.Content(indexHtml, text/html));通过以下代码打印我们就能看到嵌入挂载的文件及路径var names Assembly.GetExecutingAssembly().GetManifestResourceNames(); Console.WriteLine( 嵌入资源列表 ); foreach (var n in names) Console.WriteLine(n); // 嵌入资源列表 // AotDocsify.wwwroot..nojekyll // AotDocsify.wwwroot.about.contributing.md // AotDocsify.wwwroot.about.project.md // AotDocsify.wwwroot.advanced.i18n.md // AotDocsify.wwwroot.advanced.plugins.md // AotDocsify.wwwroot.advanced.theme.md // AotDocsify.wwwroot.examples.code-highlight.md // AotDocsify.wwwroot.examples.markdown.md // AotDocsify.wwwroot.examples.math.md // AotDocsify.wwwroot.features.cover.md // AotDocsify.wwwroot.features.multipage.md // AotDocsify.wwwroot.features.navbar.md // AotDocsify.wwwroot.features.sidebar.md // AotDocsify.wwwroot.guide.basic-usage.md // AotDocsify.wwwroot.guide.installation.md // AotDocsify.wwwroot.guide.quickstart.md // AotDocsify.wwwroot.README.md // AotDocsify.wwwroot._sidebar.md // AotDocsify.wwwroot.index.html发布完后删除除exe外的所有文件包括wwwroot目录。因为此时所有前端文件均已嵌入打包本例aot生成的最终exe大小约10mb运行后前端后端均可正常工作且后端所有资源可供前端调用不存在跨域问题。打开http://localhost:5000出页面打开http://localhost:5000/todos出接口返回值。六、最后本文主要介绍了用最少的代码量把“前端 dist 后端 API” 压成了一个 AOT 可执行文件部署只剩“复制 → 运行”两步。