Skip to main content

How to Run a Single C# File with dotnet run (.NET 10 File-Based Apps)

· 8 min read
Jagdish Kumawat
Founder @ Dewiride

Until .NET 10, even a three-line C# program needed a project file. Now dotnet run hello.cs runs a single .cs file directly, and a line at the top can pull in NuGet packages. It's perfect for small tools and quick tests, like checking that a database connection string works. This guide writes one, adds a package, shows where .NET builds it, and turns it into a normal project when it outgrows one file.

What a File-Based App Is​

Microsoft's file-based apps page describes them: "File-based apps let you build, run, and publish .NET applications from a single C# file without creating a traditional project file." The SDK does the project work behind the scenes: "The .NET SDK automatically generates the necessary project configuration based on the directives in your source file."

Prerequisites​

  • The .NET 10 SDK or later. The page says: "This article applies to: ✔️ .NET 10 SDK and later versions". Check yours with dotnet --version; this guide uses 10.0.403.
  • An empty folder for the file. Not a folder with a project file in it: Step 2 explains why.

Step 1: Write and Run One Line​

Create hello.cs with a single line:

hello-csharp/hello.cs
Console.WriteLine($"Hello from .NET {Environment.Version} on {Environment.OSVersion.Platform}");

There's no Main method and no namespace. C#'s top-level statements let the file's code be the program. Run it:

Terminal (in hello-csharp)
dotnet run hello.cs
Output
Hello from .NET 10.0.14 on Unix

A terminal where dotnet run hello.cs and the shorter dotnet hello.cs both print Hello from .NET 10.0.14 on Unix, from a folder that contains only hello.cs.

10.0.14 is the runtime the program ran on, and macOS reports itself as Unix. There's an even shorter form, which gave the same output:

Terminal
dotnet hello.cs

The folder still contains only hello.cs. The build output goes elsewhere, as Step 4 shows.

Step 2: Know When dotnet run Ignores Your File​

There's one trap. The page explains: "When a project file exists in the current working directory, dotnet run file.cs without the --file option runs that project and passes file.cs as an argument to the target app to preserve backwards compatibility." So in a folder with a .csproj, your file isn't what runs. Keep file-based apps in their own folder, or say so explicitly:

Terminal
dotnet run --file hello.cs

Step 3: Add a NuGet Package with #:package​

Lines that start with #: at the top of the file configure the build; the page calls them directives: "File-based apps use directives prefixed with #: to configure the build and run the application." #:package "Adds a NuGet package reference to your application."

Here's a real, useful one: a program that tests a MySQL connection string, with Oracle's MySql.Data driver:

mysql-connection-test/test-connection.cs
#:package MySql.Data@26.10.0

using MySql.Data.MySqlClient;

// The connection string comes from an environment variable, so no password is written in the code.
var connectionString = Environment.GetEnvironmentVariable("MYSQL_CONNECTION_STRING")
?? throw new InvalidOperationException("Set the MYSQL_CONNECTION_STRING environment variable first.");

await using var connection = new MySqlConnection(connectionString);
await connection.OpenAsync();

// Ask MySQL who we are, which version it runs, and which TLS version this connection uses.
await using var command = new MySqlCommand(
"SELECT CURRENT_USER(), VERSION(), " +
"(SELECT VARIABLE_VALUE FROM performance_schema.session_status WHERE VARIABLE_NAME = 'Ssl_version')",
connection);
await using var reader = await command.ExecuteReaderAsync();
await reader.ReadAsync();

var tls = reader.GetString(2);
Console.WriteLine($"Connected as {reader.GetString(0)} to MySQL {reader.GetString(1)}");
Console.WriteLine($"TLS: {(tls.Length == 0 ? "not used" : tls)}");

#:package MySql.Data@26.10.0 means "the package MySql.Data, version 26.10.0". Always write the version: the page says that leaving it out "currently only works when you use central package management with a Directory.Packages.props file", and otherwise to "specify the version number explicitly". await works at the top level too, so the program can be asynchronous without any ceremony.

With the connection string in the MYSQL_CONNECTION_STRING environment variable, dotnet run test-connection.cs printed:

Output
Connected as inventory_app@% to MySQL 9.7.4
TLS: TLSv1.2

How to Write a MySQL Connection String for .NET C# shows how to set the variable without typing the password.

