Projects, NuGet & Tooling — From Files to a Real Solution
So far you've been writing C# the way you learn it: a few .cs files, one Main, run it, see output. That's the language. But the moment you build something real — anything with more than a handful of files, or that pulls in someone else's code, or that you actually need to ship — you step out of the language and into the toolchain: how C# code is grouped, named, packaged, and turned into something you can hand to a server or a teammate.
Here's the mental model for this whole phase, and it's worth holding onto: a C# project is not a folder of files — it's a .csproj file that describes a folder of files. That little XML file is the source of truth. It says which version of .NET you target, which outside packages you depend on, and what kind of thing you're building (an app? a library?). Once you see the .csproj as the center of gravity, the dotnet commands, NuGet, and the IDEs all click into place around it. Everything below is a tour of that world.
Namespaces & using — giving your types an address
📝 Namespace. A namespace is a named container for your types — a way to group related classes, structs, and enums under one label so their full names don't collide. string lives in System; a JsonSerializer lives in System.Text.Json. Think of it as a folder for type names, or a postal address: System.Text.Json.JsonSerializer is the full address, and the namespace is everything before the last dot.
You declare one with namespace, and you import one with using so you can refer to its types by their short names instead of typing the full address every time.
namespace MyApp.Billing; // everything in this file lives here
public class Invoice
{
public decimal Total { get; set; }
}
// In another file:
using MyApp.Billing; // import the namespace...
using System;
Invoice inv = new() { Total = 42.50m }; // ...so we can say "Invoice", not "MyApp.Billing.Invoice"
Console.WriteLine(inv.Total); // "Console" comes from the "using System;"
What just happened: The first file put Invoice at the address MyApp.Billing.Invoice. The second file said using MyApp.Billing;, which tells the compiler "when I write a bare type name, also look in there" — so Invoice resolves without the full prefix. Without that using, you'd have to write MyApp.Billing.Invoice every single time. The same goes for using System;: it's why Console works without writing System.Console. (That ; after the namespace name is the modern file-scoped form — one namespace for the whole file. You'll also see the older namespace MyApp.Billing { ... } block style with braces; they mean the same thing.)
💡 Global & implicit usings — why your files look so bare. Modern C# projects (.NET 6 and later) lean on two conveniences so you stop repeating the same imports in every file. A global using is written once and applies to the entire project: global using System.Text.Json; in any file means every file can use it. Better still, implicit usings — turned on by one line in your .csproj — auto-add the handful of namespaces nearly every program needs (System, System.Collections.Generic, System.Linq, and friends). That's why a fresh C# file can call Console.WriteLine with no using at the top: the project already imported System for you behind the scenes.
Projects & solutions — the .csproj and the .sln
Now the heart of it. A loose pile of .cs files isn't a project. A project is defined by a .csproj file, and that's what dotnet actually compiles.
📝 Project (.csproj). A small XML file that is your project's definition: it names the target framework (which .NET version), lists your package dependencies, and sets the output type (executable vs. library). The .cs files in the same folder tree are pulled in automatically — you don't list them. One .csproj = one buildable unit (one app, or one library).
📝 Solution (.sln). A grouping of multiple projects so you can open and build them together. A web app, a shared library it depends on, and a test project — three .csproj files, one .sln tying them into a single thing your IDE opens. Small programs need no solution at all; you reach for one when a single project isn't enough.
Here's a complete, tiny .csproj — and notice how little is in it:
Exe
net8.0
enable
enable
What just happened: This is the entire project definition for a runnable app. Sdk="Microsoft.NET.Sdk" brings in all the default build machinery (so the file stays tiny). <OutputType>Exe</OutputType> says "build an executable" (drop this line and you get a library — a .dll meant to be referenced by other projects). <TargetFramework>net8.0</TargetFramework> pins which .NET version you're building against. <ImplicitUsings>enable</ImplicitUsings> is the switch from the last section that auto-imports common namespaces, and <Nullable>enable</Nullable> turns on the nullable-reference warnings you'll meet in Phase 9. There's no list of source files anywhere — every .cs under this folder is included by convention.
You don't hand-write these from scratch. The dotnet CLI scaffolds, builds, and runs them:
What just happened: dotnet new console generated a folder with a ready-to-go .csproj and a starter Program.cs. dotnet run is your fast inner loop — it compiles the project and immediately runs the result, the equivalent of "F5" from the command line. dotnet build compiles but stops there, leaving the output on disk. (dotnet new has many templates — classlib for a library, web for a web app, xunit for a test project — each scaffolds the right .csproj for that kind of thing.)
NuGet — the package manager you'll lean on constantly
You will not write everything yourself, and you shouldn't try. The .NET world has a vast catalog of ready-made libraries, and NuGet is how you get them.
📝 NuGet. The package manager for .NET. A package is a bundle of compiled, reusable code (plus metadata) published to a registry — the public one is nuget.org. You add a package to your project, and from then on your code can use it. It's the .NET equivalent of npm for JavaScript or pip for Python.
Adding one is a single command:
info : Adding PackageReference for package 'Newtonsoft.Json' into project '...HelloApp.csproj'.
info : Restoring packages for ...HelloApp.csproj...
info : Installed Newtonsoft.Json 13.0.3 from https://api.nuget.org/v3/index.json
What just happened: dotnet add package looked up Newtonsoft.Json (a hugely popular JSON library) on nuget.org, picked the latest stable version, downloaded it, and — this is the key part — recorded the dependency in your .csproj. You didn't manually copy any files. The next time you build, .NET sees the recorded dependency and restores it automatically (downloads it if it's not already cached locally), so a teammate who clones your repo just runs dotnet build and the package shows up for them too.
That recorded dependency looks like this inside the .csproj:
What just happened: A <PackageReference> is the line that says "this project depends on this package at this version." It's all dotnet add package did — edit this one line into the file. Because the dependency lives in version-controlled XML (not in a folder of copied binaries), your repo stays small and the exact versions are reproducible: anyone can rebuild the identical set of dependencies from the .csproj alone.
💡 Reach for a package before rolling your own. JSON parsing, HTTP clients with retries, date/time handling, CSV, PDFs, database access — the odds that someone has already built and battle-tested what you need are very high. Before you write a tricky utility from scratch, search nuget.org. A mature package has handled the edge cases you haven't thought of yet, and "I added a well-maintained package" is almost always the better engineering call than "I wrote my own and now I maintain it forever."
Build & publish — compile vs. ship
dotnet build and dotnet publish sound similar and are easy to mix up. The difference is who the output is for.
📝 Build vs. publish. dotnet build compiles your code into binaries for you to run and debug locally. dotnet publish produces a deployable bundle — everything needed to run on another machine, arranged for copying to a server or into a container.
What just happened: dotnet build produced the binaries under a bin/ folder, ready to run on your dev machine where the .NET runtime is already installed. dotnet publish -c Release did a Release (optimized, not debug) compile and gathered the result into ./out — a folder you can ship. Publish comes in two flavors worth knowing by name: framework-dependent (smaller output, but the target machine must have the matching .NET runtime installed) and self-contained (larger, but bundles the runtime in too, so the target needs nothing pre-installed). Self-contained is what makes "copy this folder to a bare server and run it" possible.
⚠️ The bin/ and obj/ folders — generated, never committed. Every build creates two folders: obj/ holds intermediate junk the compiler uses mid-build, and bin/ holds the final binaries. Both are generated output — they're rebuilt from your source any time, so they don't belong in version control. The default .gitignore for a .NET project already excludes them; if you ever see bin/ or obj/ showing up in git status, your ignore file is missing or wrong. Committing them is a classic beginner mistake that bloats the repo and causes merge conflicts over files nobody edits.
The wider toolchain — the batteries that come with C#
The dotnet CLI is the engine, but you'll spend your days inside richer tools built around it. A quick map so nothing surprises you:
- IDEs & editors. Visual Studio (Windows/Mac, the full-featured heavyweight), VS Code with the C# Dev Kit extension (lightweight, cross-platform, hugely popular), and JetBrains Rider (cross-platform, beloved for its refactoring tools). All three give you IntelliSense, a visual debugger, and one-click run/test — and underneath, they're all driving the same
dotnetbuild you just learned. dotnet format. The built-in code formatter. Like Go'sgofmtor Python'sblack, it enforces consistent whitespace and style so code review stops being about indentation. Run it before you commit, or wire it into your editor to run on save.- Analyzers & Roslyn. C#'s compiler is called Roslyn, and it exposes its understanding of your code to analyzers — plugins that flag bugs, style violations, and risky patterns as you type, right in the editor. Many ship with the SDK and light up automatically; teams add more for their own rules. This is the "compiles-but-probably-wrong" safety net.
- Testing & profiling. Unit testing (with frameworks like xUnit) and performance profiling are first-class in this ecosystem, but they're a big enough topic that they get their own treatment later — see Phase 16. For now, just know
dotnet testexists and runs them.
💡 Key point. C# has one of the most batteries-included, mature toolchains in software. The build tool, package manager, formatter, analyzers, and test runner are all official, all integrated, and all driven by the same dotnet CLI. You're not assembling a toolchain from third-party parts and hoping they cooperate — it ships as a coherent whole, which is a real and underrated reason teams pick .NET.
Recap
- Namespaces give types an address (
namespace MyApp;) andusingimports them so you can use short names; global/implicit usings mean modern files often need no imports at all. - A
.csprojis the project — a small XML file naming the target framework, output type, and<PackageReference>dependencies; the.csfiles come in by convention. A.slngroups multiple projects. dotnet new/build/runscaffold, compile, and run; NuGet (dotnet add package) pulls in outside libraries from nuget.org, recording them in the.csprojso they restore automatically.dotnet buildcompiles for local use;dotnet publishproduces a deployable bundle (framework-dependent or self-contained). The generatedbin/andobj/folders never get committed.- The wider toolchain — Visual Studio / VS Code + C# Dev Kit / Rider,
dotnet format, Roslyn analyzers, and the testing/profiling tools coming in Phase 16 — is mature, official, and integrated around the samedotnetCLI.
You can now organize, package, and ship real C# — not just run loose files. Next we cover the idioms and gotchas that separate "compiles" from "looks like C# a pro would write."
Quick check
Make sure the core mental model — the .csproj as the center of a project — actually stuck:
[
{
"q": "What does a `.csproj` file actually define?",
"choices": [
"The project itself: its target framework, output type, and package dependencies — the .cs files are included by convention",
"A list of every source file in the project, which you must keep up to date by hand",
"Only the compiled output binaries, regenerated on each build",
"The solution that groups several projects together"
],
"answer": 0,
"explain": "The `.csproj` is the project's definition — target framework, output type, and `<PackageReference>` dependencies. The `.cs` files under its folder are pulled in automatically, so you don't list them. Grouping multiple projects is the job of a `.sln`."
},
{
"q": "After you run `dotnet add package Newtonsoft.Json`, how does the package end up available to a teammate who clones your repo?",
"choices": [
"The command records a `<PackageReference>` in the .csproj, and `dotnet build` restores the package automatically from it",
"The package's binaries are copied into the repo and committed alongside your code",
"Your teammate must manually download the package from nuget.org and place it in bin/",
"The package is embedded directly into every .cs file that uses it"
],
"answer": 0,
"explain": "`dotnet add package` edits a `<PackageReference>` line into the `.csproj`. Because the dependency lives in version-controlled XML, a fresh clone just needs `dotnet build`, which restores (downloads) the recorded packages automatically — no binaries committed to the repo."
},
{
"q": "What's the difference between `dotnet build` and `dotnet publish`?",
"choices": [
"`build` compiles binaries for you to run locally; `publish` produces a deployable bundle (optionally self-contained) for another machine",
"`build` is for libraries and `publish` is for executables",
"They are identical; `publish` is just the older name for `build`",
"`build` downloads NuGet packages while `publish` removes them"
],
"answer": 0,
"explain": "`dotnet build` compiles binaries for local running and debugging. `dotnet publish` gathers everything needed to deploy elsewhere — framework-dependent (needs the runtime installed) or self-contained (bundles the runtime so the target needs nothing)."
}
]
← Phase 7: Errors & I/O · Guide overview · Phase 9: Idioms & Gotchas →
Check your understanding
1. What does a `.csproj` file actually define?
2. After you run `dotnet add package Newtonsoft.Json`, how does the package end up available to a teammate who clones your repo?
3. What's the difference between `dotnet build` and `dotnet publish`?