Home › Programming languages › Managed & backend
guide low

C#

Problem Is C# on .NET the right choice for a web API, desktop app or game logic? This guide covers where it fits, its performance characteristics, and where another language is a better pick.

Fast managed language on .NET: JIT or native AOT, value types and spans for low-allocation code, async/await, and Unity/Godot for games.

csharpdotnetaspnet-coreasyncgcunitynativeaot
Overview

C# runs on .NET (CoreCLR), cross-platform and open source. It sits between Java and C++: garbage-collected, but with structs, Span<T>, stack allocation and unsafe code when you need control. ASP.NET Core is among the fastest mainstream web stacks, and C# is the scripting language of Unity and an option in Godot.

Key facts

Use when

  • Web APIs, Windows desktop, Unity/Godot game logic, game servers sharing code with a Unity client.
  • Real uses by field: games (Unity scripts run on the Mono or IL2CPP back end), web services (ASP.NET Core), Windows desktop software (WPF and Windows Forms, per Microsoft's Desktop Guide).

Avoid when

  • You need Python's ML ecosystem, browser frontends, or tiny embedded targets.

Deep dive

Decision table

JobPickWhy
HTTP APIASP.NET Core minimal APIsFast, low ceremony
RPC between servicesgRPC for .NETFirst-class codegen, HTTP/2
Data accessEF Core for CRUD, Dapper/Npgsql for hot pathsProductivity vs control
Fast startup / small containers / serverlessNativeAOT publishNo JIT, ms startup, smaller memory
Unity gamesC# scripts (Mono or IL2CPP backend)The engine's language
Game server for a Unity client.NET 8+ server sharing a C# model libraryOne codebase for rules
Windows desktopWPF or WinUI; Avalonia/.NET MAUI cross-platformNative toolkits
High-throughput parsingSpan<T>, System.IO.Pipelines, Utf8JsonReaderZero-copy, low allocation

Memory and performance model

  • Classes on the GC heap; structs inline (in arrays, fields, stack). ref struct (e.g., Span<T>) can never escape to the heap, which makes stack and pooled buffers safe to slice.
  • ArrayPool<T>.Shared and MemoryPool<T> for reusable buffers; System.Text.Json source generators avoid reflection.
  • JIT: tier-0 quick code, then tier-1 with dynamic PGO (devirtualization, hot/cold splitting), on-stack replacement for long loops. Hardware intrinsics and Vector<T>/Vector128/256/512 for SIMD.
  • GC modes: Workstation (desktop), Server (per-core heaps, throughput; adapts its heap count automatically since .NET 9). GCHeapHardLimit or container limits bound memory.

Concurrency model

  • Task/ValueTask and async/await: the compiler builds state machines; I/O completes on the thread pool (IOCP on Windows, epoll/kqueue elsewhere).
  • System.Threading.Channels for producer/consumer pipelines with backpressure; Parallel.ForEachAsync, PLINQ for CPU fan-out.
  • lock, SemaphoreSlim, Interlocked; the .NET memory model is stronger than ECMA's minimum on x64 but code should use proper primitives.

Code

ASP.NET Core (.NET 8+), minimal API, bounded background queue, span-based parsing. dotnet new web, replace Program.cs:

using System.Threading.Channels;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpClient(); // pooled handlers: no socket exhaustion
builder.Services.AddSingleton(Channel.CreateBounded<string>(
    new BoundedChannelOptions(1024) { FullMode = BoundedChannelFullMode.Wait }));
builder.Services.AddHostedService<Worker>();
var app = builder.Build();

app.MapPost("/jobs/{id}", (string id, Channel<string> q) =>
    q.Writer.TryWrite(id) ? Results.Accepted() : Results.StatusCode(503)); // shed load when full
app.MapGet("/sum", (string csv) => Results.Ok(Parsing.SumCsv(csv)));
app.Run();

static class Parsing
{
    // Walks the input with spans: no Split(), no substring allocations.
    public static long SumCsv(ReadOnlySpan<char> s)
    {
        long total = 0;
        while (!s.IsEmpty)
        {
            int i = s.IndexOf(',');
            var part = i < 0 ? s : s[..i];
            if (long.TryParse(part, out var v)) total += v;
            s = i < 0 ? ReadOnlySpan<char>.Empty : s[(i + 1)..];
        }
        return total;
    }
}

sealed class Worker(Channel<string> queue, ILogger<Worker> log) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        await foreach (var id in queue.Reader.ReadAllAsync(ct))
        {
            log.LogInformation("processing {Id}", id);
            await Task.Delay(50, ct); // real async work here
        }
    }
}

