C# File-Based Apps: Run a Single .cs File With dotnet run
Seven lessons in this Part made code inside a .cs file shorter to write. This last one changes what has to exist around that file before you're even allowed to run it.
Part XII opened with a tour of what's new in C# 14 (323) and has spent seven lessons walking through it one feature at a time: extension members (324), field-backed properties (325), null-conditional assignment (326), enhanced span conversions (327), lambda parameter modifiers (328), partial constructors (329), and user-defined compound assignment (330). Every one of them shares a family resemblance — each removes a small, specific piece of ceremony from code you were already going to write, inside a project you'd already set up.
This closing lesson is different in kind, not just in degree. File-based apps don't change anything about the C# language itself — every feature from 323 through 330 still works exactly the same way inside one. What they change is the ceremony required just to get a single C# file running at all: with dotnet run app.cs, .NET 10 lets you run a plain C# file directly, no .csproj required, with a documented path to graduate into a full project the moment the file outgrows being a single file.
What Is It?
The Simple Explanation
A file-based app is a single .cs file you run directly with dotnet run app.cs — no dotnet new console, no hand-authored .csproj, nothing to create ahead of time. If the script needs a NuGet package or a particular SDK, you say so with a special directive right at the top of the same file, instead of editing a separate project file that doesn't exist yet.
The Technical Definition
C# 14, shipping with .NET 10, adds first-class tooling support for compiling and running a single .cs file without a project file, via dotnet run <file>.cs. Directives beginning with #: — such as #:sdk and #:package — appear at the very top of the file and stand in for the handful of settings a .csproj would otherwise declare: which SDK to target, and which NuGet packages to reference. The SDK synthesizes an implicit project definition from the file itself plus its directives, compiles it with the same Roslyn compiler used for any other C# project, and runs it. A documented conversion command turns a file-based app into a conventional project-based app once it needs to grow past what a single file can reasonably hold.
Project-Based (until now, the only option)
- dotnet new console creates a folder, a .csproj, and a starter file
- Package references, SDK, and target framework live in the .csproj
- Right shape for anything real: multi-file apps, libraries, anything with tests/CI
File-Based — C# 14 / .NET 10
- dotnet run app.cs runs a single file directly, nothing created ahead of time
- Package references, SDK, and similar settings live inline as #: directives, at the top of the same file
- Right shape for quick scripts, one-off tools, and learning — with a documented path to convert into a project
Why Does It Exist?
The Problem — a Real Gap at the Small End of the Spectrum
For a genuine application, the ceremony of dotnet new console plus a .csproj is a trivial, one-time cost set against months of ongoing work — nobody building a real system has ever seriously complained about it. But for a five-line idea — a quick script to reshape a CSV, a one-off tool to hit an internal API once, or someone's very first fifteen minutes ever writing C# — that same ceremony is disproportionate friction, not value. Other ecosystems have long had a "just run the file" answer for exactly this case; C# didn't have a first-class one. What existed instead were either separate, third-party scripting tools, or the informal habit of spinning up a full throwaway console project for something that was never going to need one.
The Solution — the Real Language, With the Ceremony Made Optional
File-based apps close that gap without inventing a separate, lesser dialect of C#. The file is compiled by the same Roslyn compiler, using the same language you've used throughout this entire course — every feature from lessons 323 through 330 works identically inside one. What changes is only the project-file ceremony around it: the SDK infers an implicit project from the file itself, and #: directives cover the small number of settings a script actually tends to need, right inline. And critically, the gap between "quick script" and "real project" isn't a wall — a documented command converts a file-based app into a conventional project the moment it earns that shape, carrying its directives over as the equivalent project settings.
Big Picture
How It Works
Console.WriteLine("Hello!");saved ashello.cs— nothing else needs to exist yet.
- The SDK synthesizes an implicit project definition from the file alone — default SDK, default target framework — compiles it, and runs it. No
.csprojwas ever created.
#:package Humanizer@2.14.1or#:sdk Microsoft.NET.Sdk.Web— parsed before the rest of the file, translated into the equivalent project setting the implicit project needs.
- A documented tooling command turns the file-based app into a conventional project, generating an actual
.csprojwhose settings mirror the#:directives the file already had — nothing about the app's own code has to be rewritten.
Simple Example
// report.cs — a single, complete file-based app. No .csproj exists anywhere.
#:package Humanizer@2.14.1
using Humanizer;
var minutesAgo = 5;
Console.WriteLine($"Report generated {minutesAgo.Minutes().Humanize()} ago.");
$ dotnet run report.cs
Report generated 5 minutes ago.
Meaning: The #:package directive is doing the same job a <PackageReference Include="Humanizer" Version="2.14.1" /> line would do inside a .csproj — it's just spelled inline, at the top of the one file that needs it, with no separate project file to create or maintain for a script this small.
Real-World Example — a Script That Graduates
An engineer needs to bulk-rename a folder of report files before a demo — a ten-minute task, not a project. A file-based app is exactly the right shape for it:
// rename-reports.cs
var folder = args.Length > 0 ? args[0] : Directory.GetCurrentDirectory();
foreach (var path in Directory.GetFiles(folder, "*.csv"))
{
var newName = Path.GetFileNameWithoutExtension(path) + "-2026" + Path.GetExtension(path);
File.Move(path, Path.Combine(folder, newName), overwrite: false);
Console.WriteLine($"Renamed: {Path.GetFileName(path)} -> {newName}");
}
Run with dotnet run rename-reports.cs C:\Reports, and it's done — no project ever created for a script that ran twice and was forgotten. But suppose this particular script didn't get forgotten: three other people on the team start running it weekly, someone asks for a --dry-run flag, then a config file, then a small test to make sure it never overwrites an existing file by accident. That's the exact moment a file-based app is meant to graduate — past roughly a couple hundred lines, past a single concern, past "nobody else needs this," the documented conversion command turns it into a conventional project with a real .csproj, ready for the multiple files, tests, and CI pipeline it has now earned. Nothing about that path was a dead end reached too late — it was designed in from the start.
Analogy
Sometimes you need to write something down for thirty seconds — a phone number, a reminder — and a sticky note on the monitor is exactly the right tool: no folder to create, no filing system to set up, you just write it and stick it there. Other times, information genuinely needs to become an official filed document — indexed, backed up, findable by anyone on the team a year from now. Nobody would seriously suggest filing every sticky note as a formal document, and nobody would seriously suggest important company records should live as sticky notes either. The skill is knowing which one a given piece of information actually is — and, when a sticky note turns out to matter more than expected, having an easy, well-known way to make it official instead of just hoping nobody loses the note. A file-based app is the sticky note. A conventional project is the filed document. dotnet project convert is the moment you finally put it in the filing cabinet.
Under the Hood
It's worth being precise about what a file-based app is not: it is not code being interpreted line by line the way an old-style REPL or script runner might work. The .cs file is compiled by the exact same Roslyn compiler used for any ordinary project, into a real assembly, cached on disk under a build directory the SDK manages — the same fundamental pipeline lesson 184's source-generator coverage already showed you runs on ordinary projects, just triggered from a single file instead of a .csproj.
#: directives are handled before the rest of the file is treated as C# at all — the SDK's tooling scans for them first and translates each one into the equivalent MSBuild project concept: #:sdk becomes the project's SDK attribute, #:package becomes a <PackageReference> item, and so on. This is exactly why the feature composes so cleanly with project conversion — the directives were never a separate, parallel configuration system; they're a terser, inline spelling of the same handful of settings a .csproj already understands, which is also why they must appear at the very top of the file, before any using directive or ordinary code, so the tooling can find and process all of them before compilation of the rest of the file begins.
Common Confusion
1. "This is basically the old .csx / csi scripting mode" — no, it's the real compiled language
Older C# scripting tools ran a looser dialect with its own quirks and its own separate tooling story. A file-based app is compiled by the same Roslyn compiler as any project, using the exact same language rules covered throughout this entire course — there is no relaxed "script mode" C# to separately learn.
2. "This replaces conventional projects for real apps" — no, it targets a different, smaller job
Nothing about a multi-file application, a class library, or anything with a real deployment and test story is better served by staying single-file. File-based apps are aimed specifically at the gap below that — quick scripts, one-off tools, and learning — and the moment an app needs more than one file, that's precisely the signal to convert, not a limitation to fight around.
Common Mistakes
Mistake 1 — Letting a script outgrow one file without ever converting it
Continuing to pile logic into a single, ever-growing .cs file well past the point where multiple files, tests, and a real project structure would obviously serve better. Treat "this needs more than one file" as the conversion trigger it's meant to be — the documented conversion command exists precisely so this isn't a hard decision to put off.
Mistake 2 — Misplacing a #: directive
Putting a #:package line after a using directive or other code, expecting it to still be picked up. #: directives must appear at the very top of the file, before anything else — that's what lets the tooling find and process every one of them before the rest of the file is compiled as ordinary C#.
Mistake 3 — Reaching for a full project when a file-based app would have been the better starting point
Spinning up dotnet new console, a folder, and a .csproj for a script that will run once and be deleted — exactly the ceremony this feature exists to make optional. Start with a single file for anything genuinely small and disposable; the graduation path means nothing is lost by starting small and converting later if it turns out to matter.
When Should I Use It?
- Quick, disposable scripts — reshaping a file, hitting an API once, automating a small local task.
- One-off tools that genuinely fit in a single file, especially ones only you (or a small team) will ever run.
- Learning and experimentation — trying out a language feature or an API in isolation, with none of the project-setup overhead standing between you and running the code.
- Not for multi-file applications, libraries, or anything that needs its own dedicated test suite and CI pipeline — reach for a conventional project (or convert into one) the moment single-file stops being the honest shape of the problem.
Mental Model
#: directives = the handful of settings a .csproj would hold, spelled inline instead
Conversion command = the built-in, no-loss path from "quick script" to "real project"
Remember: a sticky note, not a lesser language — and never a replacement for a real project once the work has earned one.
Key Takeaway — Closing Part XII
- A file-based app is a single .cs file run directly with dotnet run app.cs — the exact same C# language and compiler as any project, with #: directives standing in for the few settings a .csproj would otherwise declare.
- It is not a return to a separate, scripting-only C# dialect, and it is not a replacement for conventional projects on real applications — it targets a gap that specifically didn't have a first-class answer before: quick scripts, one-off tools, and learning.
- A documented conversion command turns a file-based app into a full project the moment it outgrows a single file, carrying its #: directives over as equivalent project settings — nothing about starting small has to be a dead end.
- Zoom out across this whole Part, and eight lessons turn out to share one throughline: C# keeps sanding down the friction between what you mean and what you have to type — or set up — to say it. Extension members (324) let you extend a type's shape more naturally; field-backed properties (325) remove a boilerplate backing field; null-conditional assignment (326) collapses a defensive check into one operator; enhanced span conversions (327) quietly remove casts that used to get in the way; lambda parameter modifiers (328) stop forcing a redundant type onto a modifier-carrying parameter; partial constructors (329) extend a pattern source generators already relied on (lesson 184) to one more member kind; and user-defined compound assignment (330) lets a type's author decide that += can be cheaper than a full reconstruction. Each one is small in isolation — none of them teaches a new way to think about a program — they simply remove a small tax you were paying to write the program you already had in mind.
- File-based apps are a different kind of change, and it's worth closing on that difference precisely. The other seven lessons made existing C# code shorter or cheaper to write; this lesson doesn't touch what's inside a .cs file at all — it changes what has to exist AROUND that file before you're allowed to run it. For years, C# quietly told anyone with a five-line idea: first, create a project. That barrier never bothered anyone building a real application — a .csproj is a small, one-time cost against months of work — but it was real friction for a script, a scratchpad experiment, or a first fifteen minutes learning the language, exactly where other ecosystems' "just run the file" model had a genuine edge. dotnet run app.cs, with #: directives standing in for the handful of settings a script actually needs, closes that gap without inventing a lesser, scripting-only version of the language — and the built-in path back to a conventional project the moment a file outgrows itself means this was never a trade-off between "quick" and "real." It's the same C#, finally willing to meet you where a five-line idea actually starts.
Check Your Understanding
You've seen how file-based apps close a real gap in C# without compromising what the language actually is. Let's confirm it clicked — and close out Part XII.
1. What does #:package Humanizer@2.14.1 at the top of a file-based app actually correspond to?
Show answer
Correct: B
Why B is correct: As Under the Hood explained, #: directives are parsed before the rest of the file and translated into the equivalent MSBuild project concept — #:package specifically becomes a <PackageReference> item, exactly as it would in a hand-authored .csproj.
Why A is incorrect: It's an active directive that genuinely affects what the implicit project references and compiles against — not an inert comment.
Why C is incorrect: Package restoration happens as part of the build process the SDK runs, not as a runtime, on-demand download triggered by executing a line of code.
Why D is incorrect: It's tied directly to ordinary NuGet package referencing, using the same package identity and version format any .csproj would use.
Reinforcement: #: directives are a terser, inline spelling of ordinary project-file concepts — not a separate configuration system.
2. Which statement best captures how file-based apps should be framed, according to this lesson?
Show answer
Correct: B
Why B is correct: This is the precise framing the lesson's callout insisted on — real, fully compiled C#, targeted at a specific gap (scripts, one-off tools, learning) that genuinely lacked a first-class answer before, without displacing conventional projects for real applications.
Why A is incorrect: This is exactly the misconception the callout explicitly warned against — file-based apps are not a looser scripting dialect, and are not positioned to replace conventional projects.
Why C is incorrect: The lesson's own example used #:package to reference a NuGet package (Humanizer) inside a file-based app — package references are fully supported.
Why D is incorrect: While learning is one legitimate use, the lesson also frames quick scripts and one-off tools as genuine, practical uses — not merely a teaching gimmick.
Reinforcement: Real C#, aimed at a real gap — neither a toy dialect nor a replacement for proper projects.
3. A file-based app that started as a ten-line script has grown to include multiple concerns, several other people now depend on it weekly, and it could use its own tests. What does this lesson say should happen?
Show answer
Correct: C
Why C is correct: This is precisely the "graduation path" the lesson described — a documented conversion command exists specifically for this moment, translating the file's #: directives into a real .csproj's equivalent settings without requiring a rewrite.
Why A is incorrect: The lesson explicitly frames file-based apps as targeting the small-script end of the spectrum, with growth past that point being exactly the trigger to convert, not a scenario the format is meant to absorb indefinitely.
Why B is incorrect: The whole point of the documented conversion command is that nothing has to be thrown away and rewritten — the existing code and its directives carry forward into the new project.
Why D is incorrect: .csx is associated with the older, separate scripting tools this lesson explicitly distinguished file-based apps from — it isn't the documented growth path this feature provides.
Reinforcement: Outgrowing a single file is the conversion trigger, and the conversion command is a designed-in, no-rewrite path — not an afterthought.
4. How does this lesson's synthesis distinguish file-based apps (331) from the seven feature lessons that came before it in this Part (324-330)?
Show answer
Correct: B
Why B is correct: This is exactly the distinction the Key Takeaway drew explicitly — the first seven lessons reduce friction inside code you're already writing in an existing file; file-based apps reduce a completely different kind of friction, the ceremony required before a file can run at all.
Why A is incorrect: The lesson explicitly ties all eight together under one throughline — C# reducing friction between intent and required ceremony — even while distinguishing what kind of friction each one addresses.
Why C is incorrect: Only user-defined compound assignment (330) is specifically performance-motivated among the seven; the others (like null-conditional assignment or extension members) are about expressiveness and boilerplate, not performance — and file-based apps are explicitly connected to the Part's overall friction-reduction theme, not disconnected from it.
Why D is incorrect: The lesson is explicit that file-based apps are a different kind of change, not merely one more syntax feature alongside the other seven.
Reinforcement: Same underlying value (removing unnecessary friction), applied to two different targets: what's inside the file, versus what has to exist before the file can run.
5. Why must #: directives appear at the very top of a file-based app's .cs file, before any using directive or other code?
Show answer
Correct: B
Why B is correct: Under the Hood explained this directly — the SDK's tooling scans for and processes #: directives first, translating them into project settings, before the remainder of the file is compiled as ordinary C#; that ordering is exactly why they need to be found up front.
Why A is incorrect: Ordinary C# directives like using have their own, less strict placement rules in conventional projects — this specific top-of-file requirement is particular to #: directives in file-based apps.
Why C is incorrect: #: directives are a real, functional part of the file-based app tooling, not XML documentation comments.
Why D is incorrect: Common Mistake 2 explicitly warned against misplacing a #: directive — placement does matter, and a misplaced one is treated as ordinary code or a comment instead of being parsed as a directive.
Reinforcement: The directives have to be found before the rest of the file is compiled — which is exactly why their position at the top is a real, enforced requirement, not a stylistic suggestion.
That closes Part XII — Modern C# 14. You've now toured every headline addition the language shipped alongside .NET 10: extension members, field-backed properties, null-conditional assignment, enhanced span conversions, lambda parameter modifiers, partial constructors, user-defined compound assignment, and file-based apps. Eight lessons, one throughline — C# keeps making the gap between what you mean and what you have to type smaller, whether that's inside a method body or at the very first moment you try to run a file at all.