团队里有个临时任务——把一批 JSON 销售数据转成 CSV 报表,逻辑不复杂,就几十行代码的事儿。
但你打开电脑,先 dotnet new console,等项目创建完,再建 .sln,配 .csproj,装 NuGet,写代码,调试……前后折腾了十几分钟,就为了跑一个一次性脚本。
旁边的同事用 Python 写了三行,早跑完了。
这种情况,相信很多 C# 开发者都经历过。不是 C# 不好,而是它太"正式"了——哪怕是最简单的小工具,也要搭一套完整的工程脚手架。
现在,.NET 10 给出了答案:File-Based Apps。
读完这篇文章,你将掌握:
在 .NET 10 之前,哪怕是一行 Console.WriteLine("Hello") 背后,也隐藏着一套固定成本:
.sln 文件(解决方案).csproj 文件(项目配置,XML 格式)Program.cs(入口代码)dotnet restore + dotnet buildC# 9 引入了"顶层语句(Top-level Statements)",省掉了 class Program 和 static void Main,已经是一大进步。但工程文件的开销依然存在。
这直接导致了几个现实问题:
其一,脚本场景体验极差。 日常开发中大量存在"一次性任务":数据迁移脚本、日志分析工具、API 测试小工具、环境检查脚本。为这些任务建完整工程,成本远超价值。
其二,入门门槛偏高。 对于刚接触 C# 的开发者,在写第一行代码前就要理解 MSBuild、SDK、项目结构,认知负担不小。
其三,被迫切换语言。 很多 .NET 团队在需要快速脚本时,转而使用 Python 或 Bash,但这意味着离开了熟悉的类型系统和生态,反而引入了新的维护成本。
File-Based Apps 就是专门为这个痛点设计的。
File-Based Apps 的核心思路极其简单:一个 .cs 文件就是一个完整的可执行程序,不需要任何项目文件。
bashdotnet hello.cs
就这一行命令。SDK 在后台自动完成以下工作:
#: 指令.csprojrestore + build缓存路径为:<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# 脚本!");
bashchmod +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()}");
运行方式:
bashdotnet sysinfo.cs
输出示例:

这种脚本以前要建工程才能跑,现在一个文件搞定,放到任何机器上 dotnet sysinfo.cs 直接用。
这是 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 { }

输出的 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 在后台自动完成了三件事:
.csproj — 把你的 #:package 指令翻译成标准项目文件CsvHelper@33.0.0 及其所有依赖,写入本地缓存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 自动生成的输出:

注意命令中的 -- 分隔符:它告诉 dotnet CLI,后面的参数是传给你的程序的,而不是 dotnet 自己的选项。
当一个脚本越写越大,逻辑越来越复杂,需要拆分多文件时(目前 .NET 10 仍是单文件限制,多文件支持计划在 .NET 11 加入),可以一键转换为标准项目:
bashdotnet project convert my-script.cs
# 可指定输出目录
dotnet project convert my-script.cs --output MyProject
这条命令会自动:
.csproj 文件,#: 指令转换为对应的 XML 节点Program.cs这是一个单向的平滑升级通道——脚本长大了,就毕业为工程,不需要重写。
File-Based Apps 的定位很清晰,.NET 团队也明确表示,它是脚本场景的补充,不是工程开发的替代。
适合使用的场景:
不建议使用的场景:
当前已知限制(.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 许可协议。转载请注明出处!