A class is a reference type that defines a blueprint for objects allocated on the managed heap. Multiple variables can reference the same object, so mutations through one reference are visible through all others — a property that enables shared state but also creates aliasing bugs when callers don’t expect it. Classes support single-class inheritance, virtual dispatch, finalizers, and the full range of access modifiers, making them the default choice for services, domain entities, and infrastructure types in C#. The key design decision is knowing when NOT to use a class: value-typed data carriers should be record struct or readonly struct (stack-allocated, no GC pressure), and pure data objects with value equality should be record class (auto-generated Equals/GetHashCode/==).

Deeper Explanation

Instances are heap-allocated and accessed through a reference stored on the stack (or inside another heap object). Assignment copies the reference, not the object:

public class Order
{
    public int Id { get; init; }
    public string Customer { get; set; } = string.Empty;
    public decimal Total { get; set; }
}
 
var a = new Order { Id = 1, Customer = "Acme", Total = 99.99m };
var b = a;          // b points to the SAME object
b.Total = 0m;
Console.WriteLine(a.Total); // 0 — both references share the object

Class Modifiers

abstract

An abstract class cannot be instantiated directly — it exists only to be inherited. It may contain abstract members (no body, must be overridden) and concrete members (shared implementation).

public abstract class Shape
{
    public string Color { get; set; } = "Black";
 
    // No body — every derived class MUST implement
    public abstract double Area();
 
    // Shared implementation — derived classes inherit as-is or override
    public virtual string Describe() => $"{Color} shape with area {Area():F2}";
}
 
public class Circle : Shape
{
    public double Radius { get; init; }
    public override double Area() => Math.PI * Radius * Radius;
}
 
// Shape s = new Shape();   // Compile error — cannot instantiate abstract class
Shape s = new Circle { Radius = 5 };

Key rules:

  • Can contain both abstract and non-abstract members.
  • Can have constructors (called by derived constructors via base(...)), fields, and state.
  • Cannot be sealed (the two modifiers are contradictory).
  • Cannot be static (abstract implies inheritance, static forbids it).

