CodeDancing with Milos
Back

Running C# Without a Project File - File-Based Apps in .NET 10 and C# 14

Post

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 .csproj file 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.cs

This 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 .csx script 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.cs

No 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

AspectFile-Based AppTraditional Project
Entry pointSingle .cs file.csproj + source files
Configuration#: directives in codeXML in .csproj
Build outputCached in temp directorybin/ and obj/ folders
NuGet packages#:package directive<PackageReference>
SDK selection#:sdk directiveSdk attribute in project
Multi-file supportNot in .NET 10 (coming in .NET 11)Full support
← Scroll horizontally to view more →

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 higher

If 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.0

Hello World

Create a file named hello.cs:

Console.WriteLine("Hello from file-based C#!");

Run it:

dotnet run hello.cs

Output:

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:

  1. The SDK parses your file for #: directives
  2. A virtual .csproj is generated in memory
  3. The code compiles to a temporary directory
  4. The resulting executable runs
  5. 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 $?  # 0

Environment 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.cs

Configuring 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

DirectivePurposeExample
#:packageReference NuGet packages#:package Newtonsoft.Json@13.0.3
#:projectReference other projects#:project ../MyLib/MyLib.csproj
#:propertySet MSBuild properties#:property LangVersion=preview
#:sdkChange the SDK#:sdk Microsoft.NET.Sdk.Web
← Scroll horizontally to view more →

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 prerelease

Best 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 here

Common properties:

PropertyValuesPurpose
LangVersiondefault, latest, preview, 14C# language version
Nullableenable, disable, warnings, annotationsNullable reference types
ImplicitUsingsenable, disableGlobal using directives
PublishAottrue, falseNative AOT publishing (default: true)
OutputPathPathCustom build output location
← Scroll horizontally to view more →

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:

SDKUse Case
Microsoft.NET.SdkConsole apps (default)
Microsoft.NET.Sdk.WebASP.NET Core web apps
Microsoft.NET.Sdk.WorkerBackground services
Microsoft.NET.Sdk.RazorRazor class libraries
Aspire.AppHost.Sdk.NET Aspire orchestration
← Scroll horizontally to view more →

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-c22431f35459

Reproducibility Considerations

For reproducible builds:

  1. Pin all package versions: Avoid wildcards
  2. Use global.json: Pin SDK version
  3. Commit Directory.Build.props: Version shared configuration
  4. 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/runtime

Output:

╭─────────────┬──────────────────────────────────────────╮
│ 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

  1. Package references: Two packages with wildcard versions
  2. Async/await: Full async support just like project-based apps
  3. Error handling: Network errors, API errors, parse failures
  4. Exit codes: Proper return values for shell integration
  5. Type definitions: Records defined after top-level statements
  6. 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:

  1. C# (by Microsoft)
  2. C# Dev Kit (by Microsoft)

Enable file-based app support:

  1. Open VS Code settings (Ctrl+,)
  2. Search for "Enable File Based Programs"
  3. Check the option

Alternatively, add to settings.json:

{
  "dotnet.enableFileBasedPrograms": true
}

Starting a Debug Session

With a .cs file open:

  1. F5: Start debugging
  2. Select "C#" from the configuration dropdown
  3. VS Code builds and runs with debugger attached

Alternatively, click the play button in the editor tab.

What Works Without a Project

FeatureSupported
Breakpoints✅ Yes
Step over/into/out✅ Yes
Variable inspection✅ Yes
Watch expressions✅ Yes
Call stack✅ Yes
Hot Reload✅ Yes (Edit and Continue)
Exception handling✅ Yes
Conditional breakpoints✅ Yes
← Scroll horizontally to view more →

What You Lose Compared to Projects

FeatureStatus
Solution Explorer integrationLimited
Test Explorer❌ Not available
IntelliSense for new packagesDelayed until first build
Refactoring across files❌ N/A (single file only)
Multi-project debugging❌ Not supported
Code coverage❌ Not available
← Scroll horizontally to view more →

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.cs

Then start debugging.

IntelliSense not recognizing packages

After adding a new #:package directive, run:

dotnet restore yourfile.cs

Then 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.cs

This 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=false

Or add to your file:

#:property PublishAot=false
 
// Your code

Self-Contained (includes runtime)

dotnet publish app.cs --self-contained true -r linux-x64

Targeting 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-arm64

Custom Output Location

dotnet publish app.cs -o ./dist

Native 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=false

CI/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.cs

This creates a NuGet package because PackAsTool=true is the default. Users can then install it:

dotnet tool install --global YourTool --add-source ./nupkg

To disable tool packaging:

#:property PackAsTool=false

Converting 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:

