Compare

Lang Compare

C#

App Development

Overview

Modern, object-oriented language by Microsoft. Used for desktop apps, games (Unity), web backends (ASP.NET), and cross-platform mobile apps (MAUI).

Resources

Installation & Getting Started

Install the .NET SDK, which includes the C# compiler, runtime, and CLI tools.

# macOS (Homebrew)
brew install dotnet

# Linux (Ubuntu/Debian)
sudo apt install dotnet-sdk-8.0

# Windows — download from https://dotnet.microsoft.com/download

# Or use the install script
curl -sSL https://dot.net/v1/dotnet-install.sh | bash

# Verify installation
dotnet --version
# REPL — C# Interactive (csi)
dotnet tool install -g dotnet-script
dotnet script    # Interactive REPL

# Or use C# Interactive in Visual Studio
# Tools > C# Interactive

# .NET Fiddle — try in the browser
# https://dotnetfiddle.net/

# Run a project
dotnet run

# Run a single file script (.csx)
dotnet script hello.csx

# Quick one-liner
dotnet script eval "Console.WriteLine(\"Hello!\")"

Project Scaffolding

The dotnet CLI creates projects from built-in templates.

# Console app
dotnet new console -n MyApp
cd MyApp

# Web API
dotnet new webapi -n MyApi

# Class library
dotnet new classlib -n MyLib

# List available templates
dotnet new list

# Run the project
dotnet run

# Build for release
dotnet build -c Release

# Publish self-contained
dotnet publish -c Release --self-contained

Package Management

NuGet is the package manager for .NET. Managed via the dotnet CLI or Visual Studio.

# Add a package
dotnet add package Newtonsoft.Json

# Add a specific version
dotnet add package Serilog --version 3.1.0

# Remove a package
dotnet remove package Newtonsoft.Json

# Restore packages
dotnet restore

# List installed packages
dotnet list package

# Update packages
dotnet outdated  # requires dotnet-outdated tool

Tooling & Formatter/Linter

C# uses .editorconfig for style rules and Roslyn analyzers for linting. dotnet format is the built-in formatter.

# dotnet format — built-in formatter
dotnet format                 # Format entire solution
dotnet format --verify-no-changes  # CI check

# Roslyn analyzers (NuGet packages)
dotnet add package Microsoft.CodeAnalysis.NetAnalyzers
dotnet add package StyleCop.Analyzers

# CSharpier — Prettier-inspired opinionated formatter
dotnet tool install csharpier -g
dotnet csharpier .
# .editorconfig
[*.cs]
indent_style = space
indent_size = 4
dotnet_sort_system_directives_first = true
csharp_new_line_before_open_brace = all
csharp_prefer_braces = true
dotnet_diagnostic.CA1062.severity = warning
<!-- Directory.Build.props — enable analyzers project-wide -->
<Project>
  <PropertyGroup>
    <AnalysisLevel>latest-recommended</AnalysisLevel>
    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>

Build & Compile Model

Compiled to IL (Intermediate Language), then JIT or AOT compiled. C# targets the .NET runtime (CLR).

# Build (compiles to IL in .dll)
dotnet build
dotnet build -c Release

# Run (JIT compiles IL to native at runtime)
dotnet run

# Publish — framework-dependent (needs .NET installed)
dotnet publish -c Release

# Publish — self-contained (includes runtime)
dotnet publish -c Release --self-contained -r linux-x64

# Native AOT (ahead-of-time, .NET 8+)
dotnet publish -c Release -r linux-x64 /p:PublishAot=true
# Output: single native binary, no .NET runtime needed

Execution model:

  • Source → Roslyn compiler → IL (.dll) → JIT (RyuJIT) → Machine code
  • JIT compilation happens at runtime, method by method
  • Native AOT eliminates JIT — compiles directly to native binary
  • Tiered compilation: quick JIT first, re-optimize hot paths
  • ReadyToRun (R2R) pre-compiles IL for faster startup

Libraries & Frameworks

C# has a mature ecosystem centered around .NET and NuGet. Strong in enterprise, web, games, and desktop.

