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, or the newer.slnxformat) - 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:
dotnethello.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.
New to Minimal APIs?
File-based apps pair perfectly with Minimal APIs. Learn how to build lightweight HTTP endpoints without controllers.
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.”
Dependency Injection Explained
Once you're ready to move beyond scripts, master DI - the foundation of modern .NET applications.
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:
dotnethello.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:
#:packageNewtonsoft.Json@13.0.3#:packageSerilog@4.0.0#:packageSpectre.Console@*Version syntax options:
- Exact version:
#:package Serilog@4.0.0 - 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:
#:sdkMicrosoft.NET.Sdk.WebWith the Web SDK you can spin up a full HTTP API in a single file, complete with native OpenAPI and Scalar docs that .NET 10 ships out of the box.
For .NET Aspire:
#:sdkAspire.AppHost.Sdk@9.0.0.NET Aspire Deep Dive
Want to orchestrate distributed applications? Learn how Aspire simplifies cloud-native development.
#:property - Set MSBuild Properties
Configure any MSBuild property you’d normally put in a .csproj:
#:propertyLangVersion=preview#:propertyNullable=disable#:propertyPublishAot=false#:propertyTargetFramework=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+xscript.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 (vararginargs){Console.WriteLine($"Argument: {arg}");}dotnetargs.cs--helloworld--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:
dotnetsysinfo.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:
usingSystem.Security.Cryptography;conststringlowercase="abcdefghijklmnopqrstuvwxyz";conststringuppercase="ABCDEFGHIJKLMNOPQRSTUVWXYZ";conststringdigits="0123456789";conststringspecial="!@#$%^&*()_+-=[]{}|;:,.<>?";varallChars=lowercase+uppercase+digits+special;varlength=args.Length>0&&int.TryParse(args[0], outvarlen) ?len:16;if (length<8||length>128){Console.WriteLine("Password length must be between 8 and 128 characters.");return;}varpassword=newchar[length];for (inti=0; i<length; i++){password[i] =allChars[RandomNumberGenerator.GetInt32(allChars.Length)];}Console.WriteLine($"Generated password ({length} chars):");Console.WriteLine(newstring(password));Run it:
dotnetpassgen.csdotnetpassgen.cs--24dotnetpassgen.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:
#:packageCsvHelper@33.0.0usingSystem.Text.Json;usingSystem.Text.Json.Serialization;usingCsvHelper;usingSystem.Globalization;// Read JSON using source-generated serializer (AOT-compatible)varjson=awaitFile.ReadAllTextAsync("sales.json");varsales=JsonSerializer.Deserialize(json, SaleJsonContext.Default.ListSale)!;// Process and write CSVvarsummary=sales.GroupBy(s=>s.Product).Select(g=>newSaleSummary(g.Key, g.Sum(s=>s.Amount), g.Count())).OrderByDescending(x=>x.TotalRevenue);usingvarwriter=newStreamWriter("summary.csv");usingvarcsv=newCsvWriter(writer, CultureInfo.InvariantCulture);csv.WriteRecords(summary);Console.WriteLine("Done! Check summary.csv");recordSale(stringProduct, decimalAmount, DateTimeDate);recordSaleSummary(stringProduct, decimalTotalRevenue, intCount);[JsonSerializable(typeof(List<Sale>))]partialclassSaleJsonContext : 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:
dotnetdata-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:
#:packageSystem.CommandLine@2.0.0usingSystem.CommandLine;varfileOption=newOption<FileInfo?>("--file") { Description="The file to process" };varverboseOption=newOption<bool>("--verbose") { Description="Show detailed output" };varrootCommand=newRootCommand("File statistics utility - counts lines, words, and characters"){fileOption,verboseOption};rootCommand.SetAction(async (parseResult, cancellationToken) =>{varfile=parseResult.GetValue(fileOption);varverbose=parseResult.GetValue(verboseOption);if (fileisnull){Console.WriteLine("No file specified. Use --file <path>");return1;}if (!file.Exists){Console.WriteLine($"File not found: {file.FullName}");return1;}if (verbose)Console.WriteLine($"Processing: {file.FullName}");varlines=awaitFile.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)}");return0;});returnawaitrootCommand.Parse(args).InvokeAsync();Run it:
dotnetfile-stats.cs----filemyfile.txt--verboseYou can also get auto-generated help:
dotnetfile-stats.cs----helpOutput:
Description:File statistics utility - counts lines, words, and charactersUsage: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.
Interview Questions PDF
100 real interview questions across 9 categories, junior to senior
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. And if you want to ship it in a container, you can containerize a .NET app without a Dockerfile using the SDK’s built-in publish support.
Docker for .NET Developers
Once you've published your file-based app, containerize it with Docker for easy distribution.
If you need to disable Native AOT (for example, if you’re using reflection-heavy libraries):
#:propertyPublishAot=false// Your code hereNative AOT Deployment in .NET
Learn more about Native AOT compilation, its benefits, and limitations.
Configuration and Secrets
Using appsettings.json
With the Web SDK, configuration files are automatically included:
Environment-Based Configuration
Master configuration in ASP.NET Core - appsettings, environment variables, and the Options pattern.
#:sdkMicrosoft.NET.Sdk.Webvarbuilder=WebApplication.CreateBuilder(args);// appsettings.json is automatically loadedvarconnectionString=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!
dotnetuser-secretsset"ApiKey""my-secret-key"--fileapi.csdotnetuser-secretslist--fileapi.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:
dotnetrunapi.cs--launch-profileproductionConverting to a Full Project
When your file-based app grows too complex, convert it to a standard project:
dotnetprojectconvertapi.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):
#:sdkMicrosoft.NET.Sdk.Web#:packageMicrosoft.AspNetCore.OpenApi@10.0.0varbuilder=WebApplication.CreateBuilder(args);// ...After (api/api.csproj):
<ProjectSdk="Microsoft.NET.Sdk.Web"><PropertyGroup><TargetFramework>net10.0</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings><PublishAot>true</PublishAot></PropertyGroup><ItemGroup><PackageReferenceInclude="Microsoft.AspNetCore.OpenApi"Version="10.0.0" /></ItemGroup></Project>And api/api.cs (directives removed):
varbuilder=WebApplication.CreateBuilder(args);// ...You can also specify a custom output directory:
dotnetprojectconvertapi.cs--outputMyApiProjectHow 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:
dotnetcleanhello.csdotnetcleanfile-based-apps# Clean all cached file-based appsdotnetcleanfile-based-apps--days7# 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.csscripts/├── 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
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.
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 :)
