dotnet run app.cs in .NET 10: File-Based Apps and 7 Errors

Last updated: October 30, 2026 · Tested with .NET SDK 10.0.401 (runtime 10.0.12), Humanizer 3.0.10, Windows 11

Short answer: since .NET 10 you can run a single C# file with dotnet run app.cs (or just dotnet app.cs), with no .csproj. NuGet packages, other files, the SDK and MSBuild properties go in #: directives at the top of the file. The part most tutorials skip: a file-based app has PublishAot=true by default, so reflection-based System.Text.Json is switched off even when you only dotnet run it. I ran 17 small cases on SDK 10.0.401 and broke each one on purpose; every output below is copied from those runs.

What went wrong What you see Section
Arguments without -- Required argument missing for option: '-v'. 2
A .csproj in the same folder error CS8802: Only one compilation unit can have top-level statements. 3
#:package without a version error NU1015: The following PackageReference item(s) do not have a version specified 4
Second .cs file not included error CS0103: The name 'Greeter' does not exist in the current context 5
#: directive after code error CS9297: '#:' directives cannot be after first token in file 6
JsonSerializer.Serialize in a file-based app InvalidOperationException: Reflection-based serialization has been disabled 7
Minimal API returns an anonymous object HTTP 500, JsonTypeInfo metadata ... was not provided 8

How I Tested It

Each case is its own folder with an app.cs, under a global.json that pins SDK 10.0.401. A Python script runs the dotnet command for each folder, saves stdout, stderr, exit code and time, and replaces my local paths with <work> and <temp>. The web cases start the app, call it with an HTTP request and save the status code and body. Nothing here comes from the docs alone: where the docs and my run agree, I show the run.

1. Run a C# File Without a Project

Console.WriteLine($"Hello from {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}");
Console.WriteLine($"Args: [{string.Join(", ", args)}]");
> dotnet run app.cs
Hello from .NET 10.0.12
Args: []
[exit 0, 0.4 s]

dotnet app.cs does the same thing. The first run builds a hidden project under %TEMP%\dotnet\runfile\ (1.7 s here, after clearing that cache with dotnet clean file-based-apps --days 0); later runs reuse it and took 0.4 s. A first run with a NuGet package took 11.5 s because of the restore.

You can also pipe code in with -, which is handy in shell scripts. This is the cmd.exe version:

> echo Console.WriteLine(6*7); | dotnet run -
42

2. Passing Arguments: Put Them After —

This is the first thing I got wrong. Without --, dotnet run reads the options itself, and -v is its own verbosity switch:

> dotnet run app.cs --name Rajni -v
Required argument missing for option: '-v'.
[exit 1, 0.3 s]

It also prints the full dotnet run help first. With --, everything after it goes to your args:

> dotnet run app.cs -- --name Rajni -v
Hello from .NET 10.0.12
Args: [--name, Rajni, -v]
[exit 0, 0.4 s]

3. A .csproj in the Same Folder Wins

I put app.cs next to an existing console project (Other.csproj with its own Program.cs). dotnet run app.cs doesn’t run the file. It builds the project and treats app.cs as an argument, and because SDK-style projects compile every .cs file in the folder, the build sees two files with top-level statements:

> dotnet run app.cs
Program.cs(1,1): error CS8802: Only one compilation unit can have top-level statements. [<work>\cases\03-csproj-next-door\Other.csproj]
Warning: 'app.cs' appears to be a file-based app but was passed as an argument to the project '<work>\cases\03-csproj-next-door\Other.csproj'. To run it as a file-based app, use 'dotnet run --file app.cs'. To pass it as an application argument, use 'dotnet run -- app.cs' to suppress this warning.

The warning tells you the fix:

> dotnet run --file app.cs
I am app.cs
[exit 0, 1.0 s]

Better still, keep scripts in their own folder. Folders above still count: I put a Directory.Build.props that defines a constant one level up, and the file-based app picked it up:

Directory.Build.props from the parent folder was applied

So a script inside a repo inherits the repo’s build settings (warnings as errors, analyzers, LangVersion). Usually that’s what you want; when a script fails to build for no visible reason, look there.

4. NuGet Packages with #:package

Without a version, the restore fails:

