Every .NET developer has been there. You want to test a quick idea, parse a file, or prototype an API call -- and before you write a single line of meaningful code, you are creating a project, waiting for restore, and staring at a Program.cs wrapped in boilerplate you did not ask for. Python developers just open a .py file and run it. Node developers do the same with .js. For years, C# made you pay an entry fee that scripting languages waived entirely.

.NET 10 changes that. With file-based apps, you can write a .cs file, run it with dotnet run app.cs, and get on with your day. No .csproj. No obj folder. No ceremony. And unlike third-party tools such as dotnet-script, this is baked directly into the SDK -- it works everywhere .NET does, with full NuGet support, native AOT publishing, and a clean upgrade path to a proper project when your script outgrows a single file.

The basics

At its simplest, a file-based app is just a C# file with top-level statements:

hello.cs
Console.WriteLine("Hello from a single file.");

Run it:

terminal
dotnet run hello.cs

That is genuinely it. The SDK generates a virtual project behind the scenes, compiles and runs the code, and caches the build output so subsequent runs are fast. You can also use the shorthand dotnet hello.cs, which does the same thing.

If a .csproj file already exists in the current directory, dotnet run hello.cs will run that project and pass hello.cs as an argument to preserve backwards compatibility. Use the explicit --file flag to avoid ambiguity:

terminal
dotnet run --file hello.cs

Directives: your inline project file

The real power emerges when you need NuGet packages, different SDKs, or custom MSBuild properties. File-based apps use #: directives at the top of the file to replace what would normally live in a .csproj.

Adding NuGet packages

The #:package directive adds a NuGet reference:

report.cs
#:package Spectre.Console@0.49.1
#:package CsvHelper@33.0.1

using Spectre.Console;
using CsvHelper;
using System.Globalization;

using var reader = new StreamReader("sales.csv");
using var csv = new CsvReader(reader, CultureInfo.InvariantCulture);
var records = csv.GetRecords<dynamic>().ToList();

AnsiConsole.MarkupLine($"[green]Loaded {records.Count} rows[/]");

Specify the version after an @ symbol. If you are using central package management with a Directory.Packages.props file, you can omit the version. Otherwise, append @* to pull the latest version -- though pinning a specific version is almost always the better choice for reproducibility.

Choosing an SDK

The default SDK is Microsoft.NET.Sdk, which covers console-style apps. Need ASP.NET Core? Switch the SDK:

api.cs
#:sdk Microsoft.NET.Sdk.Web

var app = WebApplication.CreateBuilder(args).Build();

app.MapGet("/", () => "Hello from a single-file API");

app.Run();

This gives you access to the full Minimal API surface, dependency injection, middleware -- everything you would have in a project-based web app. You can even use Aspire.AppHost.Sdk with a version number to run an Aspire app host from a single file.

Setting MSBuild properties

The #:property directive sets any MSBuild property:

Example.cs
#:property LangVersion=preview
#:property Nullable=disable

This is the escape hatch for anything the SDK normally controls through the project file. Need to target a specific framework? Set TargetFramework. Want to disable native AOT for publishing? Set PublishAot=false. You can even use MSBuild property functions for conditional values:

Example.cs
#:property LogLevel=$([MSBuild]::ValueOrDefault('$(LOG_LEVEL)', 'Information'))

Referencing other projects

The #:project directive references a traditional .csproj:

integration-check.cs
#:project ../SharedLibrary/SharedLibrary.csproj

using SharedLibrary;

var result = Validator.Check("test-input");
Console.WriteLine(result);

This is particularly useful for quick integration checks or utility scripts that need access to your main application's types without duplicating code.

Shebang support on Unix

On Linux and macOS, file-based apps support shebang lines for direct execution:

Example.cs
#!/usr/bin/env dotnet
#:package Spectre.Console@0.49.1

using Spectre.Console;

AnsiConsole.MarkupLine("[bold yellow]Running as a shell script[/]");

Make it executable with chmod +x script.cs and run it as ./script.cs. Two things to watch: the file must use LF line endings (not CRLF), and it must not include a byte order mark (BOM). If your script runs fine with dotnet run but fails with ./script.cs, check the line endings first.

Publishing and packaging

