
The Problem with "Projects Everywhere"
Every .NET developer knows the ritual: create a directory, run dotnet new console, watch a .csproj file materialize along with obj/ and bin/ folders, then finally write the ten lines of code you actually needed.
For production applications, this structure is essential. For everything else? It's friction.
Consider these common scenarios where traditional project scaffolding gets in the way:
-
Experiments and prototypes: You want to test an API, validate a regex, or explore a library. Creating a full project feels like building a house to try a new hammer.
-
One-off scripts: Converting file formats, processing data, automating a task. You need something that runs once and lives in a
scripts/folder without contaminating your.gitignore. -
CI/CD utilities: Quick validation scripts, deployment helpers, data migrations. These don't deserve their own solution.
-
Internal tools: Command-line utilities for your team that shouldn't require consumers to understand MSBuild.
-
Learning and teaching: Showing someone C# shouldn't start with explaining what a
.csprojfile is.
Historically, this was simply how .NET worked. The compilation model assumed projects. The tooling assumed projects. Even the simplest "Hello World" required at minimum two files. This created an unfortunate perception: C# is heavy, ceremonial, not suited for quick tasks.
Python and JavaScript developers would write a file and run it. C# developers would create infrastructure.
That changes with .NET 10.
Evolution of No-Project C# Execution
The path to file-based apps wasn't sudden. Understanding the evolution helps clarify what we have today and why it works the way it does.
The Legacy Era: csc.exe and Manual Compilation
Before the modern SDK, you could technically compile a single C# file using csc.exe directly:
# Legacy approach - DO NOT USE
csc HelloWorld.csThis produced an executable, but required the .NET Framework, Windows, manual reference management, and no NuGet support. It was compilation without convenience, useful only in very constrained scenarios.
Community Solutions: Scripts and REPLs
The community filled the gap with tools like:
- CS-Script: A scripting host for C# with its own conventions
- dotnet-script: A global tool providing
.csxscript files - Cake: A build automation system using C#
- LINQPad: An IDE for quick C# experimentation
These tools proved the demand existed. Developers wanted lightweight C# execution. But each used slightly different dialects, conventions, or file extensions. None felt like "real" C#.
Top-Level Statements: The Foundation
C# 9 introduced top-level statements, eliminating the Main method ceremony:
// Before C# 9
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
// C# 9 and later
Console.WriteLine("Hello, World!");This was transformative for code simplicity, but you still needed a .csproj file. The ceremony moved; it didn't disappear.
.NET 10: File-Based Apps as First-Class Citizens
.NET 10 completes the vision. You can now run a single .cs file directly:
dotnet run app.csNo project file required. No scaffolding. No temporary directories to clean up.
Critically, this isn't a new dialect or scripting mode. It's the same C#, the same compiler, the same runtime. The SDK generates a virtual project behind the scenes, compiles your code normally, and caches the output intelligently. When your code outgrows a single file, you convert it to a project-based app with one command.
This is what makes .NET 10's approach different from previous solutions: it's a stepping stone, not a dead end.
Introducing File-Based Apps in .NET 10
A file-based app is a C# program contained within a single .cs file that you build and run without a corresponding project file. The .NET SDK automatically generates the necessary project configuration based on directives embedded in your source file.
Key Characteristics
What File-Based Apps Are
- Real C# programs: Same compiler, same runtime, same performance
- Compiled, not interpreted: No runtime interpretation overhead
- Fully featured: NuGet packages, project references, SDK selection
- Debuggable: Full breakpoint and stepping support in VS Code
- Publishable: Native AOT by default when publishing
What File-Based Apps Are Not
- Script files: They're not interpreted like Python or Bash
- A replacement for projects: Complex applications still need project structure
- Multi-file programs (yet): .NET 10 supports single-file only; multi-file is planned for .NET 11
- Visual Studio supported: First-party IDE support is VS Code only; Visual Studio support is not planned
Supported Scenarios
File-based apps excel at:
- Command-line utilities and tools
- Automation scripts
- Learning and prototyping
- CI/CD helpers
- Quick data processing
- API exploration and testing
Current Limitations
- Single file only (no partial classes across files)
- No source generators
- No multi-targeting
- No test framework integration
- Limited IDE support outside VS Code
- No F# or VB.NET support
Running a Single C# File with .NET 10
Prerequisites
You need the .NET 10 SDK. Verify your installation:
dotnet --version
# Should output: 10.0.100 or higherIf you need to install it:
# Windows (winget)
winget install Microsoft.DotNet.SDK.10
# macOS (Homebrew)
brew install dotnet@10
# Linux (Ubuntu/Debian)
wget -O dotnet-install.sh
chmod +x dotnet-install.sh
./dotnet-install.sh --channel 10.0Hello World
Create a file named hello.cs:
Console.WriteLine("Hello from file-based C#!");Run it:
dotnet run hello.csOutput:
Hello from file-based C#!
That's it. No project file. No bin/ folder in your working directory.
What Happens Behind the Scenes
When you run dotnet run hello.cs:
- The SDK parses your file for
#:directives - A virtual
.csprojis generated in memory - The code compiles to a temporary directory
- The resulting executable runs
- Build artifacts are cached for subsequent runs
The cache location follows this pattern:
<temp>/dotnet/runfile/<appname>-<hash>/bin/<configuration>/
On Windows, this is typically under AppData\Local\Temp. On Linux/macOS, it's under /tmp.
Passing Command-Line Arguments
Arguments after the filename pass to your program:
// greet.cs
var name = args.Length > 0 ? args[0] : "World";
Console.WriteLine($"Hello, {name}!");dotnet run greet.cs -- Alice
# Output: Hello, Alice!Note the -- separator. Everything before it goes to the SDK; everything after goes to your program.
Exit Codes
Your program can return exit codes normally:
// validator.cs
if (args.Length == 0)
{
Console.Error.WriteLine("Error: No input provided");
return 1;
}
Console.WriteLine($"Validating: {args[0]}");
return 0;dotnet run validator.cs
echo $? # 1
dotnet run validator.cs -- test.json
echo $? # 0Environment Variables
Environment variables work exactly as in project-based apps:
// config.cs
var apiKey = Environment.GetEnvironmentVariable("API_KEY");
var debug = Environment.GetEnvironmentVariable("DEBUG") == "true";
Console.WriteLine($"API Key present: {!string.IsNullOrEmpty(apiKey)}");
Console.WriteLine($"Debug mode: {debug}");API_KEY=secret DEBUG=true dotnet run config.csConfiguring File-Based Apps
File-based apps use directives prefixed with #: to configure build behavior. These directives must appear at the top of your file, before any code.
Available Directives
Referencing NuGet Packages
Add packages with exact or wildcard versions:
#:package Newtonsoft.Json@13.0.3
#:package Serilog@4.*
#:package Microsoft.Extensions.Hosting@*
using Newtonsoft.Json;
using Serilog;
var json = JsonConvert.SerializeObject(new { Message = "Hello" });
Console.WriteLine(json);Version patterns:
#:package PackageName@13.0.3 // Exact version
#:package PackageName@13.0.* // Patch wildcard
#:package PackageName@13.* // Minor wildcard
#:package PackageName@* // Latest stable
#:package PackageName@*-* // Latest including prereleaseBest practice: Pin exact versions for reproducibility. Wildcards are convenient for experimentation but introduce non-determinism.
Setting MSBuild Properties
Configure any MSBuild property:
#:property LangVersion=preview
#:property Nullable=disable
#:property ImplicitUsings=disable
#:property TreatWarningsAsErrors=true
// Your code hereCommon properties:
Changing the SDK
Web APIs, Blazor, and other workloads require different SDKs:
#:sdk Microsoft.NET.Sdk.Web
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello from a single-file API!");
app.Run();Available SDKs:
Referencing Projects
You can reference existing project-based libraries:
#:project ../MyLibrary/MyLibrary.csproj
#:project ../SharedCode // Directory containing .csproj
using MyLibrary;
var result = Calculator.Add(2, 3);
Console.WriteLine(result);Using Directory.Build.props
For shared configuration across multiple file-based apps, create a Directory.Build.props in a parent directory:
<!-- Directory.Build.props -->
<Project>
<PropertyGroup>
<LangVersion>preview</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="4.0.0" />
</ItemGroup>
</Project>All file-based apps in that directory tree inherit these settings. This is useful for:
- Standardizing package versions across scripts
- Sharing common dependencies
- Enforcing team conventions
Secrets and Sensitive Configuration
For User Secrets support:
#:sdk Microsoft.NET.Sdk.Web
#:property UserSecretsId=2eec9746-c21a-4933-90af-c22431f35459
var builder = WebApplication.CreateBuilder(args);
// Secrets are available via configuration
var apiKey = builder.Configuration["ApiKey"];Initialize secrets using the standard command:
dotnet user-secrets set "ApiKey" "your-secret-key" --id 2eec9746-c21a-4933-90af-c22431f35459Reproducibility Considerations
For reproducible builds:
- Pin all package versions: Avoid wildcards
- Use global.json: Pin SDK version
- Commit Directory.Build.props: Version shared configuration
- Document external dependencies: Environment variables, secrets paths
// global.json
{
"sdk": {
"version": "10.0.100",
"rollForward": "disable"
}
}Realistic Example: File-Based App in Practice
Let's build a practical utility that demonstrates file-based apps handling real-world concerns: HTTP calls, JSON parsing, error handling, and logging.
The Task: GitHub Repository Stats Fetcher
This utility fetches public repository information from GitHub and displays formatted statistics.
// github-stats.cs
#:package System.Net.Http.Json@9.*
#:package Spectre.Console@0.49.*
using System.Net.Http.Json;
using Spectre.Console;
// Parse command-line arguments
if (args.Length == 0)
{
AnsiConsole.MarkupLine("[red]Usage:[/] dotnet run github-stats.cs -- <owner>/<repo>");
AnsiConsole.MarkupLine("[grey]Example:[/] dotnet run github-stats.cs -- dotnet/runtime");
return 1;
}
var repoPath = args[0];
var parts = repoPath.Split('/');
if (parts.Length != 2)
{
AnsiConsole.MarkupLine("[red]Error:[/] Invalid repository format. Use owner/repo");
return 1;
}
var (owner, repo) = (parts[0], parts[1]);
// Configure HTTP client with required headers
using var client = new HttpClient
{
BaseAddress = new Uri("https://api.github.com"),
DefaultRequestHeaders =
{
{ "User-Agent", "dotnet-file-based-app" },
{ "Accept", "application/vnd.github.v3+json" }
}
};
try
{
// Fetch repository data
AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.Start($"Fetching data for [blue]{repoPath}[/]...", ctx =>
{
// Synchronous wait for demo purposes
Thread.Sleep(100);
});
var response = await client.GetAsync($"/repos/{owner}/{repo}");
if (!response.IsSuccessStatusCode)
{
var error = response.StatusCode switch
{
System.Net.HttpStatusCode.NotFound => "Repository not found",
System.Net.HttpStatusCode.Forbidden => "Rate limited. Try again later",
_ => $"API error: {response.StatusCode}"
};
AnsiConsole.MarkupLine($"[red]Error:[/] {error}");
return 1;
}
var data = await response.Content.ReadFromJsonAsync<GitHubRepo>();
if (data is null)
{
AnsiConsole.MarkupLine("[red]Error:[/] Failed to parse response");
return 1;
}
// Display results
var table = new Table()
.Border(TableBorder.Rounded)
.AddColumn("Property")
.AddColumn("Value");
table.AddRow("Repository", $"[blue]{data.FullName}[/]");
table.AddRow("Description", data.Description ?? "[grey]No description[/]");
table.AddRow("Stars", $"[yellow]⭐ {data.StargazersCount:N0}[/]");
table.AddRow("Forks", $"🍴 {data.ForksCount:N0}");
table.AddRow("Open Issues", $"📋 {data.OpenIssuesCount:N0}");
table.AddRow("Language", data.Language ?? "[grey]Not specified[/]");
table.AddRow("License", data.License?.Name ?? "[grey]No license[/]");
table.AddRow("Created", data.CreatedAt.ToString("yyyy-MM-dd"));
table.AddRow("Last Updated", data.UpdatedAt.ToString("yyyy-MM-dd"));
AnsiConsole.Write(table);
return 0;
}
catch (HttpRequestException ex)
{
AnsiConsole.MarkupLine($"[red]Network error:[/] {ex.Message}");
return 1;
}
catch (TaskCanceledException)
{
AnsiConsole.MarkupLine("[red]Error:[/] Request timed out");
return 1;
}
// Type definitions (must be at end of file with top-level statements)
record GitHubRepo(
string FullName,
string? Description,
int StargazersCount,
int ForksCount,
int OpenIssuesCount,
string? Language,
LicenseInfo? License,
DateTime CreatedAt,
DateTime UpdatedAt
);
record LicenseInfo(string Name);Running the Example
dotnet run github-stats.cs -- dotnet/runtimeOutput:
╭─────────────┬──────────────────────────────────────────╮
│ Property │ Value │
├─────────────┼──────────────────────────────────────────┤
│ Repository │ dotnet/runtime │
│ Description │ .NET is a cross-platform runtime for ... │
│ Stars │ ⭐ 15,234 │
│ Forks │ 🍴 4,521 │
│ Open Issues │ 📋 8,432 │
│ Language │ C# │
│ License │ MIT License │
│ Created │ 2019-01-19 │
│ Last Updated│ 2026-01-19 │
╰─────────────┴──────────────────────────────────────────╯
What This Example Demonstrates
- Package references: Two packages with wildcard versions
- Async/await: Full async support just like project-based apps
- Error handling: Network errors, API errors, parse failures
- Exit codes: Proper return values for shell integration
- Type definitions: Records defined after top-level statements
- Rich console output: Using Spectre.Console for formatting
First Run vs Subsequent Runs
The first run compiles and caches:
github-stats succeeded (3.2s) → AppData\Local\Temp\dotnet\runfile\...
Subsequent runs with no changes skip compilation:
# No build output - runs immediately
If you modify the file, only the changed file rebuilds—not all dependencies.
Debugging & Tooling Experience
VS Code Setup
VS Code with C# Dev Kit provides first-party support for file-based apps.
Required extensions:
- C# (by Microsoft)
- C# Dev Kit (by Microsoft)
Enable file-based app support:
- Open VS Code settings (Ctrl+,)
- Search for "Enable File Based Programs"
- Check the option
Alternatively, add to settings.json:
{
"dotnet.enableFileBasedPrograms": true
}Starting a Debug Session
With a .cs file open:
- F5: Start debugging
- Select "C#" from the configuration dropdown
- VS Code builds and runs with debugger attached
Alternatively, click the play button in the editor tab.
What Works Without a Project
What You Lose Compared to Projects
Troubleshooting Debug Issues
"Please open a folder with a solution to debug"
You must have a folder open in VS Code, not just a single file. Open the directory containing your .cs file:
code .Breakpoints not hitting
Ensure the file has been built recently:
dotnet build yourfile.csThen start debugging.
IntelliSense not recognizing packages
After adding a new #:package directive, run:
dotnet restore yourfile.csThen reload the VS Code window.
When Debugging Friction Signals Migration
Consider converting to a project when you find yourself:
- Needing to debug interactions between multiple files
- Wanting test framework integration
- Requiring code coverage metrics
- Spending more time on tooling workarounds than development
- Collaborating with team members who use different editors
Publishing a File-Based App
File-based apps can be published to standalone executables, with Native AOT enabled by default.
Basic Publishing
dotnet publish app.csThis creates a Native AOT executable in the artifacts directory:
./artifacts/publish/app/release/
├── app # Native executable (Linux/macOS)
├── app.exe # Native executable (Windows)
└── app.pdb # Debug symbols
Publishing Options
Framework-Dependent (requires .NET runtime)
dotnet publish app.cs -p:PublishAot=falseOr add to your file:
#:property PublishAot=false
// Your codeSelf-Contained (includes runtime)
dotnet publish app.cs --self-contained true -r linux-x64Targeting Specific Runtimes
# Windows x64
dotnet publish app.cs -r win-x64
# macOS ARM (Apple Silicon)
dotnet publish app.cs -r osx-arm64
# Linux x64
dotnet publish app.cs -r linux-x64
# Linux ARM64
dotnet publish app.cs -r linux-arm64Custom Output Location
dotnet publish app.cs -o ./distNative AOT Considerations
Native AOT is enabled by default for file-based apps, which means:
Benefits:
- Fast startup time
- Smaller memory footprint
- No JIT compilation overhead
- Single-file deployment
Limitations:
- No runtime code generation (reflection may be limited)
- Some NuGet packages aren't AOT-compatible
- Platform-specific binaries (must publish per target)
To opt out:
#:property PublishAot=falseCI/CD Integration
File-based apps integrate naturally into CI pipelines.
GitHub Actions example:
name: Build and Publish Tool
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Publish for Linux
run: dotnet publish tool.cs -r linux-x64 -o ./artifacts/linux
- name: Publish for Windows
run: dotnet publish tool.cs -r win-x64 -o ./artifacts/windows
- name: Publish for macOS
run: dotnet publish tool.cs -r osx-x64 -o ./artifacts/macos
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: tool-binaries
path: ./artifacts/Packaging as a .NET Tool
File-based apps can be packaged as global tools:
dotnet pack app.csThis creates a NuGet package because PackAsTool=true is the default. Users can then install it:
dotnet tool install --global YourTool --add-source ./nupkgTo disable tool packaging:
#:property PackAsTool=falseConverting a File-Based App to a Project-Based App
At some point, your file-based app may need project structure. .NET 10 provides a seamless conversion path.
When to Convert
Convert to a project when you need:
The Conversion Command
dotnet project convert app.csThis command:
- Creates a new directory named after your file (e.g.,
app/) - Generates a
.csprojwith equivalent settings - Copies your
.csfile into the directory - Transforms
#:directives into proper MSBuild elements - Leaves the original file untouched
Example Conversion
Before (app.cs):
#:sdk Microsoft.NET.Sdk.Web
#:package Serilog@4.0.0
#:package Serilog.Sinks.Console@5.0.1
#:property LangVersion=preview
using Serilog;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
var app = builder.Build();
app.MapGet("/", () => "Hello!");
app.Run();After conversion:
app/
├── app.csproj
└── app.cs
app.csproj:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
</ItemGroup>
</Project>app.cs (directives removed):
using Serilog;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseSerilog();
var app = builder.Build();
app.MapGet("/", () => "Hello!");
app.Run();Post-Conversion Steps
After conversion, you may want to:
- Rename the main file to
Program.csfor convention - Add a solution file if working with multiple projects
- Split code into separate files for organization
- Add tests now that you have project structure
- Configure CI/CD for the new project structure
cd app
mv app.cs Program.cs
cd ..
dotnet new sln -n MySolution
dotnet sln add app/app.csprojCommon Migration Mistakes
Preserving Behavior
The converted project should behave identically. If it doesn't:
- Compare the generated
.csprojwith your original directives - Check for implicit settings that differ (ImplicitUsings, Nullable)
- Verify package versions match exactly
- Ensure SDK is correct
Comparison Table: File-Based App vs Project-Based App
When to Choose File-Based
✅ Quick prototypes and experiments ✅ One-off data processing scripts ✅ CI/CD utility scripts ✅ Learning and teaching ✅ Personal automation tools
When to Choose Project-Based
✅ Production applications ✅ Team collaboration ✅ Multi-file codebases ✅ Test-driven development ✅ Library development ✅ Long-term maintenance
Best Practices for Teams
When File-Based Apps Are Appropriate
Documentation Standards
Document file-based apps with comments at the top:
// migrate-data.cs
// Purpose: Migrate user data from legacy format to v2 schema
// Author: Team Infrastructure
// Last Updated: 2026-01-15
// Dependencies: Requires DB_CONNECTION_STRING environment variable
// Usage: dotnet run migrate-data.cs -- --dry-run
#:package Npgsql@8.0.0
#:package Dapper@2.1.0
// Implementation...Version Control Conventions
scripts/
├── README.md # Index of available scripts
├── migrate-data.cs # Data migration
├── generate-report.cs # Report generation
├── validate-config.cs # Configuration validation
└── Directory.Build.props # Shared configuration
README.md example:
# Team Scripts
| Script | Purpose | Usage |
|--------|---------|-------|
| migrate-data.cs | Migrate legacy data | `dotnet run migrate-data.cs -- --dry-run` |
| generate-report.cs | Weekly metrics report | `dotnet run generate-report.cs -- 2026-01-15` |Avoiding Script Sprawl
- Review periodically: Archive scripts that haven't been used in 6 months
- One purpose per file: Don't combine unrelated functionality
- Convert when growing: If a script exceeds ~200-300 lines, consider converting
- Standardize structure: Use consistent patterns across team scripts
Security Considerations
Supply chain security:
// Pin exact versions - don't use wildcards in production scripts
#:package Newtonsoft.Json@13.0.3 // ✅ Good
#:package Newtonsoft.Json@* // ❌ Risk: could pull malicious versionSecrets handling:
// ❌ Never hardcode secrets
var apiKey = "sk-1234567890";
// ✅ Use environment variables
var apiKey = Environment.GetEnvironmentVariable("API_KEY")
?? throw new InvalidOperationException("API_KEY not set");
// ✅ Or use User Secrets for development
#:property UserSecretsId=your-guid-hereInput validation:
// Validate all external input
if (!Path.GetFullPath(args[0]).StartsWith(allowedDirectory))
{
Console.Error.WriteLine("Error: Path outside allowed directory");
return 1;
}Recommended Team Conventions
- Naming: Use lowercase-with-hyphens:
migrate-data.cs,validate-config.cs - Location: Keep scripts in a dedicated
scripts/ortools/directory - Shared config: Use
Directory.Build.propsfor common settings - Documentation: Every script must have a header comment block
- Exit codes: Return 0 for success, non-zero for failure
- Error output: Write errors to stderr, results to stdout
Common Errors & Fixes
1. "The file does not exist"
Symptom:
Could not find file 'C:\path\to\app.cs'
Cause: Typo in filename or wrong working directory.
Fix:
# Verify the file exists
ls app.cs # or dir app.cs on Windows
# Use full path if needed
dotnet run ./scripts/app.cs2. "The SDK 'Microsoft.NET.Sdk.Web' could not be found"
Symptom:
error MSB4236: The SDK 'Microsoft.NET.Sdk.Web' specified could not be found.
Cause: Typo in SDK name or ASP.NET Core workload not installed.
Fix:
// Check for typos - it's Sdk, not SDK
#:sdk Microsoft.NET.Sdk.Web // ✅ Correct
#:sdk Microsoft.Net.Sdk.Web // ❌ Wrong capitalization# Install ASP.NET Core workload if missing
dotnet workload install aspnetcore3. "Package not found" or version resolution failures
Symptom:
error NU1102: Unable to find package SomePackage with version (>= 1.0.0)
Cause: Package doesn't exist, version doesn't exist, or NuGet source not configured.
Fix:
# Search for correct package name
dotnet package search SomePackage
# List available versions
dotnet package search SomePackage --exact-match
# Check NuGet sources
dotnet nuget list source4. First run is extremely slow
Symptom: Initial dotnet run app.cs takes 10+ seconds.
Cause: NuGet package restore and compilation on first run.
Fix: This is expected behavior. Subsequent runs use cached output:
# Pre-warm the cache
dotnet build app.cs
# Then run (will be fast)
dotnet run app.cs5. Changes not reflected when running
Symptom: Modified code doesn't execute after changes.
Cause: Build cache not invalidated, or editing wrong file.
Fix:
# Force rebuild
dotnet build app.cs --no-incremental
# Or clean and rebuild
dotnet clean app.cs
dotnet run app.cs6. "dotnet run" runs the project, not the file
Symptom: Running dotnet run app.cs from a directory with a .csproj runs the project instead.
Cause: The SDK finds the project file first.
Fix:
# Use explicit file path
dotnet run ./app.cs
# Or move to a directory without a project file7. Debugging doesn't start in VS Code
Symptom: F5 does nothing or shows "Please open a folder with a solution to debug."
Cause: No folder open, or file-based apps not enabled.
Fix:
- Open the folder containing the file:
code . - Enable file-based apps in settings:
"dotnet.enableFileBasedPrograms": true - Reload VS Code window
8. IntelliSense doesn't recognize packages
Symptom: Red squiggles under using statements for packages declared with #:package.
Cause: Packages not restored yet, or language server needs refresh.
Fix:
# Restore packages explicitly
dotnet restore app.csThen reload the VS Code window (Ctrl+Shift+P → "Reload Window").
9. "Top-level statements must precede namespace and type declarations"
Symptom:
error CS8803: Top-level statements must precede namespace and type declarations
Cause: Type definitions appear before executable code.
Fix: Move all class, record, struct, etc. definitions to the end of the file:
#:package SomePackage@1.0.0
// ✅ Executable code first
Console.WriteLine("Hello");
var data = new MyRecord("test");
// ✅ Type definitions last
record MyRecord(string Value);10. Native AOT publishing fails
Symptom:
error : Type 'SomeType' is not supported by Native AOT
Cause: Some packages or code patterns aren't AOT-compatible.
Fix:
// Disable Native AOT
#:property PublishAot=false
// Your code using reflection-heavy librariesOr use an AOT-compatible alternative package.
Decision Guide: File-Based App or Project?
Use this flowchart to decide:
START
│
▼
Is this a quick experiment or learning exercise?
│
├── Yes → FILE-BASED APP ✓
│
▼
Will this code be used by other team members?
│
├── Yes → Does it need tests?
│ │
│ ├── Yes → PROJECT ✓
│ │
│ ▼
│ Will it exceed 300 lines?
│ │
│ ├── Yes → PROJECT ✓
│ │
│ ▼
│ FILE-BASED APP (with documentation) ✓
│
▼
Is this a one-time or rarely-run script?
│
├── Yes → FILE-BASED APP ✓
│
▼
Does it need multiple source files?
│
├── Yes → PROJECT ✓
│
▼
Does it need source generators or analyzers?
│
├── Yes → PROJECT ✓
│
▼
Is it a library that others will reference?
│
├── Yes → PROJECT ✓
│
▼
Is it a long-running service or production app?
│
├── Yes → PROJECT ✓
│
▼
FILE-BASED APP ✓
Summary & Cheat Sheet
Key Takeaways
-
File-based apps are real C# programs, not scripts—same compiler, same runtime, same performance.
-
.NET 10 makes single-file execution first-class, with full NuGet, SDK, and project reference support.
-
Directives configure everything:
#:package,#:sdk,#:property,#:project. -
Publishing defaults to Native AOT, producing small, fast executables.
-
Conversion is seamless:
dotnet project converttransforms file-based apps to projects when needed. -
VS Code is the supported IDE for debugging file-based apps.
-
Multi-file support is coming in .NET 11—for now, keep it single-file or convert.
Command Cheat Sheet
# Run a file
dotnet run app.cs
# Run with arguments
dotnet run app.cs -- arg1 arg2
# Build without running
dotnet build app.cs
# Restore packages
dotnet restore app.cs
# Clean build artifacts
dotnet clean app.cs
# Publish (Native AOT by default)
dotnet publish app.cs
# Publish for specific platform
dotnet publish app.cs -r linux-x64
# Publish framework-dependent
dotnet publish app.cs -p:PublishAot=false
# Convert to project
dotnet project convert app.cs
# Pack as .NET tool
dotnet pack app.csDirective Quick Reference
// Reference a NuGet package (exact version)
#:package Newtonsoft.Json@13.0.3
// Reference a NuGet package (wildcard)
#:package Microsoft.Extensions.Logging@*
// Change the SDK
#:sdk Microsoft.NET.Sdk.Web
// Set an MSBuild property
#:property LangVersion=preview
#:property Nullable=enable
#:property PublishAot=false
// Reference another project
#:project ../MyLib/MyLib.csprojWhen to Bookmark This Approach
File-based apps shine for:
- Rapid prototyping during design discussions
- One-off data transformations
- Build pipeline utilities
- Learning new APIs or libraries
- Quick validation scripts
When to Move On
Convert to a project when:
- The file grows beyond comfortable reading
- You need to add tests
- Multiple people will maintain it
- It becomes a production dependency
- You need source generators or multi-targeting
What's Next?
File-based apps in .NET 10 represent a significant evolution in how we think about C# development. They lower the barrier to entry while maintaining the power and correctness that C# developers expect.
Related topics to explore:
-
File-Based Apps in CI Pipelines: Building robust automation with single-file C# utilities, handling secrets, and cross-platform deployment.
-
File-Based Apps vs PowerShell/Bash: When C# file-based apps are the better choice for automation, and when traditional shell scripts still win.
-
Building Internal Tooling with .NET 10: Creating a suite of team utilities with shared configuration, versioning, and distribution strategies.