Guide

C# fundamentals explained

A warehouse management API receives stock-adjustment events from handheld scanners, barcode readers, and an e-commerce webhook feed — all at once. The service must deduplicate events, update inventory atomically, and return consistent counts to a dashboard that refreshes every few seconds. That kind of concurrent, strongly typed backend is a natural fit for C# on .NET: a modern, garbage-collected language with first-class async/await, expressive LINQ queries, and a runtime (CLR) that compiles to native code via tiered JIT and runs on Linux containers as easily as Windows servers. C# also powers Unity game scripts, Blazor web UIs, and desktop tools — one language across cloud APIs and client runtimes. This guide covers the .NET stack, value and reference semantics, classes and records, nullable reference types, collections and LINQ, exception patterns, async concurrency, the dotnet CLI and NuGet, ASP.NET Core basics, a Harbor Supply inventory service worked example, a language decision table, common pitfalls, and a production checklist — alongside our Java and TypeScript language guides.

What C# is: CLR, SDK, and the .NET platform

C# source (.cs files) compiles to Common Intermediate Language (CIL) bytecode packaged in assemblies (.dll / .exe). The Common Language Runtime (CLR) loads assemblies, JIT-compiles hot methods to native machine code, manages memory via a generational garbage collector, and provides threading, security, and interop services. The .NET SDK bundles the compiler (csc), runtime, standard libraries (System.*), and the dotnet CLI for build, test, and publish workflows.

.NET 8 (LTS through November 2026) and .NET 9 are the current adoption targets for new services. Pin your target framework moniker (net8.0) in .csproj files and Docker base images — mixing major versions in one solution causes subtle package and API mismatches. .NET runs on Linux, macOS, and Windows; ASP.NET Core APIs typically ship as self-contained or framework-dependent deployments in slim container images.

Why teams choose C# in 2026

  • Productivity — properties, LINQ, pattern matching, and records reduce boilerplate versus older enterprise languages.
  • Performance — tiered compilation, SIMD intrinsics, and Native AOT (where applicable) compete with Go and Java on throughput benchmarks.
  • Unified stack — share DTOs and validation logic between ASP.NET Core APIs, worker services, and Unity clients via class libraries.
  • Tooling — Visual Studio, Rider, and VS Code with OmniSharp deliver refactoring, analyzers, and debugger integration that speeds large codebases.

Value types, reference types, and memory semantics

C# divides types into value types (struct, primitives, enum) stored inline or on the stack when local, and reference types (class, interface, delegate, string) allocated on the managed heap with references passed around. Understanding this split prevents subtle bugs: mutating a struct copy does not affect the original; mutating a shared class instance visible through multiple references does.

public readonly record struct SkuQuantity(string Sku, int Count);

public sealed class InventoryLine
{
    public string Sku { get; }
    public int OnHand { get; private set; }
    public void Adjust(int delta) => OnHand += delta;
}

Use readonly record struct for small immutable value bundles (SKU + count pairs). Use classes for entities with identity and mutable state guarded by methods. Avoid large structs — copying cost adds up; the framework’s own Vector3 in Unity is a struct because it is three floats passed millions of times per frame.

Nullable reference types

Since C# 8, nullable reference types (NRT) annotate whether a string or custom class reference may be null. With NRT enabled, string? signals optional values and the compiler warns on dereferencing without a null check. Treat NRT as documentation enforced at compile time — not a runtime guarantee across assembly boundaries unless callers respect annotations.

Classes, records, interfaces, and pattern matching

A class supports inheritance (: base class), virtual methods, and interfaces. A record (class or struct) emphasizes immutable data with value-based equality — ideal for commands, events, and API response DTOs:

public sealed record StockAdjustmentEvent(
    string EventId,
    string Sku,
    int Delta,
    DateTimeOffset OccurredAt);

