The .NET platform and C#
One language, two job markets — modern cross-platform .NET running on Linux and in containers, and .NET Framework keeping Windows-only systems alive that still fund a great deal of the work. Find out which one a role means before you prepare for it.
Assumes you know: Object-oriented fundamentals: classes, interfaces, inheritance, Comfort with a statically typed compiled language, SQL, and enough HTTP to describe a REST endpoint, Basic command line, on Windows or Linux
Overview
What this area actually covers
C# is a statically typed, garbage-collected, object-oriented language that compiles to an intermediate language executed by the CLR, the Common Language Runtime. The CLR provides just-in-time compilation, generational garbage collection, type safety and reflection. Around it sits a very large first-party library — collections, networking, cryptography, serialisation, dependency injection, logging — which is why .NET codebases tend to reach for fewer third-party packages than their JavaScript or Python equivalents.
The framework layer splits by workload. ASP.NET Core serves HTTP, from minimal APIs up through MVC and Razor Pages, with a middleware pipeline and built-in dependency injection. Entity Framework Core is the default ORM, and its translation behaviour is a reliable interview topic in its own right. Blazor runs C# in the browser or on the server for interactive UI. WPF and WinForms build Windows desktop applications and remain very much in production. MAUI targets mobile and desktop. Unity uses C# as its scripting language, which makes game development a distinct market that happens to share the language.
The critical distinction is not framework but runtime. .NET Framework is the original Windows-only implementation, at version 4.8, shipped as a component of Windows, in security-fix maintenance and receiving no new features. Modern .NET is the rebuilt cross-platform implementation — .NET Core, then .NET 5 onwards, now released every November with even-numbered versions carrying long-term support. The two share a language and much of an API surface, and are otherwise different platforms with different tooling, different deployment stories and different interview questions.
One more separation is worth making, because it explains why .NET conversations can be confusing. The language, the runtime and the libraries version independently. C# has its own version numbers and its own release notes; the runtime has others; the base class library moves with the runtime but individual packages ship on their own schedules. A team can be on a current C# compiler targeting an older runtime, or on a current runtime writing thoroughly dated C#. When someone says "we're on .NET 8", they have told you about the runtime and nothing about the code.
What people wrongly bundle in is Microsoft's product estate. Dynamics customisation, SharePoint development, Power Platform work and SQL Server administration all frequently appear alongside .NET on a job advert, and none of them is the same skill as writing C#.
The five areas underneath
The five subsections divide neatly into two language-and-runtime areas, one concurrency area, and two framework areas. That ordering is also a reasonable learning order, with one exception: if you are interviewing next week for a backend role, do async and EF Core first, because those two produce more interview questions than the other three combined.
| Subsection | What it is for |
|---|---|
| C# Language | The type system, LINQ, records and nullability |
| Async, Tasks & Threading | The task model and how it deadlocks |
| CLR & Memory | Where objects live and what that costs |
| ASP.NET Core | The pipeline, DI, configuration and hosting |
| Entity Framework Core | Change tracking, translation and query performance |
C# Language
The language subsection covers value types versus reference types and where each
lives, generics and variance, LINQ and how a query is evaluated, records and
with expressions, pattern matching, and nullable reference types. It exists
separately because C# is a large language that has grown quickly, and because
several of its distinctive features have no equivalent in the languages people
arrive from. Value types you define yourself, real generics that keep their type
arguments at runtime rather than erasing them, and expression trees that let LINQ be
translated to SQL instead of merely executed are all things that make C# answers
sound wrong when given in Java's semantics. Expect questions here about copying
behaviour, about deferred evaluation, and about what nullable reference types do and
do not promise.
Async, Tasks & Threading
The task model, async and await and the state machine the compiler generates from
them, synchronisation context and the deadlocks it can cause, cancellation tokens,
ValueTask, and the choice between asynchronous I/O and genuine parallelism. This is
its own area because C# adopted async/await early and built it deeply into the
platform, so nearly all modern .NET code is asynchronous and nearly all of the
characteristic production failures are asynchrony misused. The questions are about
consequences rather than syntax: what blocking on a task actually does, why
async void cannot be awaited or caught, and why adding await to something did not
make it faster.
CLR & Memory
Generational garbage collection, the large object heap, boxing and its cost, Span<T>
and stack allocation, and diagnosing memory pressure in a running process. It is
separate because it is the layer that decides whether your service is efficient, and
because .NET gives you unusually fine control over allocation for a managed platform —
you can define a type that lives on the stack, slice memory without copying it, and
pool buffers rather than allocating them. Most application code needs none of this.
Every hot path needs some of it, and senior interviews go here to find out whether
you know which is which.
ASP.NET Core
The middleware pipeline and why ordering matters, dependency-injection lifetimes and the captive-dependency problem, the configuration and options system, minimal APIs versus controllers, routing, and hosting including background services. This is the framework subsection for server work, and it exists separately because ASP.NET Core is an opinionated composition of small pieces rather than a monolith: understanding it means understanding that a request passes through an ordered list of components you assembled, each of which may short-circuit it. Nearly every ASP.NET Core bug that looks mysterious is either ordering or a lifetime mismatch.
Entity Framework Core
Change tracking and the identity map, query translation and what happens when translation fails, loading strategies and the N+1 problem, migrations, and performance tuning. EF Core earns its own area for the same reason JPA does in the Java world: it is where the money goes. It is a genuinely good ORM that will do something catastrophic if you write LINQ it cannot translate, and the boundary between what runs in the database and what runs in your process is invisible on the page. You will find questions here that are really questions about SQL, asked through an abstraction.
The vocabulary you will hit on day one
This ecosystem is unusually dense with initialisms, and several of them refer to things a newcomer will otherwise conflate. The CLR is the runtime that executes your code: it loads assemblies, compiles intermediate language to machine code, enforces type safety and runs the garbage collector. The BCL, or base class library, is the first-party set of types that come with it, and its breadth is a large part of why .NET projects carry fewer dependencies than comparable projects elsewhere. IL is the intermediate language your C# compiles to, which is why a decompiler can show you readable C# from a shipped assembly and why obfuscation is a product category here.
An assembly is the unit of deployment and versioning, usually a .dll file
containing IL and metadata. A NuGet package is how assemblies are distributed,
and a package can contain several assemblies targeting different platforms. The
target framework moniker, the string like net8.0 in a project file, declares
which platform surface you are compiling against — and getting it wrong is the usual
cause of a package that installs happily and then fails to load. It is worth
internalising early that "which .NET do I have" has two answers: the SDK is what
builds code, the runtime is what executes it, and a machine can perfectly well
have one without the other.
Two release words matter for planning rather than coding. LTS releases are supported for three years and STS releases for eighteen months, which is why enterprises tend to sit on the even-numbered versions and why a role advertising an odd-numbered one is telling you something about how the team works. And AOT, ahead-of-time compilation, produces a native executable with no just-in-time step at startup, trading some reflection-dependent functionality for faster start and smaller memory use — a trade that matters for serverless and command-line tools and rarely for a long-running API.
Where it sits in a real system
.NET occupies the same architectural position as Java in the enterprise, and for
similar reasons: a strong static type system, a mature runtime, first-class
tooling, and a vendor a procurement department already has a contract with. A
typical system is an ASP.NET Core API behind a load balancer, EF Core over SQL
Server or Postgres, Redis for caching, a message broker such as Azure Service Bus or
RabbitMQ, and a background worker hosted as a BackgroundService.
The part outsiders underestimate is how much of the work is integration rather than greenfield. .NET is where line-of-business systems live: order management, claims processing, billing, scheduling, regulatory reporting. So the code sits between a database that predates it, a SOAP service nobody will decommission, a file drop from a partner, and a reporting layer that reads the tables directly. Understanding transactions, idempotency and how a nightly batch interacts with a live API is more of the job than choosing a design pattern.
Where the runtime split shows up physically is deployment. Modern .NET publishes a self-contained or framework-dependent artefact that runs in a Linux container, so it sits in Kubernetes alongside everything else. .NET Framework needs Windows and, very often, IIS — which is why "we cannot containerise that service cheaply" is a sentence that shapes whole roadmaps.
| .NET Framework 4.8 | Modern .NET (.NET 8 and later) | |
|---|---|---|
| Platforms | Windows only | Windows, Linux, macOS |
| Release model | Windows component, features frozen | Annual November release, even versions are LTS |
| Web stack | ASP.NET MVC 5, Web Forms, WCF | ASP.NET Core, minimal APIs, gRPC |
| Deployment | IIS on a Windows host | Container, systemd service, IIS, or serverless |
| Typical work | Maintenance, incremental modernisation | New services, cloud-native systems |
How a request moves through ASP.NET Core
The single most useful mental model in this ecosystem is the middleware pipeline,
because it explains a whole class of bugs that otherwise look inexplicable. A request
enters a chain of components you assembled in Program.cs. Each one may inspect the
request, pass it to the next, inspect the response on the way back out, or
short-circuit and respond immediately. The chain is ordered, and the order is the one
you wrote.
flowchart TD
A[Request arrives] --> B[Exception handler]
B --> C[Static files]
C --> D[Routing]
D --> E[Authentication]
E --> F[Authorisation]
F --> G[Endpoint handler]
G --> H[Response unwinds back out]The gap to look at is between routing and authorisation: authorisation needs to know which endpoint was selected before it can decide whether the caller may reach it, so placing it before routing produces a policy that silently never applies. Register authentication after authorisation and you get the same class of failure from the other direction.
Dependency injection is the second half of the model, and its three lifetimes are the thing to keep straight. A singleton is created once for the application. A scoped service is created once per request. A transient service is created every time it is asked for. The failure mode that gives interviewers a reliable question is the captive dependency: inject a scoped service into a singleton and the singleton captures the first request's instance and holds it forever, which for a database context means one shared, never-reset unit of work serving every request in the process.
Who does this work
Backend engineers on .NET teams build and maintain APIs and background services,
and their hard days involve a slow EF Core query, a deadlock in SQL Server, or an
async call that blocked a thread. Full-stack engineers pair ASP.NET Core with
Angular or React — Angular is disproportionately common in this ecosystem, a
consequence of both being enterprise-default choices at the same moment. Desktop
engineers maintain and extend WPF and WinForms applications, work which is
unglamorous, well-paid and consistently short of people. Modernisation
engineers move Framework applications to modern .NET, which means untangling
HttpContext, replacing WCF, and negotiating what can be left behind. Azure
engineers sit half in .NET and half in infrastructure. Unity developers write
C# against a different runtime profile with different constraints, particularly
around allocation.
| Role | What their week looks like | Where they get stuck |
|---|---|---|
| Backend engineer | Endpoints, workers, queries | Query translation, async misuse |
| Full-stack engineer | API plus an Angular or React front end | Contract drift between the two |
| Desktop engineer | WPF or WinForms features and fixes | Threading and UI marshalling |
| Modernisation engineer | Moving Framework code forward | WCF, HttpContext, ambient state |
| Azure engineer | Services plus the infrastructure under them | Identity, networking, cost |
Two of those rows are consistently underserved by the candidate market and worth knowing about if you are choosing a direction. Desktop work is unfashionable and therefore uncrowded, while the applications themselves remain load-bearing inside manufacturing, healthcare and finance. Modernisation work is similar: it requires fluency in both halves of the platform, it is difficult to hire for, and the people who have done it successfully once are sought after precisely because the failure mode of a botched migration is so visible.
The people who specify this work — business analysts, product owners inside a regulated function — matter more here than in most ecosystems, because the systems encode business rules that only they can adjudicate. A .NET engineer who can read a requirements document and argue with it is unusually valuable.
Demand, adoption and how that is changing
Demand is high and unusually stable, because it is anchored in systems that generate revenue and cannot be switched off. Banks, insurers, healthcare providers, manufacturers, logistics operators, government departments and retailers all run substantial .NET estates. That produces steady hiring rather than spikes, and it survives downturns better than fashion-driven stacks.
The structural fact to understand is that the market is bifurcated, and the two halves feel completely different from the inside. New work is on modern .NET, is often containerised, and looks like any contemporary cloud backend. Maintenance and modernisation work is on .NET Framework and looks like archaeology. Both are real, both pay, and the second is larger than public discussion suggests — much of the budget in this ecosystem is spent keeping Framework systems running while a migration proceeds slowly. This is why the single most useful question in a .NET interview is one the candidate asks: which runtime is this role on, and what proportion of the work is new versus existing? An answer of "mostly .NET Framework 4.8 with a strategy deck about .NET 10" tells you exactly what your next two years look like.
The migration itself is worth understanding even if you never do one, because it
explains why organisations are stuck. Moving from Framework to modern .NET is not a
retarget. WCF, the old remoting-style service framework, has no direct successor in
the box, so every service contract has to become something else. Web Forms has no
migration path at all. HttpContext.Current, the ambient static that a great deal of
older code reaches for from arbitrary depths of the call stack, does not exist,
which turns an invisible dependency into an explicit parameter everywhere it was
used. Anything depending on Windows-specific APIs, COM interop or the registry ties
the application to Windows regardless of runtime. None of that is unsolvable and all
of it is expensive, which is precisely why the second market persists.
Geographically the concentration follows enterprise IT rather than venture capital. .NET is strong wherever there are large established companies with internal development teams — much of North America outside the coastal technology hubs, the UK, the Nordics, the Netherlands, Germany, and the outsourcing and captive-centre markets in Central and Eastern Europe and India. It is comparatively thin in venture-backed startups, which default to TypeScript, Python or Go, and effectively absent from data science. If you want to work at a seed-stage company, this is not the fastest route; if you want to work in a stable industry in a mid-sized city, it is one of the best.
The language itself changes quickly, which surprises people. C# adopted async and
await before most mainstream languages had anything comparable, then pattern
matching, tuples, nullable reference types, records, top-level statements, primary
constructors and collection expressions in successive annual releases. The practical
consequence for interviews is that idiomatic C# from 2015 is visibly dated, and a
candidate whose code has no pattern matching, no records and no expression-bodied
members reads as someone who stopped learning.
What makes it hard
Three things, and none of them is syntax.
The first is asynchrony done properly. async and await are easy to write and
easy to misuse, and the failure mode is specific: blocking on an asynchronous call
with .Result or .Wait() can deadlock, because the continuation needs a context
that the blocked thread is holding. Getting this right means understanding
SynchronizationContext, ConfigureAwait, cancellation tokens, and why
async void is almost always a bug.
The mechanism underneath is worth knowing because it makes the failures obvious
rather than magical. The compiler rewrites an async method into a state machine:
everything before the first await runs synchronously on the calling thread, the
method then returns a task to its caller, and the remainder is registered as a
continuation to run when the awaited operation completes.
stateDiagram-v2
[*] --> Running: caller invokes
Running --> Suspended: hits await
Suspended --> Resumed: awaited task completes
Resumed --> Running: next segment
Running --> Completed: method returns
Suspended --> Deadlocked: caller blocked on Result
Completed --> [*]The transition to study is the one into Deadlocked. The continuation needs
somewhere to run; if the only place it may run is a thread that is sitting in
.Result waiting for the very task that continuation would complete, neither side
can move. ASP.NET Core removed the synchronisation context that made this common,
which is why the deadlock is now mostly a legacy-Framework and desktop problem — and
why an engineer who learned .NET after that change may never have met it.
The second is memory behaviour, which is where the ecosystem's depth shows. Value
types versus reference types, where each actually lives, when a struct gets copied,
what boxing costs, why an object over the large-object-heap threshold is treated
differently, and how Span<T> lets you slice memory without allocating. You can
write correct C# without any of this. You cannot make a hot path fast without it,
and senior interviews go straight here.
The collector's model is simple enough to state and has real consequences. Objects
are allocated in generation zero; collections there are frequent and cheap, because
most objects die young and the collector only has to trace the survivors. Anything
surviving is promoted, and generation two collections are much more expensive. Large
objects go to a separate heap that is not compacted by default, so a workload that
repeatedly allocates big arrays fragments it and grows the process even though the
live set is small. This is why buffer pooling exists, why Span<T> is valuable, and
why "reduce allocations" is more often the right advice in .NET than "make the code
faster".
The third is EF Core's boundary. It is a good ORM that will quietly do something catastrophic if you write LINQ it cannot translate, and the difference between a query executed by the database and a query executed in your process after loading the whole table is invisible on the page. Knowing where translation stops is the skill.
Change tracking is the other half of the same subject and the half people neglect. The context keeps a copy of every entity it loaded so it can work out what changed when you save, which is convenient for a handful of entities and expensive for thousands. A read-only query that loads fifty thousand rows with tracking enabled pays for a snapshot of every one of them, and disabling tracking is a single method call that most codebases discover only after profiling.
Why study it
Study .NET if you want to build serious business systems with excellent tooling and a low tolerance for mystery. The debugger, the profiler, the IDE and the standard library are genuinely first-rate, the documentation is comprehensive, and the platform is now open source and developed in public. Study it if you want stable employment in a real industry rather than in the technology industry, or if you are already in a Microsoft-shop and want to go deeper rather than sideways.
There is a coherence argument as well. Because one vendor supplies the language, the runtime, the web framework, the ORM, the dependency-injection container, the logging abstraction and the test tooling, the pieces are designed to fit and the documentation is one estate rather than a federation. That is a genuine productivity advantage and it is the opposite trade from the JavaScript ecosystem, where you assemble your own stack from competing parts. Which trade you prefer says something real about the kind of work you will enjoy.
Do not study it if your goal is data science or machine learning; the ecosystem exists but the work is not here. Do not study it if you specifically want startup-flavoured work, because you will be swimming against the current. And do not choose it as a first language purely to learn programming — it is a fine teacher, but you will absorb a lot of framework alongside the fundamentals.
The honest correction is aimed at engineers who last evaluated this ecosystem around 2015 and concluded it was proprietary, Windows-locked and slow. All three conclusions are now wrong. The runtime is open source, runs natively on Linux, has had measurable per-release performance work for years, compiles ahead of time to a native binary when you want it to, and has a first-class command-line workflow that never requires Visual Studio. Dismissing modern .NET on a decade-old impression is one of the more common blind spots in the industry.
Your first hour
Do this from a terminal, on whatever operating system you already have, precisely because the cross-platform claim is the thing worth verifying yourself.
dotnet --list-sdks # confirm which runtimes you have
dotnet new webapi -o Orders --no-https
cd Orders && dotnet run
Then replace the generated endpoint with something you wrote:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// The route constraint :int is enforced during routing, so a non-numeric
// id never reaches the handler and never needs validating inside it.
app.MapGet("/orders/{id:int}", (int id) => new { Id = id, Status = "open" });
app.Run();
Call it, then do three more things: add a class with a constructor dependency and
register it in the container so you see how injection is wired; run
dotnet publish -c Release and look at what came out; and run
dotnet build 2>&1 | head after deliberately introducing a nullability warning, so
you have seen what the compiler tells you.
If you have a little longer, register the same class three times over — once as a singleton, once as scoped, once as transient — have it record a new identifier in its constructor, and return that identifier from two different endpoints. Call each endpoint twice. Four numbers come back, and comparing which ones match tells you more about dependency-injection lifetimes than any amount of reading, because you will have watched a scoped service change between requests and a singleton refuse to.
The artefact is a running API you can curl, plus one sentence on what
WebApplication.CreateBuilder set up on your behalf. That last part is the whole
skill of working in this ecosystem — a great deal is configured for you, and
competence is knowing what.
What this is not
.NET is not Windows-only, and repeating that in 2026 dates you immediately. Modern .NET runs on Linux in production at very large scale; the Windows dependency belongs to .NET Framework and to the desktop frameworks specifically.
.NET is not closed source. The runtime, the compiler, the libraries and ASP.NET Core are developed on GitHub under the .NET Foundation, and the design discussions happen in public issues you can read.
C# is not Java with different keywords. The overlap in the first week is real and
then they diverge sharply: value types you define yourself, real generics with
runtime type information rather than erasure, LINQ and expression trees, properties,
async built into the language, ref semantics and Span<T>. Answering a C#
question with Java's semantics is a specific and recognisable error.
async is not multithreading, and conflating the two is the most common
misunderstanding in the ecosystem. Awaiting an I/O operation releases the current
thread rather than occupying a new one — the whole point is that no thread is
waiting. Making a method async does not make anything parallel, does not make it
faster, and if the body is CPU-bound it merely moves the same work somewhere else.
Parallelism is a separate decision with separate tools.
And "nullable reference types" are not null safety. They are a compile-time flow analysis that produces warnings, erased to attributes at runtime, and a deserialiser, a reflection call or a library compiled without the feature will happily hand you a null through a reference that claims it cannot be one. Knowing the difference between a warning and a guarantee is the mark of someone who has used the feature rather than merely enabled it.
The most valuable question in a .NET interview is the one you ask: which runtime, and how much of this is new code? The stack is the same word either way, and the two jobs are not.
Where to go next
Now practise it
10 interview questions in .NET & C#, each with the rubric the interviewer is scoring against.
- How does the .NET garbage collector decide what to collect, and how would you make a hot path allocate less?
- What is the difference between a value type and a reference type in C#, and what do nullable reference types guarantee?
- A client disconnects but your server keeps working on their request. How does cancellation actually propagate in .NET?
- A read-only endpoint that returns fifty thousand rows is slow and memory-heavy in EF Core. What is the context doing?