#:package Humanizer
app.cs.csproj : error NU1015: The following PackageReference item(s) do not have a version specified: Humanizer

A version-less #:package only works with central package management (a Directory.Packages.props above the file). Otherwise add @version, or @* for the latest:

#:package Humanizer@3.0.10
using Humanizer;
Console.WriteLine(TimeSpan.FromMinutes(90).Humanize(2));
Console.WriteLine("file-based app".Pascalize());
> dotnet run app.cs
1 hour, 30 minutes
FileBasedApp
[exit 0, 11.5 s]

I’d pin the version. @* means the script can change behaviour the next time a package ships.

5. More Than One File: #:include

A file-based app compiles only the file you run. A Greeter.cs next to it is ignored:

app.cs(1,19): error CS0103: The name 'Greeter' does not exist in the current context

#:include (SDK 10.0.300 and later) adds it:

#:include Greeter.cs
Console.WriteLine(Greeter.Hello("reader"));
app.cs(1,1): warning CA2266: File-based program entry point should start with '#!' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2266)
Hello, reader

It works, with a warning I didn’t expect. Once a file-based app has more than one file, the analyzer wants the entry file marked with a shebang line. Adding #!/usr/bin/env dotnet as the first line removed the warning; on Linux and macOS that same line lets you chmod +x app.cs and run it directly.

#!/usr/bin/env dotnet
#:include Greeter.cs
Console.WriteLine(Greeter.Hello("reader"));

Included files can hold types and methods, not top-level statements. A helper.cs with a Console.WriteLine at the top gives the same CS8802 as section 3:

app.cs(2,1): error CS8802: Only one compilation unit can have top-level statements.

6. Directives Must Come First

Console.WriteLine("hi");
#:package Humanizer@3.0.10
app.cs(2,2): error CS9297: '#:' directives cannot be after first token in file

Put every #: line at the top, before using directives and code. Only the #! line goes above them.

7. The AOT Default Breaks System.Text.Json

This one cost me the most time. This file has nothing unusual in it:

using System.Text.Json;
var order = new Order(42, "Keyboard", 49.90m);
Console.WriteLine(JsonSerializer.Serialize(order));
record Order(int Id, string Item, decimal Price);

In a normal console project it prints JSON. As a file-based app, plain dotnet run (no publish) crashes:

app.cs(3,19): warning IL3050: Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.
Unhandled exception. System.InvalidOperationException: Reflection-based serialization has been disabled for this application. Either use the source generator APIs or explicitly configure the 'JsonSerializerOptions.TypeInfoResolver' property.
at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_JsonSerializerIsReflectionDisabled()

The reason is in the app.runtimeconfig.json of the hidden build under %TEMP%\dotnet\runfile\:

"System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported": false,
"System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault": false,

File-based apps set PublishAot=true by default, and with that property the SDK writes these switches even for a debug dotnet run, so you find AOT problems before you publish. It isn’t special to file-based apps: I put the same code in a normal console project with <PublishAot>true</PublishAot> and dotnet run threw the same exception. The difference is that the console template doesn’t turn AOT on and a file-based app does. The IL2026/IL3050 warnings point at the line; the exception is what you get if you ignore them. There are two fixes.

Fix 1, keep AOT: use a source-generated context.

using System.Text.Json;
using System.Text.Json.Serialization;
var order = new Order(42, "Keyboard", 49.90m);
Console.WriteLine(JsonSerializer.Serialize(order, AppJson.Default.Order));
record Order(int Id, string Item, decimal Price);
[JsonSerializable(typeof(Order))]
partial class AppJson : JsonSerializerContext;

Fix 2, a quick script you’ll never publish: turn AOT off.

#:property PublishAot=false
using System.Text.Json;
> dotnet run app.cs
{"Id":42,"Item":"Keyboard","Price":49.90}
[exit 0, 2.4 s]

If you’re choosing a JSON library for scripts anyway, I compared both in System.Text.Json vs Newtonsoft.Json on .NET 10.

8. A Web API in One File

#:sdk Microsoft.NET.Sdk.Web turns the file into an ASP.NET Core app. This is the shortest version, and it compiles and starts:

