Run and File-Based Apps

This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its dotnet CLI) and NuGet 7.9, as published in the MSBuild, .NET and NuGet documentation on learn.microsoft.com, which is the reference these pages are written and verified against. Material about .NET 11 / NuGet 8.0 is still in preview and is always flagged as such — never presented as baseline.

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

#:package Name@1.2.3

<PackageReference Include="Name" Version="1.2.3" /> (version optional with CPM)

#:project ../path/App.csproj

<ProjectReference>

#:property Name=Value

a property in the project (MSBuild expressions allowed)

#:sdk Microsoft.NET.Sdk.Web

the project SDK (default Microsoft.NET.Sdk; Name@version for others)

#:include helpers.cs

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

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 -c are taken by dotnet run.

  • A launchSettings.json nobody knows about — plain dotnet run uses its first profile.

  • #:package Name@version inside a CPM repository — NU1008.

  • A file-based app using reflection-based JSON — AOT is on by default; set PublishAot=false or 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.