Last updated: December 13, 2025
Author: Paul Namalomba
- SESKA Computational Engineer
- Software Developer
- PhD Candidate (Civil Engineering Spec. Computational and Applied Mechanics)
Contact: kabwenzenamalomba@gmail.com
Website: paulnamalomba.github.io
This guide is a comprehensive reference for C# developers covering language fundamentals, data structures (built-in and advanced), classes/objects/modules, coding style, and practical language mechanics. Designed to match the style of existing guides, it provides clear examples, code snippets, tables for comparison, tips, and pitfalls for each major topic.
Use this guide as a reference when designing APIs, choosing data structures, writing idiomatic C#, or preparing for production deployment across .NET platforms.
csc or Roslyn) into Common Intermediate Language (CIL) contained in assemblies (.dll or .exe).Key steps: 1. Source (.cs) → Roslyn compiler → IL (assembly) 2. CLR loads assembly, verifies metadata 3. JIT compiles IL methods on first use to native code 4. Execution under CLR with GC, security, and interop
Notes: - Roslyn provides compiler-as-a-service APIs used by analyzers and IDE tooling. - Ahead-of-Time (AOT) compilation and ReadyToRun options exist for performance-sensitive scenarios.
IDisposable for unmanaged resources and use using/using var for scope-based disposal.Memory tips:
- Avoid large allocations on LOH when possible.
- Prefer pooling (ArrayPool<T>) for frequently allocated buffers.
- Use Span<T>, Memory<T> to work with slices without allocations for high-performance workloads.
| Aspect | Value Types | Reference Types |
|---|---|---|
| Examples | int, float, struct, bool |
class, string, object, arrays, delegates |
| Storage | Stack (or inline in object) | Heap (object referenced by pointer) |
| Copy semantics | Copy the value (deep copy of fields) | Copy the reference (shallow copy) |
| Nullability | Non-nullable (unless nullable T?) | Can be null (nullable reference types in C#8+ enabled) |
| Default | Zero-initialized | null reference |
Pitfalls:
- Boxing/unboxing: avoid unnecessary boxing of value types into object (performance overhead).
- Structs should be small and immutable; large structs cause copies and performance overhead.
Example:
int a = 5;
int b = a; // copy of value
b = 7; // a is still 5
class Node { public int Value; }
var n1 = new Node { Value = 5 };
var n2 = n1; // reference copy
n2.Value = 7; // n1.Value is now 7
byte, sbyte, short, ushort, int, uint, long, ulongfloat, double, decimal (decimal for financial high-precision)char, bool, string (reference type), objectint?, DateTime?Use decimal for money, double for scientific, float for memory-constrained scenarios.
T[].Example:
int[] arr = new int[5];
arr[0] = 42;
// Multidimensional
int[,] matrix = new int[3,4];
// Jagged array
int[][] jagged = new int[3][];
jagged[0] = new int[] {1,2};
Notes/tips:
- Arrays have Length, not Count.
- When you need dynamic sizing use List<T>.
List<T> is the go-to dynamic array, it resizes automatically.IList<T>, IReadOnlyList<T> interfaces provide abstraction.Example:
var list = new List<string>();
list.Add("hello");
list.RemoveAt(0);
foreach(var s in list) Console.WriteLine(s);
Performance:
- List<T> has amortized O(1) append; use Capacity property to pre-allocate when size known.
Dictionary<TKey, TValue>: hash table mapping keys to values; O(1) average lookup.HashSet<T>: unique collection based on hashing.Queue<T>: FIFO, use Enqueue/Dequeue.Stack<T>: LIFO, use Push/Pop.Example:
var dict = new Dictionary<string,int>();
dict["apples"] = 3;
if (dict.TryGetValue("apples", out var val)) Console.WriteLine(val);
var set = new HashSet<int> {1,2,3};
set.Add(2); // ignored, already present
var q = new Queue<string>();
q.Enqueue("a"); var head = q.Dequeue();
var s = new Stack<int>();
s.Push(1); var top = s.Pop();
Pitfalls:
- Dictionary throws if key not present with indexer; use TryGetValue.
- Choose good Equals/GetHashCode implementations for keys.
Span<T> — stack-only, non-allocating type for contiguous memory slices.Memory<T> — heap-based, can be awaited and used across async boundaries.Use in performance-sensitive code to avoid allocations and copying.
Example:
Span<byte> buffer = stackalloc byte[256]; // stack memory
// operate on buffer without heap allocation
This section focuses on building blocks beyond the standard collections: when to implement them and how to use them in C#.
LinkedList<T> exists in BCL; implements a doubly-linked list.Example using LinkedList<T>:
var ll = new LinkedList<int>();
var node = ll.AddLast(1);
ll.AddAfter(node, 2);
ll.Remove(node);
Singly-linked list implementation (simple):
public class SinglyNode<T> { public T Value; public SinglyNode<T>? Next; }
public class SinglyLinkedList<T>
{
private SinglyNode<T>? head;
public void AddFirst(T value) { head = new SinglyNode<T>{ Value = value, Next = head }; }
public T? RemoveFirst() { if (head == null) return default; var val = head.Value; head = head.Next; return val; }
}
Tips: - Avoid using linked lists for cache-friendly workloads; arrays/Lists are often faster due to contiguous memory.
Binary search tree (BST) basic example:
public class TreeNode<T> where T : IComparable<T>
{
public T Value; public TreeNode<T>? Left; public TreeNode<T>? Right;
}
public class BinarySearchTree<T> where T : IComparable<T>
{
private TreeNode<T>? root;
public void Insert(T value) { root = InsertRec(root, value); }
private TreeNode<T> InsertRec(TreeNode<T>? node, T value)
{
if (node == null) return new TreeNode<T>{ Value = value };
if (value.CompareTo(node.Value) < 0) node.Left = InsertRec(node.Left, value);
else node.Right = InsertRec(node.Right, value);
return node;
}
}
Traversal: In-order (sorted), pre-order, post-order.
Notes:
- Self-balancing trees (AVL, Red-Black) are used in production for predictable performance.
- SortedSet<T> and SortedDictionary<TKey,TValue> implement tree-based collections in BCL.
Simple graph representation (adjacency list):
public class Graph
{
private readonly Dictionary<int, List<int>> _adj = new();
public void AddEdge(int u, int v) { if (!_adj.ContainsKey(u)) _adj[u] = new List<int>(); _adj[u].Add(v); }
public IEnumerable<int> Neighbors(int v) => _adj.TryGetValue(v, out var list) ? list : Enumerable.Empty<int>();
}
// BFS
public IEnumerable<int> BFS(int start)
{
var visited = new HashSet<int>();
var q = new Queue<int>();
q.Enqueue(start); visited.Add(start);
while (q.Count > 0) {
var v = q.Dequeue(); yield return v;
foreach(var n in Neighbors(v)) if (visited.Add(n)) q.Enqueue(n);
}
}
Pitfalls: - For weighted graphs use Dijkstra/A* algorithms; for negative weights use Bellman-Ford.
Dictionary<TKey,TValue> is a hash table using buckets; collisions handled via chaining.GetHashCode() and Equals() correctly for custom types.Example custom key:
public struct Point : IEquatable<Point>
{
public int X { get; }
public int Y { get; }
public override int GetHashCode() => HashCode.Combine(X, Y);
public bool Equals(Point other) => X == other.X && Y == other.Y;
}
Note: HashCode.Combine is available to make good composite hashes.
class — reference type with identity. Suitable for most domain objects, heavy state, and polymorphism.struct — value type, stack-allocated (or inlined). Prefer for small immutable types.record — reference type (in C# 9+) providing value-like equality semantics and concise syntax. Records can also be record struct.Examples:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public struct Point { public int X; public int Y; }
public record User(string Username, string Email);
When to use what:
- Use struct for small (<16 bytes commonly) and immutable types.
- Use record for DTOs/immutable data carriers where value equality is desired.
public class Config
{
// Auto-property
public string Name { get; set; }
// Read-only property with init-only setter (C#9+)
public string Id { get; init; }
// Constructor
public Config(string name) => Name = name;
}
Use private set or init to control mutability.
: syntax for inheritance and interface implementation.virtual/override for runtime polymorphism.Example:
public interface IRepository<T> { void Add(T item); }
public abstract class RepositoryBase<T> : IRepository<T>
{
public abstract void Add(T item);
}
public class MemoryRepository<T> : RepositoryBase<T>
{
private readonly List<T> _items = new();
public override void Add(T item) => _items.Add(item);
}
Polymorphism example:
public class Animal { public virtual string Speak() => "..."; }
public class Dog : Animal { public override string Speak() => "woof"; }
Animal a = new Dog(); Console.WriteLine(a.Speak()); // "woof"
Pitfalls: - Avoid deep inheritance hierarchies; prefer composition and explicit interfaces. - Virtual methods in constructors are dangerous (override called before derived constructor runs).
Modifiers: public, internal, protected, private, protected internal, private protected.
Rules: - Expose behaviour (methods) not internal state (fields). - Use properties with validation rather than public fields.
Example:
public class BankAccount
{
private decimal _balance;
public decimal Balance => _balance;
public void Deposit(decimal amt) { if (amt <= 0) throw new ArgumentException(); _balance += amt; }
}
IServiceCollection registration for ASP.NET or HostBuilder for generic hosts.Example DI registration (ASP.NET Core):
builder.Services.AddScoped<IUserService, UserService>();
usingCompany.Product.Module.using imports namespaces; prefer file-scoped using declarations where appropriate (C# 10+):namespace MyApp.Features;
using System.Collections.Generic;
Notes: - Avoid wildcard imports; prefer explicit naming to reduce ambiguity in large projects.
csproj*.csproj is the project descriptor for SDK-style projects in modern .NET.Minimal csproj example:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>
Major.Minor.Patch.This section aligns with .NET/C# community and Microsoft conventions and adds practical rules used in production.
PascalCase (e.g., UserRepository).PascalCase with I prefix (e.g., IRepository).PascalCase.camelCase._camelCase (leading underscore) or camelCase depending on team style.Constants: PascalCase (or ALL_CAPS rarely used in .NET).
Indentation: 4 spaces (no tabs).
public class Foo
{
public void Bar()
{
// code
}
}
/// <summary> for public APIs and include param/returns tags./// <summary>Gets the user by id.</summary>
/// <param name="id">User identifier.</param>
/// <returns>User object or null.</returns>
public User? GetUser(int id) { ... }
async/await for I/O-bound work and avoid Task.Run for CPU-bound operations in server code.ConfigureAwait(false) in library code (non-UI) where appropriate.IEnumerable<T>/IReadOnlyCollection<T> for read-only parameters to hide implementation details.foreach over for when readability wins; use indexes when you need them.null where possible; prefer nullable reference types feature (string?) and annotate APIs.StringBuilder for heavy string concatenation in loops. Use string.Concat or interpolation for light usage.Performance-specific:
- Use Span<T>/Memory<T> for zero-copy slices.
- Prefer struct for small value types; prefer class for entities and polymorphic behavior.
- Use ArrayPool<T> to reduce allocations in hot paths.
xUnit, NUnit, MSTest. Mocking: Moq, NSubstitute.WebApplicationFactory<TEntryPoint> for ASP.NET Core integration tests.dotnet format, Roslyn analyzers, FxCop/Microsoft.CodeAnalysis rules.dotnet-counters, dotnet-trace, perfcollect.Example test (xUnit):
public class CalculatorTests
{
[Fact]
public void Add_ReturnsSum()
{
var calc = new Calculator();
Assert.Equal(3, calc.Add(1,2));
}
}
Debugging tips:
- Use conditional breakpoints and tracepoints to collect runtime info without stopping.
- Use dotnet test --filter to run subsets of tests.
dotnet CLI for multi-platform builds.Interoperability:
- Use P/Invoke and DllImport to call native libraries.
- Use System.Text.Json for high-performance JSON; fallback to Newtonsoft.Json for advanced scenarios.
Deployment patterns:
- Containerize with mcr.microsoft.com/dotnet/aspnet or SDK images.
- Use CI/CD to build and publish NuGet artifacts and container images.
var name = user?.Profile?.Name ?? "(unknown)";
if (o is Person p) Console.WriteLine(p.Name);
switch (shape) { case Circle c: ...; break; }
using declaration (C#8+):using var conn = new SqlConnection(connStr);
ConfigureAwait(false) in library code (can cause deadlocks in certain sync contexts).IReadOnlyCollection<T> instead.IDisposable correctly (use SafeHandle and Dispose(bool) pattern when necessary).This guide follows the tone, structure, and style of other guides in this repository: header metadata, badges, an overview, contents list, thorough sections, code blocks, pitfalls, and practical recommendations.