RequirementWhy It Needs a Project
Multiple source files.NET 10 file-based apps are single-file
Unit testsTest frameworks require project structure
Source generatorsNot supported in file-based apps
Multi-targetingDifferent TFMs need project configuration
AnalyzersProject-level analyzer configuration
NuGet publishingLibrary packages need proper project metadata
Team collaborationProjects provide standard structure
← Scroll horizontally to view more →

The Conversion Command

dotnet project convert app.cs

This command:

  1. Creates a new directory named after your file (e.g., app/)
  2. Generates a .csproj with equivalent settings
  3. Copies your .cs file into the directory
  4. Transforms #: directives into proper MSBuild elements
  5. 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:

  1. Rename the main file to Program.cs for convention
  2. Add a solution file if working with multiple projects
  3. Split code into separate files for organization
  4. Add tests now that you have project structure
  5. 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.csproj

Common Migration Mistakes

MistakeSolution
Forgetting to remove #: directivesConversion handles this automatically
Running from wrong directoryRun conversion from directory containing .cs file
Expecting in-place conversionConversion creates a new directory
Losing custom Directory.Build.propsCopy relevant settings to new project
← Scroll horizontally to view more →

Preserving Behavior

The converted project should behave identically. If it doesn't:

  1. Compare the generated .csproj with your original directives
  2. Check for implicit settings that differ (ImplicitUsings, Nullable)
  3. Verify package versions match exactly
  4. Ensure SDK is correct

Comparison Table: File-Based App vs Project-Based App

AspectFile-Based AppProject-Based App
Startup timeFirst run: 2-5s (compile + cache). Subsequent: ~100msFirst run: Similar. Subsequent: ~100ms
Files required1 .cs fileMinimum: 1 .csproj + 1 .cs
NuGet packages#:package directive<PackageReference> in csproj
Package restoreImplicit on runExplicit or implicit
Multi-file support❌ No (.NET 10)✅ Yes
Unit testing❌ Not supported✅ Full support
IntelliSenseAfter first buildImmediate
RefactoringLimitedFull support
Source generators❌ Not supported✅ Full support
Multi-targeting❌ Not supported✅ Full support
PublishingNative AOT defaultConfigurable
CI/CDSimple (single file)Standard MSBuild
Team scalabilityIndividual useTeam collaboration
Version controlSingle fileMultiple files
IDE supportVS CodeVS Code, Visual Studio, Rider
Long-term maintenanceShort-lived scriptsProduction applications
← Scroll horizontally to view more →

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

AppropriateNot Appropriate
Build/deployment scriptsCore business logic
Data migration utilitiesShared libraries
One-time processing tasksLong-running services
Local development toolsCustomer-facing applications
Quick API explorationMulti-developer projects
← Scroll horizontally to view more →

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

  1. Review periodically: Archive scripts that haven't been used in 6 months
  2. One purpose per file: Don't combine unrelated functionality
  3. Convert when growing: If a script exceeds ~200-300 lines, consider converting
  4. 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 version

Secrets 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-here

Input validation:

// Validate all external input
if (!Path.GetFullPath(args[0]).StartsWith(allowedDirectory))
{
    Console.Error.WriteLine("Error: Path outside allowed directory");
    return 1;
}
  1. Naming: Use lowercase-with-hyphens: migrate-data.cs, validate-config.cs
  2. Location: Keep scripts in a dedicated scripts/ or tools/ directory
  3. Shared config: Use Directory.Build.props for common settings
  4. Documentation: Every script must have a header comment block
  5. Exit codes: Return 0 for success, non-zero for failure
  6. 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.cs

2. "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 aspnetcore

3. "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 source

4. 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.cs

5. 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.cs

6. "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 file

7. 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:

  1. Open the folder containing the file: code .
  2. Enable file-based apps in settings:
    "dotnet.enableFileBasedPrograms": true
  3. 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.cs

Then 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 libraries

Or 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

  1. File-based apps are real C# programs, not scripts—same compiler, same runtime, same performance.

  2. .NET 10 makes single-file execution first-class, with full NuGet, SDK, and project reference support.

  3. Directives configure everything: #:package, #:sdk, #:property, #:project.

  4. Publishing defaults to Native AOT, producing small, fast executables.

  5. Conversion is seamless: dotnet project convert transforms file-based apps to projects when needed.

  6. VS Code is the supported IDE for debugging file-based apps.

  7. 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.cs

Directive 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.csproj

When 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:

  1. File-Based Apps in CI Pipelines: Building robust automation with single-file C# utilities, handling secrets, and cross-platform deployment.

  2. File-Based Apps vs PowerShell/Bash: When C# file-based apps are the better choice for automation, and when traditional shell scripts still win.

  3. Building Internal Tooling with .NET 10: Creating a suite of team utilities with shared configuration, versioning, and distribution strategies.