File-based C# apps are a good fit when you need a small utility, repro, quick demo, proof of concept, migration helper, or one-off CLI task, but creating a full project feels heavier than the actual code.

With the .NET 10 SDK, a single .cs file can be built and run directly. It is not the same thing as old C# scripting. The SDK still compiles your C# code. It just generates the project shape for you behind the scenes.

A small example

Create a file called cleanup.cs:

if (args.Length == 0)
{
    Console.Error.WriteLine("Usage: dotnet run cleanup.cs -- <path>");
    return 1;
}

var path = args[0];

if (!Directory.Exists(path))
{
    Console.Error.WriteLine($"Directory not found: {path}");
    return 1;
}

var files = Directory.GetFiles(path, "*.tmp", SearchOption.AllDirectories);

foreach (var file in files)
{
    Console.WriteLine(file);
}

Console.WriteLine($"Found {files.Length} temporary files.");
return 0;

Run it:

dotnet run --file cleanup.cs -- ./artifacts

The -- matters. Everything after it is passed to your app instead of being interpreted by dotnet run.

The shorter form also works when you are not in a project directory:

dotnet run cleanup.cs -- ./artifacts

I would still use --file in repo scripts. If a project file exists in the current working directory, dotnet run cleanup.cs can run the project and pass cleanup.cs as an application argument. The explicit form avoids that.

Add packages in the file

File-based apps support #: directives at the top of the file. For example, this adds a NuGet package:

#:package System.CommandLine@2.0.0

using System.CommandLine;

var pathArgument = new Argument<DirectoryInfo>("path")
{
    Description = "Directory to scan."
};

var root = new RootCommand("Find temporary files.");
root.Arguments.Add(pathArgument);

root.SetAction(parseResult =>
{
    var path = parseResult.GetValue(pathArgument);
    if (path is null || !path.Exists)
    {
        Console.Error.WriteLine("Directory not found.");
        return 1;
    }

    var files = path.GetFiles("*.tmp", SearchOption.AllDirectories);
    foreach (var file in files)
    {
        Console.WriteLine(file.FullName);
    }

    return 0;
});

return root.Parse(args).Invoke();

The point is not System.CommandLine. The point is that the dependency lives with the utility. Someone can copy one file, run one command, and still get a compiled C# app.

What the SDK does behind the scenes

When you run a file-based app, the SDK creates a temporary project and build output. Later runs can reuse cached build output when the inputs have not changed.

That makes the workflow feel script-like:

dotnet run cleanup.cs -- ./logs

But the execution model is still normal .NET:

  • the code is compiled
  • NuGet restore can happen
  • MSBuild properties can apply
  • packages come from configured NuGet sources
  • the app can be built, published, packed as a .NET tool, or converted to a project

That matters when a utility grows from “quick helper” into “shared internal tool”. You are not stuck in a separate scripting model.

Convert it when it grows

If the file gets too large, convert it to a normal project:

dotnet project convert cleanup.cs

The command creates a project directory next to the original file, copies the C# file, and creates a .csproj with equivalent settings from the file-based app directives.

So you can start with one file and move to a project when you need tests, more files, or more explicit build configuration.

Preview note: multi-file apps

In the current preview tooling, file-based apps are no longer limited to exactly one source file.

The SDK supports #:include for adding more files:

#:include helpers.cs
#:include models/*.cs

The included .cs files become part of the compilation. They can contain types, methods, namespaces, and other declarations. They should not contain additional top-level statements.

.NET documents this for .NET 11 Preview 3 and .NET SDK 10.0.300 or later. I would treat it as a good fit for small helpers, not as a reason to avoid projects forever. Once the app starts looking like a real codebase, dotnet project convert is still the cleaner move.

Sources