File-Based Apps

Run a single .cs file with dotnet run app.cs, add packages and settings with #: directives, and know when to convert it to a project.

Syntax#:sdk Microsoft.NET.Sdk.Web #:package [email protected] #:property PublishAot=false // then run it with: dotnet run app.cs (or: dotnet app.cs)

The .NET 10 SDK runs a C# file with no project file: dotnet run app.cs, or simply dotnet app.cs. Behind the scenes the SDK generates a virtual project (console SDK, net10.0, implicit usings and nullable on), builds it into a cache outside your folder and runs it. A second run with an unchanged file skips the build. The file uses top-level statements and may declare types after them. Only that one file compiles; other .cs files next to it are ignored.

Directives

Lines starting with #: at the top of the file stand in for the project file. They must come before any code.

DirectiveExampleProject equivalent
#:package#:package [email protected]PackageReference with a version
#:sdk#:sdk Microsoft.NET.Sdk.WebThe Sdk attribute
#:property#:property InvariantGlobalization=trueAn MSBuild property
#:project#:project ../src/Orders.CoreProjectReference

On Linux and macOS a first line of #!/usr/bin/env dotnet plus chmod +x makes the file directly executable. Arguments after -- reach args.

Defaults that differ from a project

Running dotnet project convert app.cs shows the virtual project: besides the usual properties it sets PublishAot=true, PackAsTool=true and a generated UserSecretsId. Two consequences matter. dotnet publish app.cs produces a Native AOT executable, so a reflection-heavy package warns or breaks there. And because the AOT compiler packages are part of restore, a machine that cannot reach a NuGet feed fails even dotnet run until you add #:property PublishAot=false. The file also inherits any Directory.Build.props and Directory.Packages.props above it, so a script inside a repository picks up its warnings-as-errors and central versions.

When a single file is enough

  • Operations scripts: re-queue dead-lettered orders, reconcile a payments export, seed a test tenant.
  • Spikes: try an endpoint shape or a package API before touching the real service.
  • Bug reproductions small enough to paste into an issue.

Convert to a project when the code needs a second file, unit tests, a container image or a CI build, or when more than one person maintains it. dotnet project convert app.cs creates a folder holding a .csproj built from the directives and a copy of the source with the directives removed.

What goes wrong

Scripts are code that touches production data with nobody reviewing them. Read connection strings from environment variables or a secret store (a vault CLI or managed identity), never from literals in the file; user secrets are for development credentials only; pin every #:package version so the script behaves the same next month; and commit scripts the team relies on next to the service they operate on, where review and the repository's build settings apply.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Reconcile a payment provider's CSV export against order totals with a reviewed script committed next to the service.
  • Spike a new /inventory endpoint shape with OpenAPI output in one file before changing the real Orders.Api project.
  • Hand support engineers an executable re-queue tool for dead-lettered order messages without creating a project.

More examples

A throwaway web API in one file

<code>#:sdk Microsoft.NET.Sdk.Web</code> switches the virtual project to the Web SDK, so ASP.NET Core and its implicit usings are available exactly as in a project. This is a good way to try a response shape or an OpenAPI setting before changing the real service. <code>PublishAot=false</code> is set because reflection-based JSON for the anonymous type would not survive a Native AOT publish.

Example · text
#:sdk Microsoft.NET.Sdk.Web
#:package [email protected]
#:property PublishAot=false

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();
var app = builder.Build();

app.MapOpenApi();

var stock = new Dictionary<string, int> { ["SKU-KETTLE"] = 120, ["SKU-MUG"] = 0 };

app.MapGet("/inventory/{sku}", (string sku) =>
    stock.TryGetValue(sku, out var onHand)
        ? Results.Ok(new { sku, onHand, available = onHand > 0 })
        : Results.NotFound());

app.Run();

Running, passing arguments and converting

The first run builds and caches; later runs start quickly while the file is unchanged. The shebang line lets operators run the script like any other tool, but it still needs the .NET 10 SDK on that machine; a published executable does not. <code>dotnet project convert</code> leaves the original file alone and writes a new folder, so review the generated project before deleting the script.

Example · bash
# Run; everything after -- goes to args
dotnet run order-report.cs -- exports/orders-2026-10.csv 500
dotnet order-report.cs exports/orders-2026-10.csv 500

# Make it executable on Linux or macOS (first line of the file: #!/usr/bin/env dotnet)
chmod +x order-report.cs
./order-report.cs exports/orders-2026-10.csv 500

# Native executable (AOT is the default for file-based apps)
dotnet publish order-report.cs

# Outgrown a single file: turn it into a project
dotnet project convert order-report.cs

The project that convert produces

Each directive became a project element: <code>#:sdk</code> the <code>Sdk</code> attribute, <code>#:property</code> a property. The file-based defaults are written out explicitly, which is where <code>PublishAot</code> and <code>PackAsTool</code> surprise people. For a service you will containerise, decide deliberately: drop <code>PackAsTool</code>, and keep <code>PublishAot</code> only if every dependency is AOT-compatible.

Example · xml
<!-- web-spike/web-spike.csproj, generated by: dotnet project convert web-spike.cs -->
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
    <PackAsTool>true</PackAsTool>
    <UserSecretsId>web-spike-0d3c9a1f7e2b4c58a6d1e9f03b7c2a44</UserSecretsId>
    <InvariantGlobalization>true</InvariantGlobalization>
  </PropertyGroup>

</Project>

Discussion

  • Be the first to comment on this lesson.