2026-04-30
C#
0

目录

🤔 你有没有遇到过这种情况?
不好,而是它太"正式"了**——哪怕是最简单的小工具,也要搭一套完整的工程脚手架。
💡 核心机制:它到底是怎么工作的?
:` 指令体系
🚀 解决方案设计:三个渐进式实战场景
方案一:系统信息脚本(零依赖入门)
方案二:JSON 转 CSV 数据处理(引用 NuGet 包)
方案三:带参数解析的 CLI 工具(System.CommandLine)
📦 进阶能力:从脚本到工程的平滑迁移
🚫 理性认知:它不是银弹
和 VB.NET |
📁 目录结构最佳实践
🎯 三句话总结
🗣️ 开放讨论

🤔 你有没有遇到过这种情况?

团队里有个临时任务——把一批 JSON 销售数据转成 CSV 报表,逻辑不复杂,就几十行代码的事儿。

但你打开电脑,先 dotnet new console,等项目创建完,再建 .sln,配 .csproj,装 NuGet,写代码,调试……前后折腾了十几分钟,就为了跑一个一次性脚本。

旁边的同事用 Python 写了三行,早跑完了。

这种情况,相信很多 C# 开发者都经历过。不是 C# 不好,而是它太"正式"了——哪怕是最简单的小工具,也要搭一套完整的工程脚手架。

现在,.NET 10 给出了答案:File-Based Apps。

读完这篇文章,你将掌握:

  • File-Based Apps 的核心机制与完整指令体系
  • 3 个可直接落地的实战场景(含完整可运行代码)
  • 何时用、何时不用的决策判断

🧐 问题深度剖析:C# 的"仪式感税"

在 .NET 10 之前,哪怕是一行 Console.WriteLine("Hello") 背后,也隐藏着一套固定成本:

  • 一个 .sln 文件(解决方案)
  • 一个 .csproj 文件(项目配置,XML 格式)
  • 一个 Program.cs(入口代码)
  • 隐式的 dotnet restore + dotnet build

C# 9 引入了"顶层语句(Top-level Statements)",省掉了 class Programstatic void Main,已经是一大进步。但工程文件的开销依然存在。

这直接导致了几个现实问题:

其一,脚本场景体验极差。 日常开发中大量存在"一次性任务":数据迁移脚本、日志分析工具、API 测试小工具、环境检查脚本。为这些任务建完整工程,成本远超价值。

其二,入门门槛偏高。 对于刚接触 C# 的开发者,在写第一行代码前就要理解 MSBuild、SDK、项目结构,认知负担不小。

其三,被迫切换语言。 很多 .NET 团队在需要快速脚本时,转而使用 Python 或 Bash,但这意味着离开了熟悉的类型系统和生态,反而引入了新的维护成本。

File-Based Apps 就是专门为这个痛点设计的。


💡 核心机制:它到底是怎么工作的?

File-Based Apps 的核心思路极其简单:一个 .cs 文件就是一个完整的可执行程序,不需要任何项目文件。

bash
dotnet hello.cs

就这一行命令。SDK 在后台自动完成以下工作:

  1. 解析文件顶部的 #: 指令
  2. 在临时目录生成一个虚拟 .csproj
  3. 执行 restore + build
  4. 运行生成的可执行文件
  5. 缓存编译结果,下次运行(未修改时)几乎瞬间启动

缓存路径为:<temp>/dotnet/runfile/<appname>-<filehash>/

这意味着第一次运行有编译耗时,后续运行速度与正常程序无异。

#: 指令体系

File-Based Apps 通过文件顶部的特殊指令来完成原本在 .csproj 里做的事:

指令作用示例
#:package引用 NuGet 包#:package Newtonsoft.Json@13.0.3
#:sdk指定 SDK#:sdk Microsoft.NET.Sdk.Web
#:property设置 MSBuild 属性#:property Nullable=enable
#:project引用其他项目#:project ../Shared/Shared.csproj

版本号支持灵活写法:

csharp
#:package Newtonsoft.Json@13.0.3 // 固定版本 #:package CsvHelper@33.* // 主版本内最新 #:package Spectre.Console@* // 最新稳定版

还有一个对 Unix/Linux/macOS 用户特别友好的功能——Shebang 支持

csharp
#!/usr/bin/env dotnet Console.WriteLine("我是一个可直接执行的 C# 脚本!");
bash
chmod +x script.cs ./script.cs

注意:Shebang 需要 LF 换行符(非 CRLF),Windows 上不支持。


🚀 解决方案设计:三个渐进式实战场景

方案一:系统信息脚本(零依赖入门)

这是最轻量的场景,无需任何外部包,纯标准库实现。适合快速验证环境、检查 SDK 版本、生成部署前置检查报告。

csharp
// sysinfo.cs — 运行:dotnet sysinfo.cs Console.WriteLine("=== 系统信息检查 ==="); Console.WriteLine(); Console.WriteLine($"机器名: {Environment.MachineName}"); Console.WriteLine($"当前用户: {Environment.UserName}"); Console.WriteLine($"操作系统: {Environment.OSVersion}"); Console.WriteLine($".NET 版本: {Environment.Version}"); Console.WriteLine($"处理器数量: {Environment.ProcessorCount}"); Console.WriteLine($"当前目录: {Environment.CurrentDirectory}"); Console.WriteLine(); Console.WriteLine($"PATH 条目数: {Environment.GetEnvironmentVariable("PATH") ?.Split(Path.PathSeparator).Length ?? 0}"); Console.WriteLine($"临时目录: {Path.GetTempPath()}");

运行方式:

bash
dotnet sysinfo.cs

输出示例:

image.png

这种脚本以前要建工程才能跑,现在一个文件搞定,放到任何机器上 dotnet sysinfo.cs 直接用。


方案二:JSON 转 CSV 数据处理(引用 NuGet 包)

这是 File-Based Apps 最典型的使用场景——数据格式转换。只需在文件顶部加一行 #:package,就能拉取 NuGet 包。

测试环境:.NET 10 SDK 10.0.x,Windows 11,Intel i7-12700

先准备测试数据 sales.json

json
[ { "Product": "笔记本电脑", "Amount": 6999.00, "Date": "2026-01-15" }, { "Product": "机械键盘", "Amount": 399.00, "Date": "2026-01-15" }, { "Product": "笔记本电脑", "Amount": 6999.00, "Date": "2026-01-16" }, { "Product": "无线鼠标", "Amount": 199.00, "Date": "2026-01-16" }, { "Product": "机械键盘", "Amount": 399.00, "Date": "2026-01-17" } ]
csharp
// data-processor.cs — 运行:dotnet data-processor.cs #:package CsvHelper@33.0.0 using System.Text.Json; using System.Text.Json.Serialization; using CsvHelper; using System.Globalization; // 读取 JSON var json = await File.ReadAllTextAsync("sales.json"); var sales = JsonSerializer.Deserialize(json, SaleJsonContext.Default.ListSale)!; // LINQ 聚合:按产品分组,计算总收入 var summary = sales .GroupBy(s => s.Product) .Select(g => new SaleSummary(g.Key, g.Sum(s => s.Amount), g.Count())) .OrderByDescending(x => x.TotalRevenue); // 写出 CSV using var writer = new StreamWriter("summary.csv"); using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture); csv.WriteRecords(summary); Console.WriteLine("处理完成!请查看 summary.csv"); // 数据模型定义 record Sale(string Product, decimal Amount, DateTime Date); record SaleSummary(string Product, decimal TotalRevenue, int Count); // 因为 File-Based Apps 默认启用 Native AOT, // 需要用 Source Generator 替代反射序列化 [JsonSerializable(typeof(List<Sale>))] partial class SaleJsonContext : JsonSerializerContext { }

image.png

输出的 summary.csv

Product,TotalRevenue,Count 笔记本电脑,13998.00,2 机械键盘,798.00,2 无线鼠标,199.00,1

⚠️ 踩坑预警:Native AOT 与反射序列化的冲突

File-Based Apps 默认开启 Native AOT 发布。这意味着 JsonSerializer.Deserialize<T>(json) 这种反射式写法在 dotnet publish 时会报错。解决方案有两个:

  • 推荐:如代码所示,使用 [JsonSerializable] Source Generator,AOT 友好
  • 备选:在文件顶部加 #:property PublishAot=false,关闭 AOT(牺牲发布体积优势)

注意,第一次运行会非常慢!File-Based Apps 首次运行时,SDK 在后台自动完成了三件事:

  1. 生成虚拟 .csproj — 把你的 #:package 指令翻译成标准项目文件
  2. NuGet Restore — 从网络下载 CsvHelper@33.0.0 及其所有依赖,写入本地缓存
  3. 编译 + AOT 处理 — 因为默认启用 Native AOT,编译链路比普通模式更长
  4. 其中 NuGet Restore 是大头,尤其是网络状况不佳或首次使用该包时,100 秒以上很常见。

方案三:带参数解析的 CLI 工具(System.CommandLine

当脚本需要规范的命令行参数支持时,手动解析 args[] 既繁琐又容易出错。引入 System.CommandLine 包,可以用极少的代码实现专业级 CLI 体验——包括自动生成 --help、参数验证、Tab 补全等。

csharp
// file-stats.cs — 统计文本文件的行数、词数、字符数 // 运行:dotnet file-stats.cs -- --file myfile.txt --verbose #:package System.CommandLine@2.0.0 using System.CommandLine; var fileOption = new Option<FileInfo?>("--file") { Description = "要分析的文件路径" }; var verboseOption = new Option<bool>("--verbose") { Description = "显示详细信息" }; var rootCommand = new RootCommand("文件统计工具 - 统计行数、词数与字符数") { fileOption, verboseOption }; rootCommand.SetAction(async (parseResult, cancellationToken) => { var file = parseResult.GetValue(fileOption); var verbose = parseResult.GetValue(verboseOption); if (file is null) { Console.WriteLine("请通过 --file 指定文件路径"); return 1; } if (!file.Exists) { Console.WriteLine($"文件不存在:{file.FullName}"); return 1; } if (verbose) Console.WriteLine($"正在处理:{file.FullName}"); var lines = await File.ReadAllLinesAsync(file.FullName, cancellationToken); var words = lines.Sum(l => l.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length); var chars = lines.Sum(l => l.Length); Console.WriteLine($"行数:{lines.Length}"); Console.WriteLine($"词数:{words}"); Console.WriteLine($"字符数:{chars}"); return 0; }); return await rootCommand.Parse(args).InvokeAsync();

运行方式:

bash
# 统计文件 dotnet file-stats.cs -- --file README.md --verbose # 查看帮助(自动生成) dotnet file-stats.cs -- --help

--help 自动生成的输出:

image.png

注意命令中的 -- 分隔符:它告诉 dotnet CLI,后面的参数是传给你的程序的,而不是 dotnet 自己的选项。


📦 进阶能力:从脚本到工程的平滑迁移

当一个脚本越写越大,逻辑越来越复杂,需要拆分多文件时(目前 .NET 10 仍是单文件限制,多文件支持计划在 .NET 11 加入),可以一键转换为标准项目:

bash
dotnet project convert my-script.cs # 可指定输出目录 dotnet project convert my-script.cs --output MyProject

这条命令会自动:

  • 生成标准 .csproj 文件,#: 指令转换为对应的 XML 节点
  • 保留你的代码逻辑,移入 Program.cs
  • 结构完整,可立即用 Visual Studio 打开

这是一个单向的平滑升级通道——脚本长大了,就毕业为工程,不需要重写。


🚫 理性认知:它不是银弹

File-Based Apps 的定位很清晰,.NET 团队也明确表示,它是脚本场景的补充,不是工程开发的替代

适合使用的场景:

  • 一次性数据处理脚本(CSV/JSON 转换、日志分析)
  • 快速原型验证与 API 测试
  • CI/CD 构建脚本
  • 教学演示与入门示例
  • 个人效率工具(密码生成器、文件批处理等)

不建议使用的场景:

  • 生产环境的核心业务逻辑
  • 需要多文件协作的复杂模块
  • 需要完整 IDE 调试体验的项目(Visual Studio 目前尚不支持,VS Code 支持但体验有限)
  • 团队协作的共享代码库

当前已知限制(.NET 10):

限制项说明
单文件限制多文件支持计划在 .NET 11 加入
无 Visual Studio 支持仅支持 VS Code 和命令行
仅支持 C#暂不支持 F# 和 VB.NET
IntelliSense 有已知问题VS Code 中动态包引用的智能提示偶有缺失

📁 目录结构最佳实践

有一个容易踩的坑需要特别提醒:不要把 File-Based Apps 的 .cs 文件放在已有项目目录内部。

❌ 错误做法: MyProject/ ├── MyProject.csproj ├── Program.cs └── scripts/ └── utility.cs ← SDK 会混淆,可能触发父项目构建 ✅ 正确做法: MyProject/ ├── MyProject.csproj └── Program.cs scripts/ ← 与项目并列,独立目录 ├── utility.cs ├── data-processor.cs └── sysinfo.cs

在 repo 根目录维护一个独立的 scripts/ 文件夹,是目前最清晰的组织方式。


🎯 三句话总结

第一句:File-Based Apps 不是革命,而是一次迟来的补完——让 C# 终于能优雅地处理"不值得建工程"的场景。

第二句#:package#:sdk#:property 三个指令,覆盖了 95% 的脚本需求,学习成本极低。

第三句:它和传统工程开发不是对立关系,而是同一条路上的不同起点——脚本长大了,dotnet project convert 一键毕业。



🗣️ 开放讨论

话题一:在你的日常开发中,有哪些场景是你现在用 Python/Bash 写,但其实更想用 C# 写的?

话题二:File-Based Apps 默认启用 Native AOT,这对你的脚本使用场景是利大于弊,还是弊大于利?

欢迎在评论区分享你的实践经验和想法。


#C#开发 #.NET10 #编程技巧 #脚本开发 #性能优化

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!