skipnothing/.NET Platform

.NET Platform

CLR, JIT, GC, DI, configuration, and the NuGet/MSBuild supply chain that decides which DLLs the compiler ever sees.

You've written C#, classes, methods, and dotnet CLI basics.

Loading…
01

The .NET Runtime

How your code actually runs

5/5
#1

What is .NET

Read `dotnet --info` and decode every version number to the layer it reports on, pick SDK or runtime-only for a given machine role, and place a .NET version on the LTS/STS support timeline.

Run `dotnet --info` on a dev machine and you get three sections back: a toolchain version, an executor version, and a list of executors installed. The same word, '.NET', is naming all three at once.

10 min
#2

The CLR & Intermediate Language

Trace a method through both translations, C# to IL at build and IL to native at run, read its opcodes off the evaluation stack, find the method names in a .dll's metadata tables, and see why C# emits callvirt for a non-virtual call.

Run `dotnet build` on a one-line method and you get a `.dll`. Open it and the C# is gone, replaced by something that is neither your source nor your processor's machine code, a third language in between.

14 min
#3

JIT Compilation

Follow one method from its load-time stub to native code, watch a hot method get recompiled from Tier 0 to an optimized Tier 1 mid-run, and pin first-request latency on the JIT compiling every method in the call chain on first hit.

Call `Greet()` a thousand times and it runs in microseconds, but the very first call is measurably slower. The native code for it did not exist yet, and something had to build it in that gap.

12 min
#4

Native AOT

Publish one program two ways to see the native build drop the JIT and the IL, trace why a reflection call the trimmer cannot follow crashes at run time, prove a type sealed because nothing can subclass it, and place a workload where startup or steady-state decides between Native AOT, ReadyToRun, and the JIT.

Run `dotnet publish -r linux-x64` two ways on one console app and the output folders disagree: one drops a single file you can execute, the other a `MyApp.dll` that still needs translating into machine code as it runs.

12 min
#5

Assemblies & NuGet

Resolve a Version="8.0.0" constraint to the build NuGet actually picks under each of the four rules, locate the resolved graph in obj/project.assets.json instead of the .csproj, and separate a package's id-plus-SemVer from the four-part assembly identity the CLR loads by.

Type `Version="8.0.0"` into a project file and it reads as an exact order. It isn't: that number is a floor, and a solver walks every dependency you never typed to pick the versions that actually ship.

12 min
02

Memory & the Type System

Stack, heap, GC, and the type machinery

6/6
#6

Stack vs Heap

Predict whether a variable's bytes land on the stack or the heap from its lifetime, not its type, and trace why heap residency is what costs you at collection time.

Declare `struct Point` as a local and it sits in one place; make it a field of a class and the identical bytes sit somewhere else entirely. The type never changed, only its address did.

12 min
#7

Value Types vs Reference Types

Trace what a single `=` copies for a struct versus a class, a whole value or just a shared handle, and predict the aliasing, boxing, and default-equality behavior that follows from that one keyword.

Write `b = a`, then `b.X = 9`. Whether `a.X` reads 9 or stays untouched comes down to one keyword you picked pages earlier, `struct` or `class`, and nothing else about those two lines tells you which way it goes.

14 min
#8

The Garbage Collector

Follow the collector from the roots it marks to the survivors it promotes and compacts to new addresses, and predict how allocation rate, not a timer, sets how often it runs.

You write `new Order()` ten thousand times in a loop and never free one of them, yet memory stays flat. Something is walking your objects, deciding which are still in use, and reclaiming the rest behind your back.

15 min
#9

How Generics Work

Read a constructed generic's type argument back at run time, the move Java's erasure forbids, then connect it to why `List<int>` never boxes and why `Counter<int>` and `Counter<string>` keep separate statics.

Put `int` between the angle brackets of `List<int>` and that choice does not evaporate when you hit Build: you can ask the running program what it was and get `System.Int32` back, something Java cannot do.

14 min
#10

Span<T> & Memory<T>

Slice a string or array into a zero-copy view instead of allocating a fresh copy, and choose Span<T> for synchronous hot paths but Memory<T> the moment a buffer must survive an await.

Call `text.Substring(16)` inside a parsing loop and every iteration quietly allocates a brand-new string on the heap and copies characters into it, yet there is a way to read those exact same characters with zero new allocations at all.

12 min
#11

Nullable Reference Types

Separate the `?` that is an erased compiler note on `string` from the `?` that builds a real `Nullable<int>` struct, and read nullable warnings as compile-time intent the build can ship past, not a runtime guarantee.

Append `?` to `int` and the compiler builds a whole new struct, `Nullable<int>`, carrying its own `HasValue` flag. Append the same `?` to `string` and it builds nothing new: the type is identical and only which warnings you get changes.

10 min
03

Dependency Injection

The pattern that wires everything together

6/6
#12

Why Dependency Injection

Trace one hard-coded `new` out of a constructor and watch it unlock testing, swapping, and the dependency inversion principle, then meet the assembler .NET builds in.

A constructor that calls `new SmtpEmailSender()` has quietly chosen, for every caller and every test, the one and only way this class will ever send mail. Delete that single line and the class stops deciding.

12 min
#13

The .NET DI Container

Register services into an IServiceCollection, build it into a provider, and watch one GetRequiredService call walk the constructor graph and assemble the whole object tree, failing loud when a registration is missing.

Wiring `new OrderService(new SmtpEmailSender(new SmtpConfig(...)))` by hand at every call site is the chore .NET will take over. Write down which concrete class fills which request, ask for the top of the graph, and the whole wired object comes back.

14 min
#14