NativeAOT publish (.csproj):

<PropertyGroup>
  <TargetFramework>net8.0</TargetFramework>
  <PublishAot>true</PublishAot>
  <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>
dotnet publish -c Release -r linux-x64   # single native binary in bin/Release/net8.0/linux-x64/publish

For AOT, use WebApplication.CreateSlimBuilder and System.Text.Json source-generated contexts; reflection-heavy libraries may not work.

Efficiency

  • Cost: tiered JIT starts with unoptimized tier-0 code and re-JITs hot methods using dynamic PGO, on by default since .NET 8. In one Toub microbenchmark (Intel Xeon Platinum 8370C) a list-counting loop took 15.8 µs on .NET 7 and 6.1 µs on .NET 8 (7.1 µs without PGO). Objects of 85,000 bytes or more go to the large object heap (Microsoft docs). Native AOT apps have faster startup and smaller memory (Microsoft docs). Bound: allocation rate and GC pauses; JIT warmup at startup.
  • Levers:
    • Span<T>, stackalloc, ArrayPool<T> — fewer heap allocations and GCs (.NET performance posts) — parsing and buffer-heavy hot paths
    • Native AOT publish — faster startup, smaller memory footprint (Microsoft docs) — serverless, small containers, CLIs
    • dynamic PGO (default since .NET 8) — devirtualization and hot/cold layout; about 15 % on the microbenchmark above (7.1 to 6.1 µs, Toub) — long-running services, no change needed
    • avoid large arrays above 85,000 bytes per request — keeps them off the large object heap (Microsoft docs) — buffer-heavy services; pool them
    • Vector128/256/512 intrinsics — SIMD (gain up to the vector width, order of) — numeric kernels
  • Naive → standard → state of the art:
    • allocating strings and arrays per request · GC gen-0 churn
    • pooled buffers, Span<T>, source-generated JSON · low-allocation steady state
    • Native AOT for startup-bound hosts, dynamic PGO for throughput-bound hosts · each picked by workload

Build and deploy

  • dotnet CLI, NuGet with lock files (RestorePackagesWithLockFile), Central Package Management for many projects.
  • Containers: mcr.microsoft.com/dotnet/aspnet (JIT) or runtime-deps/chiseled images for AOT; dotnet publish /t:PublishContainer builds images without a Dockerfile.
  • Diagnostics: dotnet-counters, dotnet-trace, dotnet-dump, BenchmarkDotNet for microbenchmarks.

Pitfalls

  • Sync-over-async (.Result, .Wait()) causing thread-pool starvation or deadlocks; async all the way.
  • async void outside event handlers: exceptions crash the process.
  • new HttpClient() per request: socket exhaustion; use IHttpClientFactory or one shared instance.
  • Allocating large arrays repeatedly: LOH fragmentation and gen-2 GCs; pool them.
  • Boxing structs via interfaces or object; LINQ in hot loops.
  • Captured DbContext across threads (not thread-safe); EF Core N+1 queries via lazy loading.
  • Unity: allocations per frame cause GC spikes; the Unity GC differs from CoreCLR's.
  • NativeAOT surprises: trimming removes reflection targets; test the published binary, not dotnet run.

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
.NET runtime + SDKC# / C++dotnet/runtimebrew install dotnet (or the installer from dotnet.microsoft.com)————CoreCLR, JIT, GC, Native AOT
ASP.NET CoreC#dotnet/aspnetcoreincluded in the SDK————Kestrel, minimal APIs
EF CoreC#dotnet/efcoredotnet add package Microsoft.EntityFrameworkCore————ORM
DapperC#DapperLib/Dapperdotnet add package Dapper————micro-ORM for hot query paths
BenchmarkDotNetC#dotnet/BenchmarkDotNetdotnet add package BenchmarkDotNet————microbenchmarks

Sources

  • Microsoft, .NET documentation, including Native AOT deployment docs and The large object heap docs.
  • Toub, Performance Improvements in .NET 8 (and the yearly series), .NET Blog. post
  • ECMA-334, C# Language Specification. spec
  • Microsoft, ASP.NET Core performance best practices. docs
  • Unity, Scripting backends. docs · Microsoft, Desktop Guide for WPF and Windows Forms. docs

Alternatives and related