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.OrdinalIgnoreCasefor 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.
- Domain —
InventoryLineentity withAdjust(int delta)enforcing non-negative on-hand;StockAdjustmentEventrecord as the command DTO. - Idempotency —
processed_eventstable with primary key onevent_id; skip duplicates before mutating stock. - Persistence — EF Core
DbContextwith a transaction wrapping event insert + inventory update; optimistic concurrency viarow_versioncolumn. - API — POST
/api/v1/adjustmentsreturns 202 Accepted; GET/api/v1/warehouses/{id}/snapshotreturns SKU counts for the dashboard. - Background sync —
IHostedServiceworker draining a channel queue for bulk CSV imports without blocking HTTP threads. - 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
| Need | Prefer C# | Consider instead |
|---|---|---|
| ASP.NET Core microservices on Linux containers | Yes — mature DI, performance, tooling | Java Spring for larger JVM-only shops |
| Unity or game client scripting | Yes — primary Unity language | C++ for engine-level code only |
| Cross-platform desktop with rich UI | Yes — WPF (Windows), MAUI, Avalonia | Electron + TypeScript for web-tech teams |
| Serverless cold-start sensitive functions | Maybe with Native AOT | Go or Rust for smallest binaries |
| Data science / notebooks | Limited | Python |
| Browser-only SPA without backend | No | TypeScript + React/Vue |
| Hard real-time embedded firmware | No | C, Rust |
| High-throughput CLI tool | Maybe with Native AOT publish | Go for simplicity |
Common pitfalls
- async void — only for UI event handlers; APIs should return
Taskso exceptions propagate. - Blocking on async —
.Resultor.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
StringBuilderor 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.csprojfiles 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
- Unity fundamentals explained — C# scripting for game objects, physics, and builds
- Java fundamentals explained — JVM alternative for enterprise microservices
- REST API design explained — HTTP contracts for ASP.NET Core endpoints
- Docker fundamentals explained — container images for .NET API deployments