Service Lifetimes

Pick Transient, Scoped, or Singleton from how a service holds state, predict how many instances exist across requests, and catch the captive dependency that scope validation only flags in Development.

You change `AddScoped` to `AddSingleton` to stop rebuilding the same object, the app still runs, and three requests later one user sees another user's data. One word on a registration decided that.

15 min
#15

Registration Patterns

Hand the container a factory delegate, cover every closed type with one open generic line, and reach for TryAdd and TryAddEnumerable so a library's default yields to the app, each just a different shape of descriptor row.

Every service in your app entered through one line: `services.AddScoped<IOrderRepository, OrderRepository>()`. That line has four other shapes, and picking the wrong one is how a library's default silently wins over yours, or never does.

12 min
#16

Keyed Services

Tag each registration of one interface with a key, then resolve the one you mean with FromKeyedServices or GetRequiredKeyedService, and watch keyed and keyless rows ignore each other's lookups.

Register `EmailMessageWriter` and `SmsMessageWriter` both as `IMessageWriter`, then ask for one, and the container hands back whichever you registered last. There was no built-in way to say which one you meant, until one extra field on the row.

10 min
#17

DI Anti-Patterns

Spot four DI traps that compile and run: a constructor hiding its real dependency behind IServiceProvider, a singleton freezing a per-request service, a parameter list outgrowing its class, and a BuildServiceProvider call minting a second set of singletons, then fix each.

A class takes one constructor parameter, `IServiceProvider`, and compiles without complaint. Nothing in its signature admits it needs an email sender, so when that registration goes missing the failure lands on a customer's order, not your build.

14 min
04

Configuration & Options

Strongly-typed config done right

5/5
#18

The Configuration System

Trace one config key through a stack of providers, watch the last provider added overwrite the rest, and see why an environment variable silently beats committed appsettings.json while `__` replaces the colon in env-var names.

Set `ConnectionStrings:Default` in appsettings.json, then export that same setting as an environment variable. The exported value silently wins, even though you never reopened the JSON file and never told anything which source to prefer.

12 min
#19

The Options Pattern

Bind a config section onto a typed class, then choose between IOptions, IOptionsSnapshot, and IOptionsMonitor by how fresh the value must be, and see why a scoped snapshot cannot live inside a singleton.

An `appsettings.json` section stores `MaxRetries: 3` as text, but a C# class wants it as a real `int`. One line pours those keys into typed properties, and how you read them back has three answers.

14 min
#20

Options Validation

Attach validation rules to bound options with data annotations or a cross-field check, move the failure from the first request to startup, and read every problem from one OptionsValidationException.

A `MaxPoolSize` of 5000 sits in appsettings.json, binds cleanly into a typed object, and crashes nothing at startup. The app runs for hours on the bad value, then throws on the one request that finally reads it.

10 min
#21

Environments & Feature Flags

Flip `ASPNETCORE_ENVIRONMENT` to watch a second appsettings file layer on and `IsDevelopment()` branches switch, then contrast that frozen deploy-time string with feature flags read per request through `IsEnabledAsync`, toggled by percentage or time-window filters without a redeploy.

`Hosting environment: Production` prints in the startup log, even in an app you never configured. That one string decides which settings file loads on top of the base and which branch of your code runs.

10 min
#22

Secrets Management

Move a database password out of committed appsettings.json into a plaintext User Secrets file the repo never sees, then watch the same config["Db:Password"] read null in Production until a startup-loaded Key Vault or Secrets Manager provider supplies it, ordered by the same last-writer-wins stack.

A database password sitting in `appsettings.json` is a plaintext string your next `git commit` ships to everyone with repo access. The same value, read the same way, can instead live somewhere version control never sees.

10 min
05

Build & Distribution

Restore, metaproj, source mapping: the supply chain

4/4
#23

Restore Pipeline

Split a single `dotnet build` into its restore-then-compile halves, follow the two restore phases (an MSBuild collection pass running CollectPackageReferences into the dgspec, then a NuGet resolver writing obj/project.assets.json), and show why deleting obj/ still restores fast while a matching dgspec hash skips restore entirely.

Run `dotnet build` and the compiler is the second thing that happens, not the first. Before one line compiles, a separate step has already walked your packages, picked exact builds, and written them to disk.

14 min
#24

MSBuild Targets & Hooks

Trace which MSBuild targets fire under project restore versus solution restore, watch a BeforeTargets="Restore" hook silently no-op when a solution synthesizes its .sln.metaproj, and bind restore-time logic to CollectPackageReferences so it fires on every entry point.

Add one line to your build to pin a package version and run `dotnet restore` on the project: it works. Run the same command on the solution and it quietly does nothing, no error, no pin.

16 min
#25

Asset Flow & References

Map an `IncludeAssets`/`ExcludeAssets`/`PrivateAssets` combination to which asset streams your build consumes versus which flow to a downstream consumer, and decide when a build-time-only package (protoc tools, analyzers, source generators) earns `PrivateAssets=all`.

You add one `<PackageReference>` line and a package's whole `lib/`, `build/`, and `analyzers/` folder wires itself into your project. Some of that should reach whoever consumes your library later, most of it should not.

12 min
#26

Sources, Feeds & Mapping

Race two feeds for the same package id and watch order get ignored, compose a repo nuget.config whose <clear /> stops it inheriting machine-level sources, and pin each id to a feed with packageSourceMapping so a public impostor of a private package is never queried.

You list two servers in a config file, run `dotnet restore`, and whichever answers first hands over the package. Swap their order in the file and the result can change on its own, because the order was never a ranking.

14 min
View full 26-topic curriculum