Web Frameworks - ASP.NET Core, Minimal APIs, Blazor (WASM), Carter

Desktop / UI - WPF, WinUI 3, MAUI, Avalonia, WinForms

Game Development - Unity, Godot (C# support), MonoGame, Stride

ORM / Data - Entity Framework Core, Dapper, LINQ to DB, NHibernate

API / Communication - gRPC, SignalR, MassTransit, MediatR, Refit

Serialization - System.Text.Json, Newtonsoft.Json, MessagePack

Logging - Serilog, NLog, Microsoft.Extensions.Logging

Testing - xUnit, NUnit, FluentAssertions, Moq, NSubstitute, Bogus

Cloud - Azure SDK, AWS SDK, Pulumi

CLI - System.CommandLine, Spectre.Console, Cocona

DI / IoC - Microsoft.Extensions.DependencyInjection, Autofac

Testing

Popular frameworks: xUnit (most popular), NUnit, and MSTest. FluentAssertions improves readability. Built-in dotnet test runner.

// xUnit (recommended)
using Xunit;

public class MathTests
{
    [Fact]
    public void Add_ReturnsCorrectSum()
    {
        Assert.Equal(3, 1 + 2);
    }

    [Theory]
    [InlineData(1, 2, 3)]
    [InlineData(0, 0, 0)]
    [InlineData(-1, 1, 0)]
    public void Add_WithVariousInputs(int a, int b, int expected)
    {
        Assert.Equal(expected, a + b);
    }
}
// FluentAssertions — readable assertions
using FluentAssertions;

[Fact]
public void List_Should_Contain_Item()
{
    var list = new[] { 1, 2, 3 };
    list.Should().Contain(2);
    list.Should().HaveCount(3);
}
# Run tests
dotnet test                    # All tests
dotnet test --filter "Add"     # Specific tests
dotnet test --collect:"XPlat Code Coverage"  # With coverage
dotnet test --logger "console;verbosity=detailed"

Debugging

Visual Studio and Rider have best-in-class debuggers. VS Code with C# extension also works well.

// Debug output
Console.WriteLine($"Value: {value}");
System.Diagnostics.Debug.WriteLine("debug only");

// Debugger.Break() — programmatic breakpoint
if (suspiciousCondition)
    System.Diagnostics.Debugger.Break();

// Logging (Microsoft.Extensions.Logging)
ILogger<MyService> _logger;
_logger.LogInformation("Processing {Id}", item.Id);
_logger.LogError(ex, "Failed to process {Id}", item.Id);
_logger.LogDebug("Details: {@Item}", item); // Structured
# Visual Studio debugging
# F5: Start debugging
# F9: Toggle breakpoint
# F10: Step over, F11: Step into
# Features: conditional breakpoints, hit counts,
# data breakpoints, IntelliTrace, hot reload

# VS Code
# Install C# extension (ms-dotnettools.csharp)
# launch.json auto-generated for .NET projects
# F5 to start debugging

# Rider (JetBrains)
# Built-in debugger with decompilation

# dotnet-dump — analyze crash dumps
dotnet dump collect --process-id <pid>
dotnet dump analyze <dump-file>
# dumpheap, dumpobj, gcroot commands

# dotnet-sos — SOS debugging extension
dotnet tool install -g dotnet-sos
dotnet sos install

# Remote debugging
# Visual Studio: Debug > Attach to Process > SSH

Variables

Use var for type inference or explicit types. const and readonly for immutability.

// Type inference
var name = "Alice";
var age = 30;

// Explicit types
string label = "hello";
int count = 0;

// Constants (compile-time)
const double Pi = 3.14159;

// Readonly (runtime)
readonly int maxSize = 100;

// Destructuring (tuples)
var (x, y) = (1, 2);

// Nullable types
string? maybeNull = null;
int? maybeInt = null;

Types

Statically typed with value types, reference types, generics, and records.

// Value types
int n = 42;
double f = 3.14;
bool b = true;
char c = 'a';

// Reference types
string s = "hello";
int[] arr = { 1, 2, 3 };

// Classes
class User {
    public string Name { get; set; }
    public int Age { get; set; }
}

// Records (immutable by default)
record Point(int X, int Y);

// Enums
enum Color { Red, Green, Blue }

// Generics
List<int> nums = new() { 1, 2, 3 };
Dictionary<string, int> map = new() { ["a"] = 1 };

// Nullable reference types
string? nullable = null;

Data Structures

Rich System.Collections.Generic namespace: List<T>, Dictionary<K,V>, HashSet<T>, Queue<T>, Stack<T>, and more.

// List<T> — dynamic array
var list = new List<int> { 1, 2, 3 };
list.Add(4);
list.Remove(2);
list.Contains(3);        // true
list.Count;              // 3

// Dictionary<TKey, TValue>
var dict = new Dictionary<string, int>
{
    ["alice"] = 95,
    ["bob"] = 87,
};
dict["charlie"] = 72;
dict.TryGetValue("alice", out var score);
dict.ContainsKey("bob");

// HashSet<T>
var set = new HashSet<int> { 1, 2, 3 };
set.Add(4);
set.Contains(2);
set.IntersectWith(new[] { 2, 3, 5 });

// Arrays — fixed size
int[] arr = { 1, 2, 3 };
int[,] matrix = { { 1, 2 }, { 3, 4 } };
// Queue and Stack
var queue = new Queue<string>();
queue.Enqueue("first");
var item = queue.Dequeue();

var stack = new Stack<int>();
stack.Push(1);
var top = stack.Pop();

// Immutable collections
using System.Collections.Immutable;
var iList = ImmutableList.Create(1, 2, 3);
var iDict = ImmutableDictionary<string, int>.Empty
    .Add("a", 1);

// LINQ on any collection
var evens = list.Where(x => x % 2 == 0).ToList();
var grouped = items.GroupBy(x => x.Category);

// Span<T> — stack-allocated view (no allocation)
Span<int> span = stackalloc int[] { 1, 2, 3 };

Functions

Methods live inside classes or structs. C# also supports local functions and lambdas.

// Method
string Greet(string name) {
    return $"Hello, {name}!";
}

// Expression-bodied method
int Add(int a, int b) => a + b;

// Default parameters
int Power(int baseVal, int exp = 2)
    => (int)Math.Pow(baseVal, exp);

// Lambda expressions
Func<int, int> double = x => x * 2;
Action<string> log = msg => Console.WriteLine(msg);

// Local function
void Outer() {
    int Inner(int x) => x + 1;
    Console.WriteLine(Inner(5));
}

// Generic method
T First<T>(List<T> items) => items[0];

Conditionals

if/else, switch expressions, and pattern matching.

// If / else
if (x > 0) {
    Console.WriteLine("positive");
} else if (x == 0) {
    Console.WriteLine("zero");
} else {
    Console.WriteLine("negative");
}

// Ternary
var label = x > 0 ? "positive" : "non-positive";

// Switch expression (pattern matching)
var result = shape switch {
    Circle c => Math.PI * c.Radius * c.Radius,
    Rectangle r => r.Width * r.Height,
    _ => 0
};

// Pattern matching with when
var category = age switch {
    <= 12 => "child",
    <= 17 => "teen",
    <= 64 => "adult",
    _ => "senior"
};

// Null-coalescing
var val = input ?? "default";
var city = user?.Address?.City;

Loops

Standard for, foreach, while, and LINQ for functional iteration.

// For loop
for (int i = 0; i < 5; i++) {
    Console.WriteLine(i);
}

// Foreach
foreach (var item in new[] { "a", "b", "c" }) {
    Console.WriteLine(item);
}

// While
int n = 0;
while (n < 3) {
    n++;
}

// Do-while
do {
    n--;
} while (n > 0);

// LINQ
var squares = Enumerable.Range(0, 10)
    .Select(x => x * x)
    .ToList();

var evens = new[] { 1, 2, 3, 4 }
    .Where(x => x % 2 == 0);

Generics & Type System

Reified generics — type information preserved at runtime (unlike Java). Supports constraints, covariance, and contravariance.

// Generic method
T Identity<T>(T value) => value;

// Constraints
T Max<T>(T a, T b) where T : IComparable<T>
    => a.CompareTo(b) > 0 ? a : b;

// Multiple constraints
void Process<T>(T item)
    where T : class, IDisposable, new()
{
    // T must be: reference type, IDisposable, have parameterless constructor
}

// Generic class
class Stack<T>
{
    private readonly List<T> _items = new();
    public void Push(T item) => _items.Add(item);
    public T Pop()
    {
        var item = _items[^1];
        _items.RemoveAt(_items.Count - 1);
        return item;
    }
}
// Variance (on interfaces and delegates)
// Covariant (out) — can return T
interface IProducer<out T> { T Produce(); }
// Contravariant (in) — can accept T
interface IConsumer<in T> { void Consume(T item); }

// IEnumerable<out T> is covariant:
IEnumerable<object> objects = new List<string>(); // OK

// Generic interfaces
interface IRepository<T> where T : class
{
    Task<T?> FindByIdAsync(int id);
    Task SaveAsync(T entity);
}

// Nullable reference types (C# 8+)
string? nullable = null;     // Allowed
string nonNull = "hello";    // Compiler warns if null assigned

// Pattern matching with types
object obj = 42;
if (obj is int n) Console.WriteLine(n * 2);

// Generic math (C# 11+)
T Add<T>(T a, T b) where T : INumber<T> => a + b;

Inheritance & Composition

Single class inheritance with interfaces for multiple behavior contracts. Abstract classes, sealed classes, and extension methods.

// Class inheritance
class Animal {
    public string Name { get; }
    public Animal(string name) => Name = name;
    public virtual string Speak() => $"{Name} makes a sound";
}

class Dog : Animal {
    public Dog(string name) : base(name) {}
    public override string Speak() => $"{Name} barks";
}

// Abstract classes
abstract class Shape {
    public abstract double Area();
    public string Describe() => $"Area: {Area()}";
}

// Interfaces (multiple)
interface ISerializable { string ToJson(); }
interface IPrintable { void Print(); }

class Report : ISerializable, IPrintable {
    public string ToJson() => "{}";
    public void Print() => Console.WriteLine(ToJson());
}

// Sealed (prevent further inheritance)
sealed class Singleton : Animal {
    public Singleton() : base("single") {}
}

// Extension methods (add behavior without inheritance)
static class StringExt {
    public static string Shout(this string s) => s.ToUpper() + "!";
}
// "hello".Shout() => "HELLO!"

Functional Patterns

LINQ, lambdas, delegates, and expression-bodied members bring functional style to C#.

// Lambdas and delegates
Func<int, int> double = x => x * 2;
Func<int, int, int> add = (a, b) => a + b;
Action<string> log = msg => Console.WriteLine(msg);

// LINQ (functional collection processing)
var nums = new[] { 1, 2, 3, 4, 5 };
var squared = nums.Select(x => x * x);
var evens = nums.Where(x => x % 2 == 0);
var sum = nums.Aggregate(0, (acc, x) => acc + x);

// Chaining
var result = Enumerable.Range(0, 100)
    .Where(x => x % 2 == 0)
    .Select(x => x * x)
    .Take(10)
    .Sum();

// Higher-order functions
T Apply<T>(Func<T, T> fn, T value) => fn(value);

// Pattern matching as expression
string Describe(object obj) => obj switch {
    int n when n > 0 => "positive",
    int n => "non-positive",
    string s => $"string: {s}",
    _ => "unknown"
};

// Immutable records
record Point(int X, int Y);
var p1 = new Point(1, 2);
var p2 = p1 with { X = 10 }; // non-destructive mutation

// Local functions
int Factorial(int n) {
    int Go(int x, int acc) => x <= 1 ? acc : Go(x - 1, acc * x);
    return Go(n, 1);
}

Concurrency

Task-based async/await with the Task Parallel Library (TPL). First-class async support throughout the framework.

// async/await — non-blocking I/O
async Task<string> FetchDataAsync(string url)
{
    using var client = new HttpClient();
    return await client.GetStringAsync(url);
}

// Concurrent tasks
var tasks = new[]
{
    FetchDataAsync("/api/a"),
    FetchDataAsync("/api/b"),
};
string[] results = await Task.WhenAll(tasks);

// Cancellation
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await DoWorkAsync(cts.Token);
// Parallel — CPU parallelism
Parallel.ForEach(items, item =>
{
    Process(item);
});

// PLINQ — parallel LINQ
var results = data
    .AsParallel()
    .Where(x => x > 0)
    .Select(x => x * 2)
    .ToList();

// Channels — producer/consumer
var channel = Channel.CreateBounded<int>(100);
// Producer
await channel.Writer.WriteAsync(42);
// Consumer
var item = await channel.Reader.ReadAsync();

// Lock / SemaphoreSlim
private readonly SemaphoreSlim _semaphore = new(1, 1);
await _semaphore.WaitAsync();
try { /* critical section */ }
finally { _semaphore.Release(); }

Modules & Imports

C# uses namespaces to organize code and assemblies for distribution. using directives import namespaces. Projects produce DLLs or executables via MSBuild.

// Import namespaces
using System;
using System.Collections.Generic;
using System.Linq;

// Import with alias
using Json = System.Text.Json.JsonSerializer;
using static System.Math; // import static members

// Global usings (C# 10, apply to entire project)
global using System.Collections.Generic;

// File-scoped namespace (C# 10)
namespace MyApp.Models;

public class User {
    public string Name { get; set; }
}

// Traditional namespace
namespace MyApp.Services {
    public class UserService {
        public User GetUser() => new();
    }
}

// Internal vs public (assembly-level visibility)
public class PublicApi {}    // visible to other assemblies
internal class InternalOnly {} // same assembly only

// Assemblies: each .csproj produces a DLL
// Reference other projects in .csproj:
// <ProjectReference Include="../MyLib/MyLib.csproj" />

Error Handling

Uses try/catch/finally with typed exceptions.

// Try / catch
try {
    var result = int.Parse("not a number");
} catch (FormatException ex) {
    Console.WriteLine($"Parse error: {ex.Message}");
} catch (Exception ex) {
    Console.WriteLine($"Error: {ex.Message}");
} finally {
    Console.WriteLine("always runs");
}

// Throwing exceptions
void Divide(int a, int b) {
    if (b == 0)
        throw new DivideByZeroException();
    return a / b;
}

// Custom exceptions
class AppException : Exception {
    public int Code { get; }
    public AppException(string msg, int code)
        : base(msg) => Code = code;
}

// Exception filters
catch (Exception ex) when (ex.Message.Contains("timeout")) {
    // handle timeout specifically
}

Memory Management

Generational garbage collector (Gen 0, 1, 2 + LOH). Supports IDisposable for deterministic cleanup and Span<T> for stack allocation.

// GC is automatic — no manual free
var list = new List<int> { 1, 2, 3 };
// GC collects when Gen 0 fills up

// IDisposable — deterministic cleanup
using var file = File.OpenRead("data.txt");
// file.Dispose() called at end of scope

// Classic using statement
using (var conn = new SqlConnection(connStr))
{
    conn.Open();
    // ...
} // Dispose() called here

// Implement IDisposable
class ManagedResource : IDisposable
{
    private bool _disposed;
    public void Dispose()
    {
        if (!_disposed)
        {
            // Release resources
            _disposed = true;
        }
    }
}
// Stack allocation with Span<T> and stackalloc
Span<int> numbers = stackalloc int[100];
// No heap allocation, no GC pressure

// ArrayPool — reuse arrays
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(1024);
// ... use buffer ...
pool.Return(buffer);

// WeakReference
var weak = new WeakReference<MyObject>(obj);
if (weak.TryGetTarget(out var target))
    target.DoSomething();

// GC control
GC.Collect();                    // Force collection
GC.GetTotalMemory(true);        // Total managed memory
GC.TryStartNoGCRegion(1024);    // Suppress GC temporarily

// Value types (struct) live on stack — no GC
struct Point { public int X, Y; }

// ref struct — guaranteed stack-only
ref struct StackOnly { public Span<int> Data; }

Performance Profiling

BenchmarkDotNet for benchmarks, dotnet-trace/dotnet-counters for runtime profiling, Visual Studio Profiler for GUI.

// BenchmarkDotNet — micro-benchmarking
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]
public class SortBenchmark
{
    private int[] _data = Enumerable.Range(0, 1000).Reverse().ToArray();

