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 form | Meaning |
|---|---|
{ 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 body | A concise single-expression method or property |
init | Settable 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 GBPInterfaces 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"
};sealedprevents further inheritance and is a reasonable default for small concrete types.abstractmembers have no body and force every derived class to supply one.virtualmembers 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.
Related
Syntax and types LINQ and async/await
Last refreshed 2026-09-18.