.NET for Back-End Developers
Explain how the runtime, base class library, SDK and ASP.NET Core fit together, and pick the .NET release a production API should target.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
</Project>.NET is a platform, not a language. You write C#; the Roslyn compiler turns it into IL (intermediate language) stored in an assembly (Orders.Api.dll); the .NET runtime loads that IL, compiles it to machine code with the JIT, manages memory with the garbage collector and runs it. The C# version is chosen by the compiler from the target framework (net10.0 means C# 14), so the language is a build-time choice while the runtime is what actually serves traffic.
The four pieces
| Piece | What it contains | Where it lives |
|---|---|---|
| Runtime (CLR) | JIT, garbage collector, type loader, thread pool | Shared framework Microsoft.NETCore.App |
| Base class library | System.*: collections, I/O, HttpClient, System.Text.Json, threading | Same shared framework, versioned with the runtime |
| Microsoft.Extensions | The generic host, DI, configuration, logging, options | NuGet packages (Microsoft.Extensions.Hosting and friends), also bundled in Microsoft.AspNetCore.App |
| ASP.NET Core | Kestrel, routing, minimal APIs, MVC, SignalR | Shared framework Microsoft.AspNetCore.App |
| SDK | dotnet CLI, MSBuild, compilers, NuGet, templates | Build machines and laptops only |
The default publish is framework-dependent: the output holds your assemblies, NuGet dependencies and a .runtimeconfig.json naming the shared frameworks it needs. At start-up the host finds a matching installed runtime. That is why mcr.microsoft.com/dotnet/aspnet:10.0 carries the runtime and ASP.NET Core but no SDK, and why a missing framework fails when the process starts ("You must install or update .NET to run this application"), not when you build.
Release cadence
A major version ships every November. Even numbers are LTS with 36 months of support, odd numbers are STS with 24 months. .NET 10 is the current LTS: released November 2025, supported to November 2028, currently runtime 10.0.13 and SDK 10.0.402. .NET 8 (LTS) and .NET 9 (STS) both leave support in November 2026, the same month the next release, .NET 11, ships as STS. A team running services for years targets the LTS, which today means net10.0; moving to an STS release commits you to upgrading again within two years.
Where .NET sits for web APIs
ASP.NET Core serves HTTP from Kestrel inside your process, usually behind an ingress or load balancer on Linux containers. One runtime covers REST APIs, gRPC, background workers and real-time endpoints, with first-party DI, configuration, logging and metrics, so most services need few third-party frameworks. The costs are JIT warm-up on cold start (Native AOT addresses it), a larger memory floor than very small Go services, and some popular libraries (MediatR, AutoMapper, MassTransit) having moved to commercial licences.
What goes wrong in production
- The app targets
net10.0but the image isaspnet:8.0: the container exits at start-up. - A web API on the
runtime:10.0image: it has noMicrosoft.AspNetCore.App, so the host cannot start it. - Confusing SDK and runtime versions in an incident:
dotnet --versionprints the SDK (10.0.402), not what serves requests (10.0.13).
Example
When to use it
- Decide whether a new payments API should target net10.0 (LTS, supported to November 2028) or the next STS release.
- Diagnose a container that exits at start-up because the image has the .NET 8 runtime and the app targets net10.0.
- Explain to a reviewer why the production image is aspnet:10.0 rather than sdk:10.0, and what each one contains.
More examples
The project file that pulls in ASP.NET Core
The <code>Microsoft.NET.Sdk.Web</code> SDK adds an implicit framework reference to <code>Microsoft.AspNetCore.App</code>, so Kestrel, routing, DI and configuration come from the shared framework installed in the image rather than from NuGet. There is no package to update for an ASP.NET Core security fix: patching means updating the runtime in the base image. Old <code>Microsoft.AspNetCore.*</code> 2.x packages in a project are a red flag and usually show up in vulnerability audits.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<!-- Only real NuGet dependencies are listed; ASP.NET Core itself is not a package. -->
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.13" />
</ItemGroup>
</Project>What the host reads at start-up
The version is the minimum, not the one that runs: the host rolls forward to the newest installed 10.0 patch, so the same file runs on 10.0.13 today and on next month's patch after the image is rebuilt. If no 10.0 framework is installed the host refuses to start and prints the framework name and version it looked for, which is the first thing to read when a container exits immediately.
// bin/Release/net10.0/publish/Orders.Api.runtimeconfig.json (generated by the build)
{
"runtimeOptions": {
"tfm": "net10.0",
"frameworks": [
{ "name": "Microsoft.NETCore.App", "version": "10.0.0" },
{ "name": "Microsoft.AspNetCore.App", "version": "10.0.0" }
],
"configProperties": {
"System.GC.Server": true,
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false
}
}
}The same platform without HTTP
A worker uses the same runtime, base class library and generic host as the API, but gets the host from the <code>Microsoft.Extensions.Hosting</code> NuGet package and runs on the <code>runtime</code> image without the ASP.NET Core shared framework: dependency injection, configuration, logging and graceful shutdown work identically. Only the HTTP server is missing. That is why a team can move a slow job out of a request handler into a worker with little code change, and why one container image strategy covers both.
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<InvoiceReminderWorker>();
builder.Build().Run();
sealed class InvoiceReminderWorker(ILogger<InvoiceReminderWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
var overdue = await FindOverdueInvoiceIdsAsync(stoppingToken);
logger.LogInformation("Sending reminders for {Count} overdue invoices", overdue.Count);
}
}
private static Task<List<string>> FindOverdueInvoiceIdsAsync(CancellationToken ct) =>
Task.FromResult(new List<string> { "INV-2026-0311", "INV-2026-0318" });
}
Discussion