Classes and interfaces

Properties, records, interfaces and polymorphism — the object model that domain types and services are built from.

A class

public class Account
{
    public string Owner { get; }                 // set in the constructor only
    public decimal Balance { get; private set; }

    public Account(string owner, decimal opening)
    {
        Owner = string.IsNullOrWhiteSpace(owner)
            ? throw new ArgumentException("owner is required", nameof(owner))
            : owner;
        Balance = opening;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount));
        Balance += amount;
    }

    public override string ToString() => $"{Owner}: {Balance:C}";
}
Member formMeaning
{ get; set; }A read and write auto-property
{ get; }Immutable afterwards; set in the constructor or inline
{ get; private set; }Readable everywhere, writable only inside the class
=> expression bodyA concise single-expression method or property
initSettable only while the object is being initialised
required (C# 11)The compiler enforces that the caller sets this member
// init-only and required make immutable construction ergonomic
public class User
{
    public required string Email { get; init; }
    public string DisplayName { get; init; } = "";
}

var u = new User { Email = "[email protected]", DisplayName = "Ada" };

// a record gives value equality and a readable ToString for free
public record Money(decimal Amount, string Currency)
{
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException("currency mismatch");
        return this with { Amount = Amount + other.Amount };   // a copy, updated
    }
}

var total = new Money(10m, "GBP").Add(new Money(2.5m, "GBP"));   // 12.5 GBP

Interfaces and inheritance

A class extends exactly one base class but may implement any number of interfaces. Depend on the interface so the implementation can be replaced, including by a test double.

public interface IShape
{
    double Area();
    string Name => GetType().Name;          // default interface member
}

public sealed class Circle(double radius) : IShape
{
    public double Area() => Math.PI * radius * radius;
}

public sealed class Rectangle(double w, double h) : IShape
{
    public double Area() => w * h;
}

public static class ShapeReport
{
    public static string Describe(IEnumerable<IShape> shapes)
    {
        var total = shapes.Sum(s => s.Area());
        return $"{shapes.Count()} shapes totalling {total:F2}";
    }
}

// pattern matching against the runtime type
static string Kind(IShape s) => s switch
{
    Circle c when c.Area() > 100 => "large circle",
    Circle                       => "circle",
    _                            => "other"
};
  • sealed prevents further inheritance and is a reasonable default for small concrete types.
  • abstract members have no body and force every derived class to supply one.
  • virtual members have a body and may be overridden; without it a derived method hides the base member instead of replacing it.
  • Interface names conventionally begin with I, and good ones describe a role (IRepository<T>) rather than mirroring a class.

Constructing an object graph

public interface IPricingService { decimal PriceFor(string sku); }

public class Checkout(IPricingService pricing, ILogger<Checkout> log)
{
    public decimal Total(IEnumerable<string> skus)
    {
        var total = skus.Sum(pricing.PriceFor);
        log.LogInformation("Priced {Count} items for {Total}", skus.Count(), total);
        return total;
    }
}

// registration in a .NET host
builder.Services.AddSingleton<IPricingService, PricingService>();
builder.Services.AddScoped<Checkout>();
⚠️
Never resolve a scoped service from a singleton: the singleton outlives the scope and ends up holding a disposed instance. If a long-lived component needs short-lived work, inject IServiceScopeFactory and create a scope for each operation.

FAQ

Class or record?
A record when the type is a value with structural equality, such as a DTO, an event or a configuration object. A class when the type has identity or mutable behaviour, such as a service or an entity tracked by a database context.
Abstract class or interface?
Prefer an interface when unrelated types share a role and there is no code to reuse. Choose an abstract base when implementations genuinely share state or a common constructor and you want to control the hierarchy.

Syntax and types LINQ and async/await

Last refreshed 2026-09-18.