    [Benchmark]
    public void ArraySort() => Array.Sort((int[])_data.Clone());

    [Benchmark]
    public void LinqOrderBy() => _data.OrderBy(x => x).ToArray();
}

// Run: BenchmarkRunner.Run<SortBenchmark>();
# dotnet-counters — real-time metrics
dotnet tool install -g dotnet-counters
dotnet counters monitor --process-id 12345

# dotnet-trace — collect traces
dotnet tool install -g dotnet-trace
dotnet trace collect --process-id 12345
# Open .nettrace file in Visual Studio or PerfView

# dotnet-dump — memory analysis
dotnet tool install -g dotnet-dump
dotnet dump collect --process-id 12345
dotnet dump analyze dump.dmp

# dotnet-gcdump — GC heap analysis
dotnet gcdump collect --process-id 12345

# Visual Studio: Debug > Performance Profiler
# - CPU Usage, Memory Usage, .NET Object Allocation
# - Async tool, Database, Events

# JetBrains dotTrace / dotMemory
# Rider: built-in profiling tools

Interop

P/Invoke for native C calls, COM interop, and strong .NET interop across languages (F#, VB.NET). NativeAOT enables C-compatible exports.

// P/Invoke — call native C libraries
using System.Runtime.InteropServices;

[DllImport("libc", EntryPoint = "sqrt")]
static extern double Sqrt(double x);

double result = Sqrt(16.0); // 4.0

// Windows API
[DllImport("user32.dll")]
static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);

