Run and File-Based Apps
|
This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The C# Reference bibliography lists the reference material consulted while preparing these pages. |
dotnet run builds a project and starts it in one step — the command for the inner loop. Since .NET 10 it also
runs a single .cs file with no project at all: a file-based app, with its packages and settings declared in
#: directives at the top of the file. This page runs both with .NET SDK 10.0.402 on Ledgerline.
dotnet run
dotnet run restores and builds the project in the current folder (or --project), then runs the output. Everything
after -- is passed to the application:
dotnet run --project src/Ledgerline.Cli -- check samples/Ledgerline.Report/journal.ledger.csv
OK: 4 lines, every entry balances.
Without --, arguments that are not dotnet run options are also forwarded (dotnet run --project
src/Ledgerline.Cli check reached the program as check), but an argument that is a dotnet run option — -c, -f, -r, -e, -v — is taken by dotnet run. Always use -- in scripts.
dotnet run exits with the application’s exit code. --no-build runs the existing output (and implies
--no-restore).
Launch profiles
Properties/launchSettings.json holds named profiles with arguments and environment variables — the same file Visual
Studio uses. Ledgerline.Cli gets two:
{
"profiles": {
"check-sample": {
"commandName": "Project",
"commandLineArgs": "check ../../samples/Ledgerline.Report/journal.ledger.csv",
"environmentVariables": { "LEDGERLINE_CULTURE": "en-GB" }
},
"balance-sample": {
"commandName": "Project",
"commandLineArgs": "balance ../../samples/Ledgerline.Report/journal.ledger.csv"
}
}
}
cd src/Ledgerline.Cli
dotnet run
dotnet run --launch-profile balance-sample
dotnet run --no-launch-profile
dotnet run -e LEDGERLINE_CULTURE=fr-FR --launch-profile check-sample
Using launch settings from ~/ledgerline/src/Ledgerline.Cli/Properties/launchSettings.json...
OK: 4 lines, every entry balances.
Using launch settings from ~/ledgerline/src/Ledgerline.Cli/Properties/launchSettings.json...
[
{
"Account": "1000-Cash",
…
usage: ledgerline check|balance <journal.csv>
Using launch settings from ~/ledgerline/src/Ledgerline.Cli/Properties/launchSettings.json...
OK: 4 lines, every entry balances.
Without --launch-profile the first profile runs (or the one named by DOTNET_LAUNCH_PROFILE), so a
launchSettings.json changes what a plain dotnet run does. --no-launch-profile ignores the file; -e NAME=VALUE
adds or overrides environment variables. The "Using launch settings…" line goes to standard output on .NET 10 (.NET 11
moves it to standard error), so filter it when piping the program’s output.
File-based apps
A file-based app is one .cs file with top-level statements. The SDK generates a virtual project for it, builds
it and runs it. Directives at the top of the file replace the project file:
| Directive | Equivalent |
|---|---|
|
|
|
|
|
a property in the project (MSBuild expressions allowed) |
|
the project SDK (default |
|
add more source files (paths, globs, MSBuild properties) |
A script with a shebang
#!/usr/bin/env -S dotnet run
#:property TargetFramework=net10.0
// Usage: ./balance.cs <amount> <amount> ...
decimal total = args.Sum(decimal.Parse);
Console.WriteLine($"{args.Length} amounts, total {total:0.00}");
return total == 0 ? 0 : 1;
chmod +x balance.cs
./balance.cs 120.00 -120.00; echo "exit=$?"
./balance.cs 10 -3; echo "exit=$?"
dotnet run balance.cs -- 1 -1
2 amounts, total 0.00
exit=0
2 amounts, total 7.00
exit=1
2 amounts, total 0.00
The first run took 2.2 s (restore and compile); later runs of an unchanged file start from the cache. -S lets env
pass dotnet run as two words; without it, use #!/usr/bin/env dotnet, which may let dotnet consume arguments
that look like its own options.
Packages: Ledgerline from the local feed
samples/quick-ledger.cs uses the Ledgerline package. As first written, with a version:
#:package Ledgerline@1.0.0
using Ledgerline;
var lines = LedgerReader.Parse("""
date,entry,account,debit,credit,memo
2026-02-01,E1,1000-Cash,75.00,,Sale
2026-02-01,E1,4000-Sales,,75.00,Sale
""");
Console.WriteLine(Ledger.ToJson(Ledger.TrialBalance(lines)));
it failed, because the file lives inside the Ledgerline repository and inherits its Directory.Packages.props:
~/ledgerline/samples/quick-ledger.cs.csproj : error NU1008: The following PackageReference items cannot define a value for Version: Ledgerline. Projects using Central Package Management must define a Version value on a PackageVersion item. For more information, visit https://aka.ms/nuget/cpm/gettingstarted
The build failed. Fix the build errors and run again.
With #:package Ledgerline (the version now comes from Directory.Packages.props, and the package from the local
feed named in the repository’s nuget.config), the build succeeded and the program crashed:
Unhandled exception. System.InvalidOperationException: Reflection-based serialization has been disabled for this application. Either use the source generator APIs or explicitly configure the 'JsonSerializerOptions.TypeInfoResolver' property.
at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_JsonSerializerIsReflectionDisabled()
…
at Ledgerline.Ledger.ToJson(IReadOnlyList`1 balances)
at Program.<Main>$(String[] args) in ~/ledgerline/samples/quick-ledger.cs:line 10
File-based apps set PublishAot=true by default, and that setting applies its consequences even to dotnet run:
JsonSerializerIsReflectionEnabledByDefault is false. Ledgerline.ToJson uses reflection-based
System.Text.Json, so it throws. A script that uses a library which is not AOT-compatible opts out:
#:package Ledgerline
#:property PublishAot=false
[
{
"Account": "1000-Cash",
"Debit": 75.00,
"Credit": 0,
"Balance": 75.00
},
{
"Account": "4000-Sales",
"Debit": 0,
"Credit": 75.00,
"Balance": -75.00
}
]
The crash is also a finding about the library: Ledgerline should use a source-generated JsonSerializerContext and
declare IsAotCompatible
(Designing a Library for NuGet).
Project references and more files
#:project builds a project and references it, which suits scripts that live in the repository:
#:project ../src/Ledgerline/Ledgerline.csproj
#:property PublishAot=false
using Ledgerline;
var lines = LedgerReader.ReadFile(args[0]);
Console.WriteLine($"{lines.Count} lines, unbalanced: {Ledger.FindUnbalancedEntries(lines).Count}");
dotnet run samples/project-ref.cs -- samples/Ledgerline.Report/journal.ledger.csv
4 lines, unbalanced: 0
#:include adds source files. The SDK what’s-new pages list it for .NET 11, but it already worked with SDK 10.0.402 — together with a new analyzer warning on the entry file:
#:include helpers.cs
Console.WriteLine(Helpers.Hello());
~/fba4/main.cs(1,1): warning CA2266: File-based program entry point should start with '#!' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2266)
hi
Source: File-based apps: project, include, sdk.
Implicit build files and the cache
A file-based app is affected by the same files as a project in its folder: Directory.Build.props,
Directory.Build.targets, Directory.Packages.props, nuget.config and global.json — the reason for the NU1008
above. The documentation recommends keeping file-based apps outside a project’s folder ("cone") and being
deliberate about which repository files apply.
The build output is cached per file, keyed by the file’s content, its directives, the SDK version and the implicit
build files. The documentation puts it under <temp>/dotnet/runfile; on this Linux machine (no TMPDIR) it was
~/.local/share/dotnet/runfile/<name>-<hash>/:
~/.local/share/dotnet/runfile/balance-0411735fedf09e5619650715df8ce7e318e87276747834e328f13b970f429370/
bin/ build-start.cache build-success.cache csc.rsp obj/
csc.rsp is there because an unchanged, simple file-based app can be rebuilt by calling the compiler directly, without
MSBuild. When a change in an implicit build file is not picked up, dotnet clean balance.cs cleans one app, and
dotnet clean file-based-apps (with --days, default 30) removes unused cache folders.
Publishing, packing and converting
dotnet publish on a file-based app produces a Native AOT executable in artifacts/<name>/ next to the file:
dotnet publish balance.cs
./artifacts/balance/balance 5 -5
balance.cs -> ~/.local/share/dotnet/runfile/balance-0411…/bin/release/balance.dll
Generating native code
balance.cs -> ~/fba/artifacts/balance/
2 amounts, total 0.00
The folder held balance (1.3 MB, a native ELF executable) and balance.dbg (2.6 MB of native debug symbols). Native
AOT needs the platform linker toolchain (clang here)
(Publish and Deployment Models).
PackAsTool=true is also a default, so dotnet pack makes a .NET tool package:
The package balance.1.0.0 is missing a readme. Go to https://aka.ms/nuget/authoring-best-practices/readme to learn why package readmes are important.
Successfully created package '~/fba/artifacts/balance/balance.1.0.0.nupkg'.
Packed after the publish above, the package contained tools/net10.0/any/balance.dbg — the stale 2.6 MB native
debug file from the shared artifacts/balance/ folder. Packed in a fresh folder, the equivalent package held only the
.dll, .pdb, deps.json, runtimeconfig.json and DotnetToolSettings.xml. Pack from a clean folder.
When a script outgrows one file, dotnet project convert turns it into a project, writing the implicit settings out
explicitly:
dotnet project convert conv.cs
cat conv/conv.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<PackAsTool>true</PackAsTool>
<UserSecretsId>conv-87e2ae13133a646c22ccf5c2e689b114fdf6d218b505654f1c8030f49a82cc09</UserSecretsId>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
</Project>
The directives became project settings and were removed from conv/conv.cs; the original conv.cs was left in place.
Review PublishAot, PackAsTool and UserSecretsId — most projects want none of them.
Common mistakes
-
Arguments without
--— options such as-care taken bydotnet run. -
A
launchSettings.jsonnobody knows about — plaindotnet runuses its first profile. -
#:package Name@versioninside a CPM repository — NU1008. -
A file-based app using reflection-based JSON — AOT is on by default; set
PublishAot=falseor use source generation. -
Scripts inside a project folder — the project’s build files apply to them.
-
Packing after publishing in the same folder — stale files end up in the tool package.