Abstract vs Interface: abstract classes carry state and shared implementation but lock you into single inheritance. Interfaces (especially with default interface methods in C# 8+) provide multiple implementation but cannot hold instance state.

sealed

A sealed class cannot be inherited. The compiler can devirtualize method calls on sealed types, enabling small performance gains.

public sealed class JwtToken
{
    public string Value { get; }
    public DateTime Expiry { get; }
 
    public JwtToken(string value, DateTime expiry)
    {
        Value = value;
        Expiry = expiry;
    }
 
    public bool IsExpired => DateTime.UtcNow > Expiry;
}
 
// class ExtendedToken : JwtToken { }  // Compile error — cannot inherit from sealed

You can also seal individual overrides to stop further overriding down the chain:

public class Base
{
    public virtual void Execute() { }
}
 
public class Middle : Base
{
    public sealed override void Execute() { /* final implementation */ }
}
 
// public class Bottom : Middle
// {
//     public override void Execute() { } // Compile error — Execute is sealed in Middle
// }

string is a sealed class in the BCL. All structs are implicitly sealed.

static

A static class cannot be instantiated or inherited. It can only contain static members. The compiler enforces this — you cannot add instance fields, properties, or methods.

public static class MathHelpers
{
    public static double Clamp(double value, double min, double max)
        => Math.Max(min, Math.Min(max, value));
 
    public static double Lerp(double a, double b, double t)
        => a + (b - a) * Clamp(t, 0, 1);
}
 
// var h = new MathHelpers();  // Compile error

Key rules:

  • Implicitly sealed and abstract at IL level (the CLR has no concept of “static class”).
  • Cannot implement interfaces or extend base classes (other than object).
  • Extension methods must be defined in a static, non-generic, non-nested class.
  • Static constructors run once, before the first member access, and are thread-safe by the runtime guarantee.

Gotcha: static classes are singletons by nature. If they hold mutable state (static fields), you get global mutable state — hard to test and prone to race conditions.

partial

The partial keyword splits a class definition across multiple files. The compiler merges them into a single type. Commonly used for separating generated code from hand-written code.

// Order.cs
public partial class Order
{
    public int Id { get; set; }
    public string Customer { get; set; } = string.Empty;
}
 
// Order.Validation.cs
public partial class Order
{
    public bool IsValid() => Id > 0 && !string.IsNullOrWhiteSpace(Customer);
}

Key rules:

  • All parts must use partial and have the same accessibility, type-parameter list, and enclosing namespace.
  • If any part is abstract, the whole type is abstract. Same for sealed and static.
  • Partial methods (C# 9+) can have implementations in another part; if no implementation is provided for a partial method without accessibility modifier, the compiler removes the call entirely.
  • Heavily used by source generators, EF Core scaffolding, WinForms/WPF designers, and Razor pages.
  • Also applies to structs, interfaces, and records.

Modifier Compatibility

Modifier combinationAllowed?
abstract + sealedNo in C# source (static is the IL equivalent)
abstract + staticNo
sealed + staticRedundant — static is already sealed
partial + any modifierYes
abstract + partialYes
sealed + partialYes

Modern Construction Features

  • Primary constructors (C# 12) — declare constructor parameters on the class header; they’re in scope for the whole body (field/property initializers, methods). Unlike record primary constructors, they do not auto-generate public properties — the parameters are captured as private state:

    public sealed class OrderService(IOrderRepository repo, ILogger<OrderService> log)
    {
        public Task SaveAsync(Order o) { log.LogInformation("saving"); return repo.SaveAsync(o); }
    }
  • required members (C# 11) force the caller to set a property in the object initializer even though it’s not a constructor parameter — compile-time enforcement without boilerplate constructors:

    public sealed class Customer { public required string Name { get; init; } }
    var c = new Customer { Name = "Acme" }; // omitting Name is a compile error
  • Constructor chaining with : this(...) reuses one constructor from another (and : base(...) calls the base). Initialization order matters: instance field initializers run before the constructor body; a static constructor runs once, lazily, before first use — and if it throws, the type is permanently unusable (TypeInitializationException on every later access).

  • file-scoped types (C# 11) (file class X) limit visibility to one source file — useful for source generators. The full access ladder is privateprivate protectedprotected/internalprotected internalpublic.

Pitfalls

  1. Reference equality surprise== compares references, not content. Two new Order(...) with identical fields are not equal unless you override ==/Equals. Use records or implement IEquatable<T> for value-like equality.

  2. Finalizer abuse — Finalizers delay GC (objects with finalizers are promoted to Gen 1+), are non-deterministic, and run on a single finalizer thread. Prefer IDisposable/IAsyncDisposable with using. Only add a finalizer as a safety net for unmanaged resources.

  3. Static mutable state — Static fields in any class (including non-static classes) are effectively global state. They survive GC, are shared across threads, and make unit testing painful. If you must use them, make them readonly or guard with lock/Interlocked.

  4. Abstract class tight coupling — Deriving from an abstract class couples you to its implementation details (constructor signature, protected fields, method call order). Changes in the base class can break all derived classes (fragile base class problem). Prefer interface contracts when you do not need shared state.

  5. Partial class hidden members — Source generators can add fields, methods, and interface implementations to your partial class that you do not see in your source file. Name collisions produce confusing compiler errors pointing at generated code.

Tradeoffs

DecisionOption AOption BWhen AWhen B
class vs record classRegular class (manual equality, mutable by default)Record class (value equality, with expressions, immutable by convention)Entities with identity semantics (two Order objects with same data are different if IDs differ), mutable state machines, servicesDTOs, events, messages, API responses — anywhere value equality is natural and immutability is preferred
abstract class vs interfaceAbstract class (shared state + implementation, single inheritance)Interface (multiple implementation, no instance state, default methods since C# 8)Need shared fields/constructors, template method pattern, protected stateNeed multiple implementations per type, or only defining a contract without shared state
sealed vs openSealed (no inheritance, enables devirtualization)Open (extensible)Leaf types, DTOs, types not designed for extension — sealed is safer defaultExplicitly designed for inheritance with documented extension points
static class vs singletonStatic class (no instance, no DI, no interface)Singleton via DI (services.AddSingleton<T>())Pure utility functions with no state and no need for testing isolationNeeds DI injection, interface-based testing, or configuration-dependent behavior

Decision rule: default to sealed class for new types (prevents accidental inheritance, enables compiler optimizations). Use record class for immutable data carriers. Use abstract class only when you need shared instance state across a type hierarchy — otherwise prefer interfaces.

Questions

References