// LibraryImport (source-generated, .NET 7+)
[LibraryImport("libm")]
private static partial double sqrt(double x);
// NativeAOT — export C-compatible functions
[UnmanagedCallersOnly(EntryPoint = "add")]
public static int Add(int a, int b) => a + b;
// Build: dotnet publish /p:PublishAot=true
// Produces .so/.dylib/.dll callable from C

// COM interop (Windows)
dynamic excel = Activator.CreateInstance(
    Type.GetTypeFromProgID("Excel.Application")!);
excel.Visible = true;

// Process — call system commands
using System.Diagnostics;
var output = Process.Start(new ProcessStartInfo("ls", "-la") {
    RedirectStandardOutput = true
})!.StandardOutput.ReadToEnd();

// .NET languages interop seamlessly
// C# can call F#, VB.NET, and vice versa
// All compile to the same IL

Packaging & Distribution

Publish libraries to NuGet. Distribute apps as self-contained executables, Docker containers, or native AOT binaries.

# Create a NuGet package
dotnet pack -c Release
# Output: bin/Release/MyLib.1.0.0.nupkg

# Publish to NuGet.org
dotnet nuget push MyLib.1.0.0.nupkg --api-key <key> \
  --source https://api.nuget.org/v3/index.json

# .csproj package metadata
# <PackageId>MyLib</PackageId>
# <Version>1.0.0</Version>
# <Authors>Me</Authors>
# <Description>My library</Description>
# <PackageLicenseExpression>MIT</PackageLicenseExpression>
# Self-contained deployment (includes .NET runtime)
dotnet publish -c Release --self-contained -r linux-x64
dotnet publish -c Release --self-contained -r osx-arm64
dotnet publish -c Release --self-contained -r win-x64

# Single file
dotnet publish -c Release -r linux-x64 \
  /p:PublishSingleFile=true /p:IncludeNativeLibrariesForSelfExtract=true

# Native AOT (no .NET runtime, fast startup)
dotnet publish -c Release -r linux-x64 /p:PublishAot=true

# .NET CLI tools
dotnet pack
dotnet tool install -g my-tool
# Users: dotnet tool install -g my-tool

# Docker
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /app
COPY . .
RUN dotnet publish -c Release -o /out

FROM mcr.microsoft.com/dotnet/aspnet:8.0
COPY --from=build /out .
ENTRYPOINT ["dotnet", "MyApp.dll"]