#:sdk Microsoft.NET.Sdk.Web
var app = WebApplication.CreateBuilder(args).Build();
app.MapGet("/", () => new { message = "hello from a file-based web app" });
app.Run("http://localhost:5199");

But the first request fails, for the same AOT reason as section 7. The response is a 500 with an empty body; the console shows why:

fail: Microsoft.AspNetCore.Server.Kestrel[13]
System.NotSupportedException: JsonTypeInfo metadata for type '<>f__AnonymousType0`1[System.String]' was not provided by TypeInfoResolver of type '[]'. If using source generation, ensure that all root types passed to the serializer have been annotated with 'JsonSerializableAttribute', along with any types that might be serialized polymorphically.
Request finished HTTP/1.1 GET http://localhost:5199/ - 500 0 - 66.4465ms

A JsonSerializerContext needs a named type, so return a record and register the context:

#:sdk Microsoft.NET.Sdk.Web
using System.Text.Json.Serialization;

var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(o =>
    o.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJson.Default));

var app = builder.Build();
app.MapGet("/", () => new Hello("hello from a file-based web app"));
app.Run("http://localhost:5199");

record Hello(string Message);

[JsonSerializable(typeof(Hello))]
partial class AppJson : JsonSerializerContext;
> curl http://localhost:5199/
HTTP 200
{"message":"hello from a file-based web app"}

For a quick prototype, putting #:property PublishAot=false above the #:sdk line makes the anonymous-object version work too:

> curl http://localhost:5199/
HTTP 200
{"message":"hello from a file-based web app"}

For anything bigger than a few endpoints, see minimal APIs in ASP.NET Core and move to a project (section 10).

9. Publish to a Native Executable

dotnet publish app.cs produces a native AOT executable in an artifacts folder next to the file. For the source-generated JSON app from section 7:

> dotnet publish app.cs
app.cs -> <work>\cases\12-aot-json-fixed\artifacts\app\
[exit 0, 4.0 s]
2,163,712 app.exe
> artifacts\app\app.exe
{"Id":42,"Item":"Keyboard","Price":49.90}
[exit 0, 0.3 s]

A 2.1 MB exe that starts in a fraction of a second and needs no .NET runtime on the target machine. Native AOT on Windows needs the Visual Studio C++ build tools. On my machine the first publish failed with 'vswhere.exe' is not recognized as an internal or external command at the link step. A normal .csproj AOT publish failed the same way, so it’s my Visual Studio setup, not file-based apps; adding C:\Program Files (x86)\Microsoft Visual Studio\Installer to PATH fixed it.

Also note: the section 7 app without the fix publishes successfully (only warnings) and then crashes with the same InvalidOperationException when you run the exe. Treat IL2026 and IL3050 warnings as errors.

10. When It Outgrows One File: dotnet project convert

#:package Humanizer@3.0.10
#:property LangVersion=preview
using Humanizer;
Console.WriteLine("converted".Pascalize());

dotnet project convert app.cs creates an app folder with a copy of the file (directives removed) and this project file:

<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<PackAsTool>true</PackAsTool>
<LangVersion>preview</LangVersion>
<PackageReference Include="Humanizer" Version="3.0.10" />

The hidden defaults become visible: PublishAot and PackAsTool are both true, plus a UserSecretsId. If you converted because reflection-based JSON or another AOT-unfriendly library got in the way, delete the PublishAot line; delete PackAsTool unless you want dotnet pack to build a .NET tool. Running convert a second time fails with The target directory already exists, so it never overwrites your project.

Quick Reference

Task Command or directive
Run a file dotnet run app.cs or dotnet app.cs
Run when a .csproj is in the folder dotnet run --file app.cs
Pass arguments dotnet run app.cs -- --name x
Add a package #:package Name@version
Add another file #:include Greeter.cs (+ #!/usr/bin/env dotnet first)
Web app #:sdk Microsoft.NET.Sdk.Web
Turn off AOT defaults #:property PublishAot=false
Native exe dotnet publish app.cs
Clear the build cache dotnet clean file-based-apps
Move to a project dotnet project convert app.cs

Official reference: File-based apps on Microsoft Learn.

Related: System.Text.Json vs Newtonsoft.Json · C# source generators · minimal APIs · upgrading to .NET 11 · all C# tutorials

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *