11 questions foundLazy<T> defers creation of the instance until it's first accessed AND guarantees thread-safe initialization by default, making it the cleanest modern way to implement a Singleton without manually writing double-checked locking.
public class ConfigManager {
private static readonly Lazy<ConfigManager> _instance = new(() => new ConfigManager());
public static ConfigManager Instance => _instance.Value;
private ConfigManager() { }
}
Real-world example Ensuring a single, shared application configuration or logging instance across the entire application.
Common follow-ups: Why is a plain 'static readonly' field alone not always sufficient to guarantee thread-safe lazy initialization?
Multithreading & Task Parallel Library
A Factory Method encapsulates object-creation logic behind a method (often on a base class or interface) that subclasses or implementations override to decide WHICH concrete type to instantiate — this decouples the calling code from needing to know or reference specific concrete classes directly.
public abstract class ShapeFactory {
public abstract IShape CreateShape();
}
public class CircleFactory : ShapeFactory {
public override IShape CreateShape() => new Circle();
}
Real-world example Building a document-processing system where different factories create different parser implementations depending on file type.
Common follow-ups: How does the Factory Method pattern differ from the more general Abstract Factory pattern?
Interfaces & Abstract Classes
Define a common interface (or a delegate type) representing an interchangeable algorithm, implement multiple concrete strategies, and inject/pass the chosen one into the context class — modern C# often prefers a simple Func<> delegate over a full interface hierarchy for lightweight strategies.
// Interface-based
public interface IDiscountStrategy { decimal Apply(decimal price); }
public class PercentageDiscount : IDiscountStrategy {
public decimal Apply(decimal price) => price * 0.9m;
}
// Delegate-based (lighter weight)
Func<decimal, decimal> percentageDiscount = price => price * 0.9m;
Real-world example Supporting multiple interchangeable pricing, sorting, or validation strategies that can be swapped at runtime.
Common follow-ups: When would you prefer the interface-based approach over the simpler delegate-based one for the Strategy pattern?
Delegates Events & Lambdas
The Repository pattern defines an interface (like IRepository<T>) exposing domain-focused CRUD operations, hiding the underlying data access technology (Entity Framework, raw SQL, an external API) behind that abstraction — letting business logic depend on the abstraction rather than a specific persistence mechanism.
public interface IRepository<T> {
Task<T?> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
}
public class UserRepository : IRepository<User> {
// implementation using EF Core, backed by AppDbContext
}
Real-world example Decoupling business logic from Entity Framework specifics, making it possible to swap the data layer or mock it easily in tests.
Common follow-ups: Is the Repository pattern still considered necessary on top of Entity Framework Core's DbSet<T>, which already provides similar abstraction?
Dependency Injection & IoC Principles
Create a decorator class that implements the SAME interface as the object it wraps, holds a reference to the wrapped instance, and adds behavior before/after delegating the actual work to it — decorators can be chained/stacked to layer multiple behaviors independently.
public interface INotifier { void Send(string message); }
public class EmailNotifier : INotifier {
public void Send(string message) => Console.WriteLine($"Email: {message}");
}
public class LoggingNotifierDecorator : INotifier {
private readonly INotifier _inner;
public LoggingNotifierDecorator(INotifier inner) { _inner = inner; }
public void Send(string message) {
Console.WriteLine($"Logging: about to send '{message}'");
_inner.Send(message);
}
}
Real-world example Wrapping a core notification service with logging, retry logic, or caching behavior without modifying its original implementation.
Common follow-ups: How does the Decorator pattern differ from simply using inheritance to add the extra behavior?
Interfaces & Abstract Classes
A simple event-based Observer uses standard C# events (publisher raises an event, subscribers attach handlers) — straightforward but tightly coupled to the delegate/event model. IObservable<T>/IObserver<T> (the Reactive Extensions foundation) formalizes the pattern with an explicit Subscribe()/OnNext()/OnCompleted()/OnError() contract, better suited for composable, LINQ-style event stream processing.
// Event-based
public class StockTicker { public event Action<decimal>? PriceChanged; }
// IObservable-based (more composable, works with Rx operators)
public class StockTickerObservable : IObservable<decimal> {
public IDisposable Subscribe(IObserver<decimal> observer) { /* ... */ return Disposable.Empty; }
}
Real-world example Choosing plain events for a simple UI click handler versus IObservable<T> for a composable, filterable stream of live stock price updates.
Common follow-ups: What extra composability (like filtering or throttling) does Reactive Extensions (Rx.NET) add on top of raw IObservable<T>?
Delegates Events & Lambdas
Unit of Work tracks all changes made across multiple repositories during a business operation and commits them together in ONE atomic transaction (typically via SaveChanges() in EF Core) — this ensures related changes across different repositories either ALL succeed or ALL fail together, maintaining data consistency.
public interface IUnitOfWork : IDisposable {
IRepository<Order> Orders { get; }
IRepository<Customer> Customers { get; }
Task<int> SaveChangesAsync();
}
// Usage
unitOfWork.Orders.Add(newOrder);
unitOfWork.Customers.Update(customer);
await unitOfWork.SaveChangesAsync(); // both changes committed together, atomically
Real-world example Ensuring an order placement (which updates both an Orders table AND a Customer's loyalty points) either fully succeeds or fully rolls back together.
Common follow-ups: Does Entity Framework Core's DbContext already implement the Unit of Work pattern internally, making a separate abstraction sometimes redundant?
Dependency Injection & IoC Principles
CQRS separates WRITE operations (Commands, which change state and typically return little/nothing) from READ operations (Queries, which return data without side effects) into distinct request/handler pairs — often implemented with a mediator pattern where each Command/Query has its own dedicated Handler class, keeping business logic focused and independently testable.
public record CreateOrderCommand(int CustomerId, List<OrderItem> Items) : IRequest<int>;
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, int> {
public async Task<int> Handle(CreateOrderCommand command, CancellationToken ct) {
var order = new Order(command.CustomerId, command.Items);
await _repository.AddAsync(order);
return order.Id;
}
}
Real-world example Structuring a complex domain's write and read operations into clearly separated, independently-scalable handler classes.
Common follow-ups: What benefits does CQRS provide specifically in systems with very different read versus write scaling requirements?
Records & Pattern Matching
Define a builder class with methods that each set part of the object being built and RETURN 'this' (the builder itself), enabling method chaining; a final Build() method assembles and returns the fully-configured object — useful when an object has many optional configuration parameters that would otherwise require an unwieldy constructor with many arguments.
public class EmailBuilder {
private readonly Email _email = new();
public EmailBuilder To(string address) { _email.To = address; return this; }
public EmailBuilder Subject(string subject) { _email.Subject = subject; return this; }
public Email Build() => _email;
}
var email = new EmailBuilder().To("sam@x.com").Subject("Hello").Build();
Real-world example Constructing a complex configuration object (like an HTTP request or an email message) with many optional parameters readably.
Common follow-ups: How do C# 12's collection expressions or object initializer syntax sometimes reduce the NEED for a full Builder pattern for simpler cases?
Records & Pattern Matching
Each handler holds a reference to the NEXT handler in the chain; it either processes the request itself and stops, or passes it along to the next handler if it can't handle it — commonly implemented in ASP.NET Core's own middleware pipeline, which is essentially a real-world Chain of Responsibility implementation.
public abstract class ValidationHandler {
protected ValidationHandler? Next;
public ValidationHandler SetNext(ValidationHandler next) { Next = next; return next; }
public abstract bool Handle(Order order);
}
public class CreditCheckHandler : ValidationHandler {
public override bool Handle(Order order) =>
CheckCredit(order) && (Next?.Handle(order) ?? true);
}
Real-world example Building a multi-step validation pipeline for an order (credit check, inventory check, fraud check) where each step can short-circuit the chain.
Common follow-ups: How does ASP.NET Core's own middleware pipeline (app.Use(...)) directly mirror this exact pattern?
Interfaces & Abstract Classes