Interfaces define contracts; default interface methods (C# 8+) let you evolve APIs. Prefer composition and dependency injection over deep inheritance trees. C# 11+ pattern matching on switch expressions handles discriminated unions cleanly when combined with records:

string Describe(IAdjustment adj) => adj switch
{
    StockAdjustmentEvent e when e.Delta > 0 => $"restock {e.Sku}",
    StockAdjustmentEvent e => $"shrink {e.Sku}",
    _ => "unknown"
};

Properties replace Java-style getters/setters: public int OnHand { get; private set; }. Use init accessors for immutable properties set only at construction time.

Collections, generics, and LINQ

Generics are reified at runtime for reference and value type parameters (unlike Java’s type erasure) — List<int> and List<string> are distinct CLR types. Core collections live in System.Collections.Generic:

  • List<T> — dynamic array; default for ordered sequences.
  • Dictionary<TKey,TValue> — hash map; use StringComparer.OrdinalIgnoreCase for SKU keys if case-insensitive.
  • HashSet<T> — unique membership; deduplicate event IDs before processing.
  • Queue<T> / Stack<T> — FIFO/LIFO work queues in background workers.

LINQ (Language Integrated Query) chains lazy operators over IEnumerable<T> and IQueryable<T>:

var lowStock = inventory
    .Where(line => line.OnHand < line.ReorderPoint)
    .OrderBy(line => line.OnHand)
    .Select(line => line.Sku)
    .ToList();

LINQ to Objects runs in memory; LINQ to Entities (EF Core) translates expression trees to SQL — always inspect generated SQL for N+1 query patterns. Prefer TryGetValue on dictionaries and Span<T> / Memory<T> for hot paths that want to avoid allocations.

Exceptions, validation, and error models

C# uses exceptions for exceptional failures, not control flow. Unlike Java, C# does not distinguish checked vs unchecked exceptions — every method may throw, and callers choose whether to catch. For domain validation, return Result types or use libraries like FluentResults, or model errors in ASP.NET Core’s ProblemDetails responses for HTTP APIs.

if (delta == 0)
    throw new ArgumentException("delta must be non-zero", nameof(delta));

using var scope = logger.BeginScope(new Dictionary<string, object>
{
    ["eventId"] = evt.EventId,
    ["sku"] = evt.Sku
});

using declarations (C# 8+) dispose IDisposable resources at scope exit — database connections, transactions, and HTTP responses. Combine with await using for IAsyncDisposable in EF Core 5+. Log exceptions with structured fields; never log secrets or full payment payloads.

Async and await: non-blocking I/O without callback hell

async/await compiles state machines that release threads during I/O waits — critical for ASP.NET Core servers handling thousands of concurrent requests on a modest thread pool. Mark I/O-bound methods async Task<T> and await database, HTTP, and file calls; avoid Task.Run for wrapping synchronous I/O (it steals thread-pool threads without fixing blocking).

public async Task<InventorySnapshot> GetSnapshotAsync(
    string warehouseId, CancellationToken ct)
{
    await using var conn = dataSource.CreateConnection();
    await conn.OpenAsync(ct);
    // ...
}

Pass CancellationToken from ASP.NET Core request abort signals through your call stack so clients disconnecting mid-request stop expensive queries. Use ConfigureAwait(false) in library code (not ASP.NET Core handlers) to avoid unnecessary context captures. For CPU-bound parallelism, use Parallel.ForEachAsync (.NET 6+) with a bounded MaxDegreeOfParallelism rather than unbounded Task.WhenAll fan-out.

dotnet CLI, NuGet, and project structure

The dotnet CLI scaffolds and builds solutions:

  • dotnet new webapi -n Harbor.Supply.Api — ASP.NET Core minimal API or controller template.
  • dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL — pulls NuGet dependencies with version resolution.
  • dotnet test / dotnet publish -c Release — CI-friendly build and container-ready output.

Organize solutions into projects: Domain (entities, no framework refs), Application (use cases), Infrastructure (EF Core, HTTP clients), Api (controllers or minimal endpoints). Keep business rules testable without spinning up Kestrel. Central Package Management (Directory.Packages.props) pins versions across a monorepo — similar to Maven BOMs or npm workspaces.

ASP.NET Core essentials for HTTP APIs

ASP.NET Core is the cross-platform web framework for C#. A minimal API endpoint maps HTTP verbs to handlers with built-in dependency injection, middleware pipelines, and model binding:

app.MapPost("/adjustments", async (
    StockAdjustmentEvent evt,
    IInventoryService svc,
    CancellationToken ct) =>
{
    await svc.ApplyAsync(evt, ct);
    return Results.Accepted();
});

Middleware order matters: exception handling, HTTPS redirection, authentication, authorization, then endpoints. Use Entity Framework Core for ORM access to PostgreSQL, or Dapper for hand-written SQL in latency-sensitive paths. Validate inputs with DataAnnotations or FluentValidation; return RFC 7807 ProblemDetails for 400/404/409 responses per our REST API design guide.

Worked example: Harbor Supply inventory service

Harbor Supply runs regional warehouses. Scanners POST stock adjustments; the e-commerce platform emits reservation events. The inventory API must apply adjustments idempotently and expose real-time counts.

  1. DomainInventoryLine entity with Adjust(int delta) enforcing non-negative on-hand; StockAdjustmentEvent record as the command DTO.
  2. Idempotencyprocessed_events table with primary key on event_id; skip duplicates before mutating stock.
  3. Persistence — EF Core DbContext with a transaction wrapping event insert + inventory update; optimistic concurrency via row_version column.
  4. API — POST /api/v1/adjustments returns 202 Accepted; GET /api/v1/warehouses/{id}/snapshot returns SKU counts for the dashboard.
  5. Background syncIHostedService worker draining a channel queue for bulk CSV imports without blocking HTTP threads.
  6. Deploy — publish to a Docker image on mcr.microsoft.com/dotnet/aspnet:8.0; health checks on /health; scale horizontally behind a load balancer with sticky sessions disabled.

Result: a service that survives scanner retries, keeps inventory consistent under concurrent updates, and separates HTTP concerns from domain logic for fast unit tests with xUnit and NSubstitute mocks.

Language decision table

NeedPrefer C#Consider instead
ASP.NET Core microservices on Linux containersYes — mature DI, performance, toolingJava Spring for larger JVM-only shops
Unity or game client scriptingYes — primary Unity languageC++ for engine-level code only
Cross-platform desktop with rich UIYes — WPF (Windows), MAUI, AvaloniaElectron + TypeScript for web-tech teams
Serverless cold-start sensitive functionsMaybe with Native AOTGo or Rust for smallest binaries
Data science / notebooksLimitedPython
Browser-only SPA without backendNoTypeScript + React/Vue
Hard real-time embedded firmwareNoC, Rust
High-throughput CLI toolMaybe with Native AOT publishGo for simplicity

Common pitfalls

  • async void — only for UI event handlers; APIs should return Task so exceptions propagate.
  • Blocking on async.Result or .Wait() on async code causes deadlocks in ASP.NET Core; await all the way up.
  • Capturing large objects in closures — lambdas in hot loops extend lifetimes unexpectedly; prefer local copies or structs.
  • LINQ over huge collections in memory — filter in SQL with EF Core instead of loading entire tables.
  • Ignoring nullable warnings — suppressing NRT without fixing call sites reintroduces NullReferenceException.
  • Mutable string concatenation in loops — use StringBuilder or interpolation outside loops.
  • Default struct equality bugs — mutable structs break hash-based collections; prefer readonly structs or classes.
  • Logging sensitive SKUs or PII — structured logs still need redaction policies in retail and health contexts.

Production checklist

  • Pin net8.0 (or current LTS) in all .csproj files and CI images.
  • Enable nullable reference types and treat warnings as errors in CI.
  • xUnit or NUnit unit tests; WebApplicationFactory integration tests for API endpoints.
  • Serilog or built-in logging with JSON output; OpenTelemetry traces and metrics.
  • Health checks (/health, /alive) wired to orchestrators.
  • EF Core migrations versioned in source control; review generated SQL on large tables.
  • Secrets via environment variables, Azure Key Vault, or HashiCorp Vault — never in appsettings committed to git.
  • Rate limiting and request size caps on public adjustment endpoints.
  • Load test idempotent event replay and optimistic concurrency conflict rates.
  • Dependabot or Renovate for NuGet security advisories.

Key takeaways

  • C# on .NET combines modern language features with a high-performance CLR suitable for Linux-hosted APIs and Unity clients alike.
  • Distinguish value vs reference types; use records for immutable commands and entities with behavior for mutable domain state.
  • async/await and LINQ are core idioms — use them with cancellation tokens and database-side filtering.
  • ASP.NET Core plus EF Core covers most inventory, order, and webhook services with clear project layering.
  • Choose C# when .NET tooling, Unity overlap, or Microsoft ecosystem integration outweigh Go’s simpler deploy model or Java’s larger legacy footprint.

Related reading