Step 4: See Where It Was Built​

dotnet build works on a single file too, and shows where the output goes:

Terminal (in mysql-connection-test)
dotnet build test-connection.cs
Output
Restore complete (1.9s)
test-connection.cs net10.0 succeeded (0.4s) → /Users/you/Library/Application Support/dotnet/runfile/test-connection-6ef13498c57fd7651a0dea955e431ead95d0ecf337b11b25e4cef8a68730367c/bin/debug/test-connection.dll

Build succeeded in 2.6s

On this Mac, the build lives under ~/Library/Application Support/dotnet/runfile/, in a folder named after the file plus a long hash. That's why the program's own folder stays clean. Microsoft's pages describe the location differently; the file-based apps page says "the system's temporary directory under <temp>/dotnet/runfile/…", while its tutorial shows the Library/Application Support path for Unix. The path your build prints is the one that counts. The first run is slower because of this build; the page notes that "The .NET SDK caches build outputs to improve performance on subsequent invocations of dotnet run."

Step 5: Convert It to a Project When It Grows​

When the program needs a second file, tests, or settings a directive can't express, convert it:

Terminal (in mysql-connection-test)
dotnet project convert test-connection.cs --output test-connection-project

It prints nothing. Afterwards, the folder holds the original file and a new project folder:

The folder after converting
test-connection.cs
test-connection-project/test-connection.cs
test-connection-project/test-connection.csproj

The original test-connection.cs is untouched; the new folder holds a copy, without the #:package line, and a project file:

test-connection-project/test-connection.csproj
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<PackAsTool>true</PackAsTool>
<UserSecretsId>test-connection-…</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MySql.Data" Version="26.10.0" />
</ItemGroup>

</Project>

A terminal listing the folder after dotnet project convert: the original test-connection.cs plus a test-connection-project folder with a copy of the file and a test-connection.csproj that references MySql.Data 26.10.0 and sets PublishAot and PackAsTool.

The project shows the defaults a file-based app had all along: net10.0, implicit usings, nullable reference types, and three you might not expect:

  • PublishAot: the page notes that "File-based apps enable native AOT publishing by default", and gives the way out for a single file: "Disable this feature by adding #:property PublishAot=false at the top of your file."
  • PackAsTool: the program can be packed as a .NET tool.
  • UserSecretsId: the app can read .NET user-secrets in development.

Without --output, the command asks where to put the project, in your terminal: Specify the output directory (press Enter for default: 'test-connection', directory must not exist):. --output answers that up front, which is also what scripts need.

Common Mistakes​

MistakeWhat happensDo this instead
Running a .cs file in a folder with a .csprojThe project runs instead of your fileUse its own folder, or dotnet run --file
#:package without a versionFails unless the folder uses central package managementWrite Name@Version
Directives below the codeThey're not read as directivesPut every #: line at the top of the file
Expecting a bin folder next to the fileIt isn't thereRead the path dotnet build prints

Troubleshooting​

SymptomLikely causeFix
The first run takes several secondsThe SDK restores packages and builds the file onceRun it again; later runs reuse the build
dotnet project convert waits for inputIt's asking for the output folderPress Enter, or pass --output

FAQ​

Is this a script, like a .ps1 or .sh file?​

It's a real C# program, compiled and type-checked like any project, just without the project file. On macOS and Linux it can behave like a script, though: with #!/usr/bin/env dotnet as its first line and chmod +x hello2.cs, running ./hello2.cs printed Hello from a shebang.

Can a file-based app use Directory.Build.props from a parent folder?​

Yes. I put a Directory.Build.props that sets <AssemblyName>NameFromDirectoryBuildProps</AssemblyName> in a parent folder, and dotnet build hello2.cs produced NameFromDirectoryBuildProps.dll. So a file-based app inside a solution folder gets the solution's build settings, strict analyzers and warnings-as-errors included.

Conclusion​

With .NET 10, dotnet run file.cs, or just dotnet file.cs, runs a single C# file with no project. #:package Name@Version adds NuGet packages, the build output lives outside your folder, and dotnet project convert file.cs --output folder turns it into a normal project, leaving the original untouched, once it needs more than one file.

Additional Resources​

Stay Updated

Subscribe to our newsletter for the latest tutorials, tech insights, and developer news.

By subscribing, you agree to our privacy policy. Unsubscribe at any time.