File-based apps are not just for development -- they are first-class publishable artefacts. Run dotnet publish script.cs and you get a self-contained executable. The default publishing mode is native AOT, which produces a small, fast binary with no runtime dependency.

If native AOT does not suit your needs (perhaps you rely on reflection-heavy libraries), disable it:

Example.cs
#:property PublishAot=false

You can also package a file-based app as a .NET tool with dotnet pack script.cs. The SDK sets PackAsTool=true by default, so the output is ready for dotnet tool install without any additional configuration.

Piping code from stdin

For truly ad-hoc scenarios, you can pipe C# directly into the CLI:

terminal
echo 'Console.WriteLine(DateTime.UtcNow);' | dotnet run -

The - argument tells dotnet run to read from standard input. This is handy for shell scripts that generate C# dynamically, or for quick one-liners where even creating a file feels like too much friction.

Launch profiles

File-based apps support launch profiles through a flat configuration file. Instead of Properties/launchSettings.json, create a file named {appname}.run.json alongside your .cs file:

api.run.json
{
  "profiles": {
    "https": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5001;http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

Run with a specific profile using dotnet run api.cs --launch-profile https. Each file-based app in a directory can have its own launch profile, keeping configurations isolated.

When your script outgrows a single file

The conversion story is one of the strongest parts of this feature. When a file-based app grows complex enough to need multiple files, tests, or a proper solution structure, run:

terminal
dotnet project convert app.cs

This creates a new directory named after your app, generates a .csproj with equivalent SDK references, properties, and NuGet packages, and copies your code into a Program.cs file. The original .cs file is left untouched. All #: directives are translated into their MSBuild equivalents automatically.

The transition is smooth and deliberate. You do not have to plan ahead for it, and you do not lose any configuration in the process.

Build caching and the virtual project

Under the hood, the SDK generates a virtual MSBuild project each time you run a file-based app. Build outputs are cached based on the source file content, directive configuration, SDK version, and any implicit build files in the directory tree. Subsequent runs skip compilation entirely if nothing has changed.

The cache lives in your system's temp directory under <temp>/dotnet/runfile/<appname>-<hash>/. If you ever see stale behaviour, clear it with:

terminal
dotnet clean file-based-apps

You can also target a specific file with dotnet clean app.cs. The --days flag lets you clean up only artefacts older than a threshold (the default is 30 days).

One gotcha with caching: concurrent runs of the same file-based app can cause contention over build output files. If you need to run multiple instances in parallel, build first and then run with --no-build:

terminal
dotnet build app.cs
dotnet run app.cs --no-build &
dotnet run app.cs --no-build &

Common pitfalls

Placing scripts inside project directories. File-based apps inherit Directory.Build.props, global.json, and other implicit build files from parent directories. If your script sits inside a traditional project's directory tree, it will pick up that project's build configuration -- often with confusing results. Keep file-based apps in a separate directory, outside any .csproj cone.

Forgetting the version on #:package. Without central package management, omitting the version does not automatically pull the latest -- it fails. Either pin a specific version with @2.14.1 or use @* for the latest, but be aware that @* makes builds non-reproducible.

CRLF line endings with shebangs. Windows editors default to CRLF. If you author a shebang script on Windows and deploy it to Linux, it will not execute. Configure your editor to use LF for .cs files that will run as shell scripts, or set up a .gitattributes rule.

Expecting multi-file support. .NET 10 supports exactly one .cs file per file-based app. If you need to split code across files, you need a project. Multi-file support is planned for .NET 11.

Assuming Visual Studio support. At the time of writing, file-based apps are a first-class experience in VS Code and the CLI. Visual Studio support is more limited -- the full IDE experience with IntelliSense and debugging works best from the command line and VS Code.

What is coming in .NET 11

The .NET 11 previews are already extending this feature. Multi-file support is the headline addition, allowing file-based apps to span more than one .cs file while keeping the project-free workflow. JetBrains Rider 2026.1 has already shipped support for running file-based C# programs, and broader IDE support across the ecosystem will likely follow.

The combination of multi-file support and the existing #:project directive should close most of the gaps that currently push developers toward a full project structure for anything beyond a quick script.

Summary