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.
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.
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.
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.
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.
Console.WriteLine("Hello!"); saved as hello.cs — nothing else needs to exist yet..csproj was ever created.#:package Humanizer@2.14.1 or #:sdk Microsoft.NET.Sdk.Web — parsed before the rest of the file, translated into the equivalent project setting the implicit project needs..csproj whose settings mirror the #: directives the file already had — nothing about the app's own code has to be rewritten.// 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.
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.
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.
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.
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.
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.
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.
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#.
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.
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?
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?
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?
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)?
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?
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.
dotnetmadeeasy.com — Learn C# and .NET, the right way.