Exploring C# File-Based Apps in .NET 10: Run a Single .cs File
Learn how C# file-based apps in .NET 10 let you run a single .cs file with dotnet run, add packages with directives, and grow into a full project.
Imagine you want to write a short note to a friend. You do not buy a notebook, punch holes in the pages, and bind them together first. You just grab one piece of paper and start writing. If the note grows into a long story, then you bind the pages into a book.
For years, writing C# felt like you always had to bind the book first. Before you could run even one line, you needed a project file, folders, and some setup. That was a lot of work just to try a small idea.
.NET 10 changes this. Now you can grab "one piece of paper" — a single .cs file — and run it right away. This feature is called file-based apps. In this post you will learn what they are, how they work, and when to use them, all in simple steps.
What is a file-based app?
A file-based app is a C# program that lives in one file and runs without a project file. You write your code in something like hello.cs, and then you run it:
// hello.cs
Console.WriteLine("Hello from a single file!");dotnet run hello.csThat is it. No .csproj. No solution. No Program.cs inside a folder. Just one file and one command.
This works because of top-level statements, a C# feature that lets you skip the class Program and static void Main wrapper. The code you see above is the whole program. The .NET SDK reads your file, quietly builds a hidden project around it, and runs the result.
Why this is a big deal
Before .NET 10, the smallest "real" C# program still needed a project. People who came from Python or JavaScript found this strange. In those languages you can save one file and run it. Now C# can do the same thing. This makes C# friendlier for:
- Learning — a student can try ideas without setup.
- Prototyping — you can test a small idea in seconds.
- Automation scripts — small helper tools that do one job.
How it runs behind the scenes
When you type dotnet run hello.cs, a few things happen that you never see. Understanding them helps you trust the tool.
First, the SDK reads your file and any special lines at the top (we will meet those soon). It then builds a virtual project in a temporary folder. It compiles your code into a real program and runs it. The build output is stored in a cache folder under your system's temp directory.
The clever part is the cache. The next time you run the same file, if nothing changed, the SDK skips the build and runs the saved program straight away. This makes the second run much faster.
What happens on dotnet run
Steps
Read file
Scan code and #: directives
Build virtual project
SDK makes a hidden project
Compile
Turn code into a program
Cache
Save output in temp folder
Run
Start your app
The cache decides whether to rebuild based on a few things: your source file content, the directives you used, the SDK version, and any shared build files nearby. If all of those are the same as last time, you get the fast path.
| Run number | What the SDK does | Speed |
|---|---|---|
| First run | Builds, caches, then runs | Slower |
| Next run (no changes) | Uses cached program | Fast |
| After you edit the file | Rebuilds, updates cache | Slower again |
Adding power with directives
A plain file is nice, but real programs often need extra things: a NuGet package, a different SDK, or a build setting. In a normal project these live in the .csproj. In a file-based app, they live as special lines at the top of your file. These lines start with #: and are called directives.
There are five of them. Let me show each one simply.
#:package — add a NuGet package
This pulls in a library from NuGet. For example, Spectre.Console makes colourful console output.
#:package Spectre.Console@*
using Spectre.Console;
AnsiConsole.MarkupLine("[green]Hello[/], [yellow]World[/]!");The @* means "use the latest version". You can also pin a version, like #:package Serilog@3.1.1, which is safer for real tools because the version will not change under you.
#:sdk — choose a different SDK
By default a file-based app uses Microsoft.NET.Sdk, which is for console programs. If you want a tiny web API, switch the SDK:
#:sdk Microsoft.NET.Sdk.Web
var builder = WebApplication.CreateBuilder();
var app = builder.Build();
app.MapGet("/", () => "Hello from a single-file web API!");
app.Run();Run it with dotnet run api.cs and you have a working web server from one file. This is the same minimal API style you would use in a full ASP.NET Core project.
#:property — set a build setting
This sets an MSBuild property, which is just a build option. For example, you can turn off Native AOT publishing:
#:property PublishAot=false#:project — reference another project
If your script needs code from a real project nearby, point to it:
#:project ../SharedLibrary/SharedLibrary.csproj#:include — bring in another file
Newer SDK versions let you split helper code into another file and include it:
#:include helpers.csHere is a quick table you can keep as a cheat sheet.
| Directive | What it does | Example |
|---|---|---|
#:package | Add a NuGet package | #:package Newtonsoft.Json@13.0.3 |
#:sdk | Pick a different SDK | #:sdk Microsoft.NET.Sdk.Web |
#:property | Set a build option | #:property PublishAot=false |
#:project | Reference a project | #:project ../Lib/Lib.csproj |
#:include | Add another file | #:include models.cs |
Useful CLI commands
The same dotnet commands you already know work with a single file. Here are the handy ones.
dotnet run app.cs # build and run
dotnet build app.cs # only build
dotnet clean app.cs # remove build output
dotnet publish app.cs # make a standalone program
dotnet pack app.cs # package as a .NET toolYou can pass arguments to your program after a -- separator:
dotnet run app.cs -- hello worldEverything after -- goes to your app, not to dotnet. So inside your code, args[0] would be hello and args[1] would be world.
You can even pipe code straight from your terminal without saving a file at all:
echo 'Console.WriteLine("hi from stdin");' | dotnet run -The lone - tells dotnet to read the code from standard input. This is great for very quick one-off tests.
Running like a real script (shebang)
On Linux and macOS you can make your file run like any other script. You add a shebang line at the very top, then mark the file as executable.
#!/usr/bin/env -S dotnet --
#:package Spectre.Console@*
using Spectre.Console;
AnsiConsole.MarkupLine("[green]Hello, World![/]");Then in your terminal:
chmod +x app.cs
./app.csNow your C# file behaves like a shell or Python script. A small note: use LF line endings (not Windows CRLF) and do not add a BOM, or the shebang may not work. The -S flag and the -- separator make sure your own arguments reach your app and are not swallowed by dotnet.
Running a C# file as a shell script
Steps
Add shebang
#!/usr/bin/env -S dotnet --
chmod +x
Mark file executable
./app.cs
Run it directly
When the file grows: convert to a project
A single file is perfect for small things. But programs have a way of growing. One day your app.cs is 30 lines; a month later it is 800 lines with many features. At that point a real project is easier to manage.
The good news: you do not start over. One command turns your file into a normal project.
dotnet project convert app.csThis reads your #: directives and creates a proper .csproj with matching settings, packages, and properties. Your original file is left untouched, and a new project folder appears next to it. After that, you work exactly like any normal .NET project. The language and tools stay the same the whole way — only the wrapping changes.
This smooth path is one of the best parts of the design. You are never stuck. You start small, and when you outgrow one file, you take one step up. There is no rewrite and no new language to learn.
A complete example
Let me tie it together with one small but real program. This script asks NuGet's Humanizer library to turn a number of seconds into friendly text.
#:package Humanizer@2.14.1
using Humanizer;
var seconds = args.Length > 0 ? int.Parse(args[0]) : 4000;
var friendly = TimeSpan.FromSeconds(seconds).Humanize();
Console.WriteLine($"{seconds} seconds is about {friendly}.");Run it like this:
dotnet run friendly.cs -- 9000You will see something like 9000 seconds is about 2 hours. In just a few lines, with one package and no project file, you built a useful little tool.
Good habits and a few limits
File-based apps are powerful, but a few simple habits keep you out of trouble.
- Do not put a script inside a project folder. A
.csprojnearby can change how your script builds. Keep scripts in their own folder. - Pin package versions for tools you depend on.
@*is handy for quick tests, but a fixed version like@13.0.3keeps a tool stable over time. - Watch shared build files. Files like
Directory.Build.propsin a parent folder affect your script too. This is useful, but it can surprise you. - Mind the cache with parallel runs. Running many copies of the same file at once can clash over build files. If you need that, build once with
dotnet build app.cs, then run with--no-build.
| Best for | Not the best fit |
|---|---|
| Small scripts and tools | Large multi-project solutions |
| Learning and demos | Big team codebases with many files |
| Quick automation tasks | Apps needing complex build pipelines |
These are not hard rules, just gentle guidance. When a script stops feeling small, that is your sign to run dotnet project convert and move on to a full project.
Quick recap
- File-based apps let you run a single
.csfile in .NET 10 withdotnet run app.cs— no project file needed. - The SDK builds a hidden virtual project, caches it, and reuses the cache when nothing changes.
- Directives at the top add power:
#:package,#:sdk,#:property,#:project, and#:include. - You can pass arguments after
--, pipe code from stdin with-, and on Unix run the file like a script using a shebang. - When your file grows,
dotnet project convert app.csturns it into a normal project with no rewrite. - Keep scripts in their own folder, pin versions for stable tools, and watch shared build files.
File-based apps make C# feel light and friendly again. You can start with one piece of paper, write your idea, and only bind the book when the story gets long. That is a very kind way to learn and build.
References and further reading
- File-based apps - Microsoft Learn — the official reference for all directives and commands.
- Announcing dotnet run app.cs - .NET Blog — the original announcement with the design story.
- Build file-based apps - C# tutorial - Microsoft Learn — a hands-on walkthrough.
- Exploring C# File-based Apps in .NET 10 - Milan Jovanović — a clear community deep dive.
- dotnet run app.cs - Andrew Lock — a careful look at how the feature works under the hood.
Related Posts
Run C# Scripts With dotnet run app.cs: No Project Files Needed
Learn to run C# scripts with dotnet run app.cs in .NET 10 — no project files, no setup. Add NuGet packages, pass arguments, and automate tasks fast.
How to Create Command-Line Console Applications in .NET
A beginner-friendly guide to building command-line console apps in .NET 10 with the dotnet CLI, arguments, options, and System.CommandLine.
Building File-Based Apps in .NET With Multi-File Support
Learn how to run C# without a project file using dotnet run app.cs in .NET 10, split code across files with #:include, and add packages with directives.
The New .slnx Solution Format: A Simple Migration Guide for .NET 10
Learn the new XML-based .slnx solution format in .NET 10, why it beats the old .sln file, and how to migrate safely with one CLI command.
How to Start a New .NET Project in 2026: A Beginner's Friendly Guide
A warm, step-by-step guide to starting a new .NET 10 project in 2026 with the dotnet CLI, the right template, and good folder habits from day one.
How to Write Elegant Code with C# Pattern Matching
Learn C# pattern matching the easy way. Use is, switch expressions, property and list patterns to write clean, readable, and elegant .NET code.