What if I told you that you can now write C# like Python? Just create a .cs file, write your code, and run it directly. No .csproj file. No solution. No ceremony.
That’s exactly what .NET 10 brings with file-based apps — a game-changer for scripting, prototyping, and learning C#.
In this article, we’ll explore how file-based apps work, all the directives you can use, practical examples for scripting and data processing, and when you should (or shouldn’t) use this feature.
The sample code for this article is available in the .NET Web API Zero to Hero course repository under modules/01-getting-started/file-based-apps/.
Let’s get into it.
What Are File-Based Apps?
File-based apps are a new feature in .NET 10 that lets you write and run C# programs from a single .cs file without creating a project file. Think of it like running a Python script — just write code and execute it.
Before .NET 10, even the simplest C# program required:
- A
.csprojproject file - Often a solution file (
.sln) - The
dotnet newcommand to scaffold everything
Now? You just need this:
Console.WriteLine("Hello from file-based apps!");Save it as hello.cs and run:
dotnet hello.csThat’s it. No boilerplate. No ceremony. Just code.
This is built on top of top-level statements (introduced in C# 9) but takes it further by eliminating the need for project infrastructure entirely.
Why Does This Matter?
File-based apps solve real problems that .NET developers have faced for years:
- Learning C# is now easier — Beginners don’t need to understand MSBuild, project files, or solution structures just to print “Hello World”
- Quick prototyping — Test an idea in seconds without creating a full project
- Scripting in C# — Replace Bash or PowerShell scripts with type-safe C# code
- One-off utilities — Write a quick data converter or file processor without project overhead
- Better samples — Library authors can include runnable examples without cluttering repos with project files
The barrier to entry just dropped from “learn project files and MSBuild” to “write C# and run it.”
Getting Started
Prerequisites
You need the .NET 10 SDK installed. Download it from dotnet.microsoft.com/download/dotnet/10.0.
Verify your installation:
dotnet --version# Should show 10.0.xYour First File-Based App
Create a file named hello.cs:
Console.WriteLine("Hello from .NET 10!");Console.WriteLine($"Running on {Environment.OSVersion}");Console.WriteLine($"Current time: {DateTime.Now:F}");Run it:
dotnet hello.csOutput:
Hello from .NET 10!Running on Microsoft Windows NT 10.0.22631.0Current time: Sunday, January 26, 2026 10:30:00 AMThe first run takes a moment because the SDK compiles the code. Subsequent runs are faster thanks to caching.
All Available Directives
File-based apps use special directives (prefixed with #:) to configure packages, SDKs, and build properties. These must be placed at the top of your file.
#:package — Add NuGet Packages
Add NuGet package references directly in your code:
#:package Newtonsoft.Json@13.0.3#:package Serilog@4.0.0#:package Spectre.Console@*Version syntax options:
- Exact version:
#:package [email protected] - Wildcard (latest):
#:package Spectre.Console@* - Partial wildcard:
#:package CsvHelper@33.*
#:sdk — Specify the SDK
By default, file-based apps use Microsoft.NET.Sdk. For web applications, you need the Web SDK:
#:sdk Microsoft.NET.Sdk.WebFor .NET Aspire:
#:sdk Aspire.AppHost.Sdk@9.0.0#:property — Set MSBuild Properties
Configure any MSBuild property you’d normally put in a .csproj:
#:property LangVersion=preview#:property Nullable=disable#:property PublishAot=false#:property TargetFramework=net10.0#:project — Reference Other Projects
Reference existing project files:
#:project ../SharedLibrary/SharedLibrary.csprojShebang Support (Unix/Linux/macOS)
Make your file directly executable on Unix-like systems:
#!/usr/bin/env dotnetConsole.WriteLine("I'm a script!");Then:
chmod +x script.cs./script.csNote: Shebang requires LF line endings (not CRLF) and no BOM. This doesn’t work on Windows.
CLI Commands Reference
Here’s everything you can do with file-based apps:
| Command | Description |
|---|---|
dotnet run hello.cs | Build and run |
dotnet hello.cs | Shorthand for run |
dotnet build hello.cs | Build only (output in temp folder) |
dotnet publish hello.cs | Publish (Native AOT by default!) |
dotnet pack hello.cs | Package as .NET tool |
dotnet project convert hello.cs | Convert to full project |
dotnet clean hello.cs | Clean build artifacts |
dotnet restore hello.cs | Restore packages |
dotnet run hello.cs -- arg1 arg2 | Pass arguments to your program |
Passing Arguments
Use -- to separate CLI arguments from your program’s arguments:
foreach (var arg in args){ Console.WriteLine($"Argument: {arg}");}dotnet args.cs -- hello world --verboseOutput:
Argument: helloArgument: worldArgument: --verbosePiping from Stdin
You can even pipe code directly:
'Console.WriteLine("Hello from stdin!");' | dotnet run -Practical Examples
Let’s look at real-world scenarios where file-based apps shine.
Example 1: System Info Script
Let’s start with something simple — a script that displays information about your system. This is useful for debugging environment issues or just quickly checking your setup. No external packages needed.
Create sysinfo.cs:
Console.WriteLine("=== System Information ===");Console.WriteLine();Console.WriteLine($"Machine Name: {Environment.MachineName}");Console.WriteLine($"User Name: {Environment.UserName}");Console.WriteLine($"OS: {Environment.OSVersion}");Console.WriteLine($".NET Version: {Environment.Version}");Console.WriteLine($"64-bit OS: {Environment.Is64BitOperatingSystem}");Console.WriteLine($"64-bit Process: {Environment.Is64BitProcess}");Console.WriteLine($"Processor Count: {Environment.ProcessorCount}");Console.WriteLine($"Current Dir: {Environment.CurrentDirectory}");Console.WriteLine();Console.WriteLine("=== Environment Variables ===");Console.WriteLine();Console.WriteLine($"PATH entries: {Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator).Length ?? 0}");Console.WriteLine($"TEMP: {Path.GetTempPath()}");Run it:
dotnet sysinfo.csOutput:
=== System Information ===
Machine Name: DEV-WORKSTATIONUser Name: mukeshOS: Microsoft Windows NT 10.0.22631.0.NET Version: 10.0.064-bit OS: True64-bit Process: TrueProcessor Count: 16Current Dir: C:\scripts
=== Environment Variables ===
PATH entries: 42TEMP: C:\Users\mukesh\AppData\Local\Temp\This is about as simple as it gets — pure C# with no packages, no directives, just code. The Environment class gives you access to all sorts of system information. You could extend this to check for specific tools, SDK versions, or environment variables your project needs.
Example 2: Password Generator
Here’s a practical utility you’ll actually use — a random password generator. It creates secure passwords with a mix of characters, and you can specify the length via command-line arguments.
Create passgen.cs:
using System.Security.Cryptography;
const string lowercase = "abcdefghijklmnopqrstuvwxyz";const string uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";const string digits = "0123456789";const string special = "!@#$%^&*()_+-=[]{}|;:,.<>?";
var allChars = lowercase + uppercase + digits + special;var length = args.Length > 0 && int.TryParse(args[0], out var len) ? len : 16;
if (length < 8 || length > 128){ Console.WriteLine("Password length must be between 8 and 128 characters."); return;}
var password = new char[length];for (int i = 0; i < length; i++){ password[i] = allChars[RandomNumberGenerator.GetInt32(allChars.Length)];}
Console.WriteLine($"Generated password ({length} chars):");Console.WriteLine(new string(password));Run it:
dotnet passgen.csdotnet passgen.cs -- 24dotnet passgen.cs -- 32Output:
Generated password (16 chars):kP9#mX2$vL5@nQ8!
Generated password (24 chars):Hj3$kL9@mN5#pQ2!xR7&vB4^
Generated password (32 chars):aK8#mP3$xL5@nQ9!hR2&vB7^jT4%wC6*Code Walkthrough
Character sets — We define four character categories: lowercase, uppercase, digits, and special characters. Combining them gives us a pool of 85 characters.
Argument parsing — The script checks args[0] for a custom length. If not provided or invalid, it defaults to 16 characters. We also validate the length is between 8 and 128.
Secure randomness — Instead of Random, we use RandomNumberGenerator.GetInt32() from System.Security.Cryptography. This provides cryptographically secure random numbers, which is important for password generation.
Building the password — We create a character array and fill each position with a random character from our pool, then convert it to a string for output.
This is the kind of utility that’s perfect for file-based apps — something you need occasionally, want to run quickly, and don’t want to maintain as a full project.
Example 3: Data Processing
One of the most common scripting tasks is transforming data from one format to another. Let’s say you have a sales.json file with transaction records, and you need to generate a CSV summary grouped by product. Normally, you’d create a console project, add NuGet packages, write the code, and then run it. With file-based apps, you can do this in a single file.
Here’s what we’re building: a script that reads sales data from JSON, groups it by product, calculates totals, and outputs a CSV file.
First, create a sample sales.json file to work with:
[ { "Product": "Laptop", "Amount": 999.99, "Date": "2026-01-15" }, { "Product": "Mouse", "Amount": 29.99, "Date": "2026-01-15" }, { "Product": "Laptop", "Amount": 999.99, "Date": "2026-01-16" }, { "Product": "Keyboard", "Amount": 79.99, "Date": "2026-01-16" }, { "Product": "Mouse", "Amount": 29.99, "Date": "2026-01-17" }]Now create data-processor.cs:
#:package CsvHelper@33.0.0
using System.Text.Json;using System.Text.Json.Serialization;using CsvHelper;using System.Globalization;
// Read JSON using source-generated serializer (AOT-compatible)var json = await File.ReadAllTextAsync("sales.json");var sales = JsonSerializer.Deserialize(json, SaleJsonContext.Default.ListSale)!;
// Process and write CSVvar summary = sales .GroupBy(s => s.Product) .Select(g => new SaleSummary(g.Key, g.Sum(s => s.Amount), g.Count())) .OrderByDescending(x => x.TotalRevenue);
using var writer = new StreamWriter("summary.csv");using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);csv.WriteRecords(summary);
Console.WriteLine("Done! Check summary.csv");
record Sale(string Product, decimal Amount, DateTime Date);record SaleSummary(string Product, decimal TotalRevenue, int Count);
[JsonSerializable(typeof(List<Sale>))]partial class SaleJsonContext : JsonSerializerContext { }Note: Since file-based apps use Native AOT by default, we use source-generated JSON serialization instead of reflection-based. The
[JsonSerializable]attribute generates the serialization code at compile time, making it AOT-compatible.
Run it:
dotnet data-processor.csCode Walkthrough
Package directive — We only need CsvHelper as an external package. System.Text.Json is already included in the .NET runtime.
Source-generated JSON — Since file-based apps use Native AOT by default, reflection-based serialization is disabled. The SaleJsonContext class with the [JsonSerializable] attribute tells the compiler to generate serialization code at compile time. This is the AOT-compatible way to use System.Text.Json.
Reading and deserializing — We read the JSON file and deserialize using SaleJsonContext.Default.ListSale instead of the generic Deserialize<T>() method. This uses the source-generated serializer.
LINQ aggregation — The code groups sales by product, then projects each group into a SaleSummary record with the product name, total revenue, and count. We use a concrete record type instead of an anonymous type for better AOT compatibility.
Writing CSV — CsvHelper handles the CSV formatting. We create a StreamWriter for the output file and pass it to CsvWriter, which writes the records with proper headers.
The output summary.csv will look like:
Product,TotalRevenue,CountLaptop,1999.98,2Keyboard,79.99,1Mouse,59.98,2This entire workflow — reading JSON, transforming data, writing CSV — happens in one file with zero project setup. Quite handy for ad-hoc data tasks, yeah?
Example 4: CLI Tool with Argument Parsing
Sometimes you need a reusable command-line utility with proper argument handling. Instead of manually parsing args[], you can use Microsoft’s System.CommandLine library to get features like help text, validation, and tab completion — all in a single file.
Here’s what we’re building: a file statistics tool that counts lines, words, and characters in any text file. It supports a --file argument to specify the input and a --verbose flag for detailed output.
Create file-stats.cs:
#:package System.CommandLine@2.0.0
using System.CommandLine;
var fileOption = new Option<FileInfo?>("--file") { Description = "The file to process" };var verboseOption = new Option<bool>("--verbose") { Description = "Show detailed output" };
var rootCommand = new RootCommand("File statistics utility - counts lines, words, and characters"){ fileOption, verboseOption};
rootCommand.SetAction(async (parseResult, cancellationToken) =>{ var file = parseResult.GetValue(fileOption); var verbose = parseResult.GetValue(verboseOption);
if (file is null) { Console.WriteLine("No file specified. Use --file <path>"); return 1; }
if (!file.Exists) { Console.WriteLine($"File not found: {file.FullName}"); return 1; }
if (verbose) Console.WriteLine($"Processing: {file.FullName}");
var lines = await File.ReadAllLinesAsync(file.FullName, cancellationToken); Console.WriteLine($"Lines: {lines.Length}"); Console.WriteLine($"Words: {lines.Sum(l => l.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length)}"); Console.WriteLine($"Characters: {lines.Sum(l => l.Length)}");
return 0;});
return await rootCommand.Parse(args).InvokeAsync();Run it:
dotnet file-stats.cs -- --file myfile.txt --verboseYou can also get auto-generated help:
dotnet file-stats.cs -- --helpOutput:
Description: File statistics utility - counts lines, words, and characters
Usage: file-stats [options]
Options: --file <file> The file to process --verbose Show detailed output --version Show version information -?, -h, --help Show help and usage informationCode Walkthrough
Defining options — We create two Option<T> objects: one for the file path (which System.CommandLine automatically converts to a FileInfo) and one for the verbose flag. Each option has a name and description.
Building the command — The RootCommand is the entry point for our CLI. We add both options to it using collection initializer syntax.
Setting the action — In System.CommandLine 2.0, we use SetAction with an async delegate. The delegate receives a ParseResult and CancellationToken as parameters.
Getting parsed values — Inside the action, we use parseResult.GetValue() to retrieve the parsed option values. This is type-safe and returns the correct type for each option.
Validation — We check if a file was provided and if it exists. Returning 1 indicates an error, while 0 means success.
Processing — We read all lines asynchronously and use LINQ to calculate statistics. The StringSplitOptions.RemoveEmptyEntries ensures we don’t count empty strings as words.
Invoking the command — We call rootCommand.Parse(args).InvokeAsync() to parse the arguments and execute the action. The exit code is returned to the shell.
The -- separator in the run command tells the .NET CLI that everything after it should be passed to your program, not interpreted as dotnet options.
This is how file-based apps let you build proper CLI tools without any project ceremony. You get all the benefits of System.CommandLine — help text, validation, error handling — in a single script file.
Native AOT — Enabled by Default!
Here’s something important: file-based apps publish with Native AOT enabled by default.
When you run dotnet publish hello.cs, you get:
- A self-contained executable
- No .NET runtime dependency
- Faster startup time
- Smaller memory footprint
This is perfect for CLI tools and utilities.
If you need to disable Native AOT (for example, if you’re using reflection-heavy libraries):
#:property PublishAot=false
// Your code hereConfiguration and Secrets
Using appsettings.json
With the Web SDK, configuration files are automatically included:
#:sdk Microsoft.NET.Sdk.Web
var builder = WebApplication.CreateBuilder(args);
// appsettings.json is automatically loadedvar connectionString = builder.Configuration.GetConnectionString("Default");Console.WriteLine($"Connection: {connectionString}");Create appsettings.json in the same directory:
{ "ConnectionStrings": { "Default": "Server=localhost;Database=MyDb" }}User Secrets
Yes, user secrets work with file-based apps!
dotnet user-secrets set "ApiKey" "my-secret-key" --file api.csdotnet user-secrets list --file api.csThe secrets ID is generated based on your file path hash.
Launch Profiles
Create a [filename].run.json file alongside your .cs file for launch configuration:
{ "profiles": { "development": { "commandName": "Project", "launchBrowser": true, "applicationUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "production": { "commandName": "Project", "applicationUrl": "http://localhost:5001", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Production" } } }}Run with a specific profile:
dotnet run api.cs --launch-profile productionConverting to a Full Project
When your file-based app grows too complex, convert it to a standard project:
dotnet project convert api.csThis command:
- Creates a new directory named after your file
- Generates a proper
.csprojfile - Moves your code (removing
#:directives) - Translates directives into MSBuild properties and package references
Before (api.cs):
#:sdk Microsoft.NET.Sdk.Web#:package Microsoft.AspNetCore.OpenApi@10.0.0
var builder = WebApplication.CreateBuilder(args);// ...After (api/api.csproj):
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <PublishAot>true</PublishAot> </PropertyGroup>
<ItemGroup> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" /> </ItemGroup></Project>And api/api.cs (directives removed):
var builder = WebApplication.CreateBuilder(args);// ...You can also specify a custom output directory:
dotnet project convert api.cs --output MyApiProjectHow It Works Under the Hood
When you run dotnet hello.cs, here’s what happens:
- Validation — The SDK confirms the file ends with
.csor starts with#! - Directive Parsing — Roslyn extracts all
#:directives from the file - Virtual Project Generation — An in-memory
.csprojis created with your directives translated to MSBuild elements - Build Execution — MSBuild runs
RestoreandBuildtargets - Caching — Artifacts are stored in a temp folder for faster subsequent runs
- Execution — The compiled app runs
Cache location: <temp>/dotnet/runfile/<appname>-<filehash>/
To clean the cache:
dotnet clean hello.csdotnet clean file-based-apps # Clean all cached file-based appsdotnet clean file-based-apps --days 7 # Remove unused for 7+ daysFolder Layout Best Practices
Here’s an important gotcha that can cause unexpected behavior: don’t nest file-based apps inside project directories.
The Anti-Pattern
❌ BAD - Don't do this:
MyProject/├── MyProject.csproj├── Program.cs└── scripts/ └── utility.cs ← This will cause problems!When you run dotnet utility.cs from inside a project folder, the SDK gets confused. It may try to build the parent project instead, or you’ll get unexpected build conflicts.
The Correct Approach
✅ GOOD - Keep file-based apps separate:
MyProject/├── MyProject.csproj└── Program.cs
scripts/├── utility.cs├── data-processor.cs└── sysinfo.csBy keeping your file-based apps in a separate directory (sibling to your projects, not nested inside them), you avoid build conflicts and make it clear which files are standalone scripts vs. project source files.
Tip: Create a
scripts/folder at the root of your repository for all your file-based utilities. This keeps them organized and prevents accidental inclusion in project builds.
Implicit Build Files
File-based apps automatically respect configuration files from the same or parent directories:
global.json— SDK version pinningNuGet.config— Package sourcesDirectory.Build.props/Directory.Build.targets— Shared MSBuild propertiesDirectory.Packages.props— Central package management
This means you can create a Directory.Build.props to share settings across multiple file-based apps:
<!-- Directory.Build.props --><Project> <PropertyGroup> <LangVersion>preview</LangVersion> <Nullable>enable</Nullable> </PropertyGroup></Project>What’s NOT Supported
Before diving into limitations, let’s be explicit about what’s not coming to file-based apps:
- Visual Basic .NET — No plans to support VB.NET file-based apps
- F# — No plans for F# support (though the community has requested it)
- Visual Studio support — Currently works with VS Code and CLI only; full VS support is not planned for the initial release
- Multi-file apps — Planned for .NET 11, not .NET 10
The .NET team has been clear that file-based apps are designed for C# scripting scenarios, not as a replacement for traditional project-based development.
Current Limitations
Before you go all-in on file-based apps, be aware of these limitations:
| Limitation | Details |
|---|---|
| Single file only | Multi-file support is planned for .NET 11 |
| No Visual Studio support | Works with VS Code and CLI only |
| No VB.NET or F# support | C# only for now |
| No direct assembly references | Use Directory.Build.targets as a workaround |
| IntelliSense has issues | Known issue in VS Code (GitHub #49293) |
| Not for production | Best for scripting, prototyping, and learning |
When to Use File-Based Apps
Perfect for:
- Learning C# and .NET
- Quick prototyping and experiments
- One-off scripts and utilities
- CI/CD automation scripts
- Sample code in library repos
- Data processing and transformations
Not recommended for:
- Production applications
- Large or complex projects
- Team projects requiring full IDE support
- Applications needing multiple source files
File-Based Apps vs. CSX Scripts
If you’ve used .csx scripts before, here’s how file-based apps compare:
| Feature | File-based Apps (.cs) | CSX Scripts (.csx) |
|---|---|---|
| Built into SDK | Yes | No (third-party) |
| Standard C# syntax | Yes | Different dialect |
| Convert to project | Yes | No |
| REPL support | No | Yes |
| Active development | Yes | Limited |
| NuGet packages | #:package | #r "nuget:" |
File-based apps use standard C# syntax, making it easy to copy code to/from regular projects.
Frequently Asked Questions
Can I use file-based apps in production?
File-based apps are not recommended for production applications. They're designed for scripting, prototyping, learning, and one-off utilities. For production workloads, convert to a full project using 'dotnet project convert' to get proper build pipelines, testing support, and IDE integration.
What's the difference between file-based apps and CSX scripts?
File-based apps use standard C# syntax and are built into the .NET 10 SDK — no third-party tools needed. CSX scripts use a different dialect and require external tools like dotnet-script. File-based apps can be converted to full projects, while CSX cannot. Both support NuGet packages, but with different directive syntax.
How do I add NuGet packages to a file-based app?
Use the #:package directive at the top of your file. For example: '#:package [email protected]' for a specific version, or '#:package Spectre.Console@*' for the latest version. The SDK handles package restoration automatically on first run.
Does Visual Studio support file-based apps?
Not yet. File-based apps currently work with Visual Studio Code and the command line only. Full Visual Studio support is not planned for the initial .NET 10 release. Use VS Code with the C# Dev Kit extension for the best experience.
What happens when I run 'dotnet app.cs'?
The SDK validates your file, parses all #: directives, generates a virtual .csproj in a temp folder, restores NuGet packages, compiles your code, and runs the executable. The first run is slower due to compilation, but subsequent runs use cached artifacts and launch almost instantly.
Can I use multiple .cs files in a file-based app?
Not in .NET 10 — file-based apps are strictly single-file. Multi-file support is planned for .NET 11. For now, if you need multiple files, use 'dotnet project convert' to create a full project structure.
Do file-based apps support debugging?
Yes, you can debug file-based apps in VS Code. The SDK generates a temporary project that debuggers can attach to. Set breakpoints in your .cs file and use the standard debugging workflow. However, the experience is smoother with full projects.
Summary
File-based apps in .NET 10 are a significant quality-of-life improvement for .NET developers. They lower the barrier to entry for newcomers, speed up prototyping for experienced developers, and make C# a viable scripting language.
Key takeaways:
- Run C# with
dotnet hello.cs— no project file needed - Use
#:package,#:sdk,#:property, and#:projectdirectives - Native AOT is enabled by default for publishing
- Convert to a full project when complexity grows with
dotnet project convert - Not for production — best for learning, scripting, and prototyping
This is just the beginning. Multi-file support is coming in .NET 11, and IDE support will continue to improve.
This article is part of our FREE .NET Web API Zero to Hero Series → Start the course here
If you found this helpful, share it with your colleagues — and if there’s a topic you’d like to see covered next, drop a comment and let me know.
Happy Coding :)


