Behavioral Design Patterns
Behavioral patterns deal with algorithms and the assignment of responsibilities between objects. They describe patterns of communication between objects.
Chain of Responsibility
Intent: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
π Theory & When to Use
When to Use
- More than one object may handle a request, and the handler isnβt known a priori
- You want to issue a request to one of several objects without specifying the receiver explicitly
- The set of objects that can handle a request should be specified dynamically
Real-World Analogy
Technical support escalation: Level 1 support tries to solve the issue. If they canβt, it goes to Level 2, then Level 3, etc. Each level either handles the request or passes it up the chain.
Key Participants
- Handler: Interface for handling requests; optionally implements successor link
- ConcreteHandler: Handles requests itβs responsible for; can access successor
- Client: Initiates the request to a handler on the chain
π UML Diagram
βββββββββββββββββββββββββββ
β <<interface>> β
β IHandler β
βββββββββββββββββββββββββββ€
β + SetNext(IHandler) β
β + Handle(Request) β
ββββββββββββββ²βββββββββββββ
β
ββββββββββββββ΄βββββββββββββ
β AbstractHandler β
βββββββββββββββββββββββββββ€
β - nextHandler: IHandler β
βββββββββββββββββββββββββββ€
β + SetNext(IHandler) β
β + Handle(Request) β
ββββββββββββββ²βββββββββββββ
β
ββββββββββ΄βββββββββ
β β
βββββ΄ββββ βββββ΄ββββ
βHandlerβ βHandlerβ
β A ββββββββ>β B ββββββββ> ...
βββββββββ next βββββββββ
π» Short Example (~20 lines)
public interface IHandler
{
IHandler SetNext(IHandler handler);
string Handle(string request);
}
public abstract class AbstractHandler : IHandler
{
private IHandler? _next;
public IHandler SetNext(IHandler handler) { _next = handler; return handler; }
public virtual string Handle(string request) => _next?.Handle(request) ?? $"Unhandled: {request}";
}
public class DogHandler : AbstractHandler
{
public override string Handle(string request) =>
request == "MeatBall" ? "Dog: I'll eat the MeatBall" : base.Handle(request);
}
public class CatHandler : AbstractHandler
{
public override string Handle(string request) =>
request == "Fish" ? "Cat: I'll eat the Fish" : base.Handle(request);
}
// Usage
var dog = new DogHandler();
var cat = new CatHandler();
dog.SetNext(cat);
Console.WriteLine(dog.Handle("Fish")); // Cat: I'll eat the Fish
π» Medium Example (~50 lines)
// Request class
public class PurchaseRequest
{
public string Description { get; }
public decimal Amount { get; }
public PurchaseRequest(string description, decimal amount) => (Description, Amount) = (description, amount);
}
// Handler interface
public interface IApprover
{
IApprover? Successor { get; set; }
void ProcessRequest(PurchaseRequest request);
}
// Abstract handler
public abstract class Approver : IApprover
{
public IApprover? Successor { get; set; }
protected string Name { get; }
protected decimal ApprovalLimit { get; }
protected Approver(string name, decimal limit) => (Name, ApprovalLimit) = (name, limit);
public virtual void ProcessRequest(PurchaseRequest request)
{
if (request.Amount <= ApprovalLimit)
Console.WriteLine($"{Name} approved ${request.Amount} for {request.Description}");
else if (Successor != null)
Successor.ProcessRequest(request);
else
Console.WriteLine($"Request for ${request.Amount} requires board approval");
}
}
// Concrete handlers
public class TeamLead : Approver
{
public TeamLead() : base("Team Lead", 1000) { }
}
public class Manager : Approver
{
public Manager() : base("Manager", 5000) { }
}
public class Director : Approver
{
public Director() : base("Director", 20000) { }
}
// Usage
var teamLead = new TeamLead();
var manager = new Manager();
var director = new Director();
teamLead.Successor = manager;
manager.Successor = director;
teamLead.ProcessRequest(new PurchaseRequest("Office supplies", 500)); // Team Lead approved
teamLead.ProcessRequest(new PurchaseRequest("New laptop", 2500)); // Manager approved
teamLead.ProcessRequest(new PurchaseRequest("Server upgrade", 15000)); // Director approved
teamLead.ProcessRequest(new PurchaseRequest("New building", 500000)); // Requires board
π» Production-Grade Example
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
// Request and result types
public record ValidationRequest(
string Email,
string Password,
string? PhoneNumber,
Dictionary<string, object> Metadata
);
public record ValidationResult(
bool IsValid,
List<string> Errors,
Dictionary<string, object> Context
)
{
public static ValidationResult Success(Dictionary<string, object>? context = null) =>
new(true, new List<string>(), context ?? new Dictionary<string, object>());
public static ValidationResult Failure(string error) =>
new(false, new List<string> { error }, new Dictionary<string, object>());
public static ValidationResult Failure(IEnumerable<string> errors) =>
new(false, errors.ToList(), new Dictionary<string, object>());
public ValidationResult AddError(string error)
{
Errors.Add(error);
return this with { IsValid = false };
}
}
// Handler interface with async support
public interface IValidationHandler
{
IValidationHandler? Next { get; set; }
Task<ValidationResult> HandleAsync(ValidationRequest request, CancellationToken ct = default);
}
// Base handler
public abstract class ValidationHandler : IValidationHandler
{
public IValidationHandler? Next { get; set; }
protected readonly ILogger Logger;
protected ValidationHandler(ILogger logger) => Logger = logger;
public virtual async Task<ValidationResult> HandleAsync(ValidationRequest request, CancellationToken ct = default)
{
var result = await ValidateAsync(request, ct);
if (!result.IsValid)
{
Logger.LogWarning("Validation failed at {Handler}: {Errors}",
GetType().Name, string.Join(", ", result.Errors));
return result;
}
if (Next != null)
{
return await Next.HandleAsync(request, ct);
}
return result;
}
protected abstract Task<ValidationResult> ValidateAsync(ValidationRequest request, CancellationToken ct);
}
// Concrete handlers
public class EmailValidationHandler : ValidationHandler
{
private static readonly Regex EmailRegex = new(
@"^[^@\s]+@[^@\s]+\.[^@\s]+$",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
public EmailValidationHandler(ILogger<EmailValidationHandler> logger) : base(logger) { }
protected override Task<ValidationResult> ValidateAsync(ValidationRequest request, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(request.Email))
return Task.FromResult(ValidationResult.Failure("Email is required"));
if (!EmailRegex.IsMatch(request.Email))
return Task.FromResult(ValidationResult.Failure("Invalid email format"));
Logger.LogDebug("Email validation passed for {Email}", request.Email);
return Task.FromResult(ValidationResult.Success());
}
}
public class PasswordValidationHandler : ValidationHandler
{
private readonly PasswordOptions _options;
public PasswordValidationHandler(ILogger<PasswordValidationHandler> logger, PasswordOptions? options = null)
: base(logger)
{
_options = options ?? new PasswordOptions();
}
protected override Task<ValidationResult> ValidateAsync(ValidationRequest request, CancellationToken ct)
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(request.Password))
{
errors.Add("Password is required");
return Task.FromResult(ValidationResult.Failure(errors));
}
if (request.Password.Length < _options.MinLength)
errors.Add($"Password must be at least {_options.MinLength} characters");
if (_options.RequireUppercase && !request.Password.Any(char.IsUpper))
errors.Add("Password must contain at least one uppercase letter");
if (_options.RequireLowercase && !request.Password.Any(char.IsLower))
errors.Add("Password must contain at least one lowercase letter");
if (_options.RequireDigit && !request.Password.Any(char.IsDigit))
errors.Add("Password must contain at least one digit");
if (_options.RequireSpecialChar && !request.Password.Any(c => !char.IsLetterOrDigit(c)))
errors.Add("Password must contain at least one special character");
return Task.FromResult(errors.Any()
? ValidationResult.Failure(errors)
: ValidationResult.Success());
}
}
public class DuplicateCheckHandler : ValidationHandler
{
private readonly IUserRepository _userRepository;
public DuplicateCheckHandler(ILogger<DuplicateCheckHandler> logger, IUserRepository userRepository)
: base(logger)
{
_userRepository = userRepository;
}
protected override async Task<ValidationResult> ValidateAsync(ValidationRequest request, CancellationToken ct)
{
var existingUser = await _userRepository.GetByEmailAsync(request.Email, ct);
if (existingUser != null)
return ValidationResult.Failure("Email is already registered");
return ValidationResult.Success();
}
}
public class RateLimitHandler : ValidationHandler
{
private readonly IRateLimiter _rateLimiter;
public RateLimitHandler(ILogger<RateLimitHandler> logger, IRateLimiter rateLimiter)
: base(logger)
{
_rateLimiter = rateLimiter;
}
protected override async Task<ValidationResult> ValidateAsync(ValidationRequest request, CancellationToken ct)
{
var clientId = request.Metadata.GetValueOrDefault("ClientIp")?.ToString() ?? "unknown";
if (!await _rateLimiter.TryAcquireAsync(clientId, ct))
return ValidationResult.Failure("Too many requests. Please try again later.");
return ValidationResult.Success();
}
}
// Configuration
public record PasswordOptions
{
public int MinLength { get; init; } = 8;
public bool RequireUppercase { get; init; } = true;
public bool RequireLowercase { get; init; } = true;
public bool RequireDigit { get; init; } = true;
public bool RequireSpecialChar { get; init; } = false;
}
// Interfaces for dependencies
public interface IUserRepository
{
Task<object?> GetByEmailAsync(string email, CancellationToken ct = default);
}
public interface IRateLimiter
{
Task<bool> TryAcquireAsync(string clientId, CancellationToken ct = default);
}
// Chain builder
public class ValidationChainBuilder
{
private readonly List<Func<IServiceProvider, IValidationHandler>> _handlerFactories = new();
public ValidationChainBuilder Add<THandler>() where THandler : IValidationHandler
{
_handlerFactories.Add(sp => sp.GetRequiredService<THandler>());
return this;
}
public IValidationHandler Build(IServiceProvider serviceProvider)
{
if (_handlerFactories.Count == 0)
throw new InvalidOperationException("No handlers configured");
var handlers = _handlerFactories.Select(f => f(serviceProvider)).ToList();
for (int i = 0; i < handlers.Count - 1; i++)
{
handlers[i].Next = handlers[i + 1];
}
return handlers[0];
}
}
// DI Registration
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddValidationChain(this IServiceCollection services)
{
services.AddScoped<EmailValidationHandler>();
services.AddScoped<PasswordValidationHandler>();
services.AddScoped<DuplicateCheckHandler>();
services.AddScoped<RateLimitHandler>();
services.AddScoped<IValidationHandler>(sp =>
{
var builder = new ValidationChainBuilder()
.Add<RateLimitHandler>()
.Add<EmailValidationHandler>()
.Add<PasswordValidationHandler>()
.Add<DuplicateCheckHandler>();
return builder.Build(sp);
});
return services;
}
}
// Usage
public class RegistrationService
{
private readonly IValidationHandler _validationChain;
private readonly ILogger<RegistrationService> _logger;
public RegistrationService(IValidationHandler validationChain, ILogger<RegistrationService> logger)
{
_validationChain = validationChain;
_logger = logger;
}
public async Task<RegistrationResult> RegisterAsync(RegistrationRequest request, CancellationToken ct = default)
{
var validationRequest = new ValidationRequest(
request.Email,
request.Password,
request.PhoneNumber,
new Dictionary<string, object> { ["ClientIp"] = request.ClientIp }
);
var result = await _validationChain.HandleAsync(validationRequest, ct);
if (!result.IsValid)
{
return new RegistrationResult(false, result.Errors);
}
// Continue with registration...
return new RegistrationResult(true, new List<string>());
}
}
public record RegistrationRequest(string Email, string Password, string? PhoneNumber, string ClientIp);
public record RegistrationResult(bool Success, List<string> Errors);
β Interview Q&A
Q1: Whatβs the difference between Chain of Responsibility and Decorator? A1: Both chain objects, but Decorator adds behavior to every call in sequence. Chain of Responsibility passes a request until ONE handler processes it and stops. Decorator enhances; Chain routes.
Q2: How does ASP.NET Core middleware relate to Chain of Responsibility? A2: Middleware IS Chain of Responsibility. Each middleware can handle the request, modify it, or pass to the next. The order matters, and any middleware can short-circuit the chain.
Q3: What happens if no handler processes the request? A3: Design choice: return a default response, throw an exception, or have a catch-all handler at the end. In production, always handle the βno handlerβ case explicitly.
Q4: How do you decide handler order? A4: Performance-critical handlers (rate limiting) go first, quick-fail validations before expensive ones, authentication before authorization. Order affects both correctness and performance.
Command
Intent: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
π Theory & When to Use
When to Use
- Parameterize objects with an action to perform
- Specify, queue, and execute requests at different times
- Support undo/redo operations
- Support logging changes so they can be reapplied after a crash
- Structure a system around high-level operations built on primitive operations
Real-World Analogy
Restaurant orders: the waiter (Invoker) takes your order (Command), writes it down, and passes it to the kitchen (Receiver). The order encapsulates everything needed to prepare the dish.
Key Participants
- Command: Interface declaring Execute method
- ConcreteCommand: Binds a Receiver to an action
- Receiver: Knows how to perform the operations
- Invoker: Asks the command to execute the request
- Client: Creates ConcreteCommand and sets its Receiver
π UML Diagram
βββββββββββββββ βββββββββββββββββββ
β Client β β Invoker β
ββββββββ¬βββββββ βββββββββββββββββββ€
β β - command β
β β + SetCommand() β
β β + Execute() β
β ββββββββββ¬βββββββββ
β β
βΌ βΌ
βββββββββββββββββββ βββββββββββββββββββ
β <<interface>> β β ConcreteCommand β
β ICommand β<ββββΌββββββββββββββββββ€
βββββββββββββββββββ€ β - receiver β
β + Execute() β β - state β
β + Undo() β β + Execute() β
βββββββββββββββββββ β + Undo() β
ββββββββββ¬βββββββββ
β
βΌ
βββββββββββββββββββ
β Receiver β
βββββββββββββββββββ€
β + Action() β
βββββββββββββββββββ
π» Short Example (~20 lines)
public interface ICommand
{
void Execute();
void Undo();
}
public class Light
{
public void On() => Console.WriteLine("Light is ON");
public void Off() => Console.WriteLine("Light is OFF");
}
public class LightOnCommand : ICommand
{
private readonly Light _light;
public LightOnCommand(Light light) => _light = light;
public void Execute() => _light.On();
public void Undo() => _light.Off();
}
// Usage
var light = new Light();
var command = new LightOnCommand(light);
command.Execute(); // Light is ON
command.Undo(); // Light is OFF
π» Medium Example (~50 lines)
// Command interface
public interface ICommand
{
void Execute();
void Undo();
}
// Receiver
public class TextEditor
{
public StringBuilder Content { get; } = new();
public int CursorPosition { get; set; }
public void Insert(string text) => Content.Insert(CursorPosition, text);
public void Delete(int start, int length) => Content.Remove(start, length);
public override string ToString() => Content.ToString();
}
// Concrete commands
public class InsertTextCommand : ICommand
{
private readonly TextEditor _editor;
private readonly string _text;
private readonly int _position;
public InsertTextCommand(TextEditor editor, string text)
{
_editor = editor;
_text = text;
_position = editor.CursorPosition;
}
public void Execute()
{
_editor.CursorPosition = _position;
_editor.Insert(_text);
_editor.CursorPosition = _position + _text.Length;
}
public void Undo()
{
_editor.Delete(_position, _text.Length);
_editor.CursorPosition = _position;
}
}
// Invoker with history
public class CommandManager
{
private readonly Stack<ICommand> _history = new();
private readonly Stack<ICommand> _redoStack = new();
public void Execute(ICommand command)
{
command.Execute();
_history.Push(command);
_redoStack.Clear();
}
public void Undo()
{
if (_history.TryPop(out var command))
{
command.Undo();
_redoStack.Push(command);
}
}
public void Redo()
{
if (_redoStack.TryPop(out var command))
{
command.Execute();
_history.Push(command);
}
}
}
// Usage
var editor = new TextEditor();
var manager = new CommandManager();
manager.Execute(new InsertTextCommand(editor, "Hello "));
manager.Execute(new InsertTextCommand(editor, "World"));
Console.WriteLine(editor); // Hello World
manager.Undo();
Console.WriteLine(editor); // Hello
manager.Redo();
Console.WriteLine(editor); // Hello World
π» Production-Grade Example
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Text.Json;
// Command interfaces
public interface ICommand
{
string Id { get; }
string Name { get; }
DateTime CreatedAt { get; }
}
public interface ICommand<TResult> : ICommand
{
Task<TResult> ExecuteAsync(CancellationToken ct = default);
}
public interface IUndoableCommand<TResult> : ICommand<TResult>
{
Task UndoAsync(CancellationToken ct = default);
bool CanUndo { get; }
}
// Base command
public abstract class CommandBase<TResult> : ICommand<TResult>
{
public string Id { get; } = Guid.NewGuid().ToString("N")[..8];
public abstract string Name { get; }
public DateTime CreatedAt { get; } = DateTime.UtcNow;
public abstract Task<TResult> ExecuteAsync(CancellationToken ct = default);
}
// Command result
public record CommandResult<T>(
bool Success,
T? Data,
string? Error = null,
Dictionary<string, object>? Metadata = null
)
{
public static CommandResult<T> Ok(T data) => new(true, data);
public static CommandResult<T> Fail(string error) => new(false, default, error);
}
// Domain commands
public class CreateOrderCommand : CommandBase<CommandResult<Order>>, IUndoableCommand<CommandResult<Order>>
{
private readonly IOrderRepository _repository;
private readonly IEventPublisher _eventPublisher;
private readonly ILogger<CreateOrderCommand> _logger;
public string CustomerId { get; }
public List<OrderItem> Items { get; }
public override string Name => "CreateOrder";
public bool CanUndo => _createdOrder != null;
private Order? _createdOrder;
public CreateOrderCommand(
IOrderRepository repository,
IEventPublisher eventPublisher,
ILogger<CreateOrderCommand> logger,
string customerId,
List<OrderItem> items)
{
_repository = repository;
_eventPublisher = eventPublisher;
_logger = logger;
CustomerId = customerId;
Items = items;
}
public override async Task<CommandResult<Order>> ExecuteAsync(CancellationToken ct = default)
{
try
{
_logger.LogInformation("Creating order for customer {CustomerId}", CustomerId);
var order = new Order
{
Id = Guid.NewGuid().ToString(),
CustomerId = CustomerId,
Items = Items,
Status = OrderStatus.Created,
CreatedAt = DateTime.UtcNow
};
await _repository.CreateAsync(order, ct);
_createdOrder = order;
await _eventPublisher.PublishAsync(new OrderCreatedEvent(order.Id, CustomerId), ct);
_logger.LogInformation("Order {OrderId} created successfully", order.Id);
return CommandResult<Order>.Ok(order);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create order for customer {CustomerId}", CustomerId);
return CommandResult<Order>.Fail(ex.Message);
}
}
public async Task UndoAsync(CancellationToken ct = default)
{
if (_createdOrder == null)
throw new InvalidOperationException("Cannot undo - order was not created");
_logger.LogInformation("Undoing order creation: {OrderId}", _createdOrder.Id);
await _repository.DeleteAsync(_createdOrder.Id, ct);
await _eventPublisher.PublishAsync(new OrderCancelledEvent(_createdOrder.Id), ct);
}
}
public class UpdateOrderStatusCommand : CommandBase<CommandResult<Order>>, IUndoableCommand<CommandResult<Order>>
{
private readonly IOrderRepository _repository;
private readonly ILogger<UpdateOrderStatusCommand> _logger;
public string OrderId { get; }
public OrderStatus NewStatus { get; }
public override string Name => "UpdateOrderStatus";
public bool CanUndo => _previousStatus.HasValue;
private OrderStatus? _previousStatus;
public UpdateOrderStatusCommand(
IOrderRepository repository,
ILogger<UpdateOrderStatusCommand> logger,
string orderId,
OrderStatus newStatus)
{
_repository = repository;
_logger = logger;
OrderId = orderId;
NewStatus = newStatus;
}
public override async Task<CommandResult<Order>> ExecuteAsync(CancellationToken ct = default)
{
var order = await _repository.GetByIdAsync(OrderId, ct);
if (order == null)
return CommandResult<Order>.Fail($"Order {OrderId} not found");
_previousStatus = order.Status;
order.Status = NewStatus;
order.UpdatedAt = DateTime.UtcNow;
await _repository.UpdateAsync(order, ct);
_logger.LogInformation("Order {OrderId} status changed: {OldStatus} -> {NewStatus}",
OrderId, _previousStatus, NewStatus);
return CommandResult<Order>.Ok(order);
}
public async Task UndoAsync(CancellationToken ct = default)
{
if (!_previousStatus.HasValue)
throw new InvalidOperationException("Cannot undo - no previous status recorded");
var order = await _repository.GetByIdAsync(OrderId, ct);
if (order != null)
{
order.Status = _previousStatus.Value;
await _repository.UpdateAsync(order, ct);
}
}
}
// Command dispatcher with logging and history
public interface ICommandDispatcher
{
Task<TResult> DispatchAsync<TResult>(ICommand<TResult> command, CancellationToken ct = default);
Task UndoLastAsync(CancellationToken ct = default);
IReadOnlyList<ICommand> GetHistory();
}
public class CommandDispatcher : ICommandDispatcher
{
private readonly ILogger<CommandDispatcher> _logger;
private readonly ICommandStore _commandStore;
private readonly List<ICommand> _history = new();
private readonly SemaphoreSlim _lock = new(1, 1);
public CommandDispatcher(ILogger<CommandDispatcher> logger, ICommandStore commandStore)
{
_logger = logger;
_commandStore = commandStore;
}
public async Task<TResult> DispatchAsync<TResult>(ICommand<TResult> command, CancellationToken ct = default)
{
await _lock.WaitAsync(ct);
try
{
_logger.LogInformation("Dispatching command: {CommandName} ({CommandId})",
command.Name, command.Id);
var sw = Stopwatch.StartNew();
var result = await command.ExecuteAsync(ct);
sw.Stop();
// Store command for audit
await _commandStore.SaveAsync(new CommandRecord
{
Id = command.Id,
Name = command.Name,
ExecutedAt = DateTime.UtcNow,
DurationMs = sw.ElapsedMilliseconds,
Payload = JsonSerializer.Serialize(command)
}, ct);
_history.Add(command);
_logger.LogInformation("Command {CommandId} completed in {DurationMs}ms",
command.Id, sw.ElapsedMilliseconds);
return result;
}
finally
{
_lock.Release();
}
}
public async Task UndoLastAsync(CancellationToken ct = default)
{
await _lock.WaitAsync(ct);
try
{
if (_history.Count == 0)
throw new InvalidOperationException("No commands to undo");
var lastCommand = _history[^1];
if (lastCommand is IUndoableCommand<object> undoable && undoable.CanUndo)
{
await undoable.UndoAsync(ct);
_history.RemoveAt(_history.Count - 1);
_logger.LogInformation("Undid command: {CommandName}", lastCommand.Name);
}
else
{
throw new InvalidOperationException($"Command {lastCommand.Name} cannot be undone");
}
}
finally
{
_lock.Release();
}
}
public IReadOnlyList<ICommand> GetHistory() => _history.AsReadOnly();
}
// Supporting types
public class Order
{
public string Id { get; set; } = "";
public string CustomerId { get; set; } = "";
public List<OrderItem> Items { get; set; } = new();
public OrderStatus Status { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
public record OrderItem(string ProductId, int Quantity, decimal UnitPrice);
public enum OrderStatus { Created, Confirmed, Shipped, Delivered, Cancelled }
public record OrderCreatedEvent(string OrderId, string CustomerId);
public record OrderCancelledEvent(string OrderId);
public record CommandRecord
{
public string Id { get; init; } = "";
public string Name { get; init; } = "";
public DateTime ExecutedAt { get; init; }
public long DurationMs { get; init; }
public string Payload { get; init; } = "";
}
// Interfaces
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(string id, CancellationToken ct = default);
Task CreateAsync(Order order, CancellationToken ct = default);
Task UpdateAsync(Order order, CancellationToken ct = default);
Task DeleteAsync(string id, CancellationToken ct = default);
}
public interface IEventPublisher
{
Task PublishAsync<T>(T @event, CancellationToken ct = default);
}
public interface ICommandStore
{
Task SaveAsync(CommandRecord record, CancellationToken ct = default);
}
// DI Registration
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddCommandPattern(this IServiceCollection services)
{
services.AddScoped<ICommandDispatcher, CommandDispatcher>();
// Register repositories, event publishers, etc.
return services;
}
}
β Interview Q&A
Q1: Whatβs the relationship between Command and CQRS? A1: CQRS separates Commands (writes) from Queries (reads). The Command pattern provides structure for the Command side - encapsulating mutations as objects that can be validated, logged, and potentially undone.
Q2: How do you handle Command failures in a distributed system? A2: Use compensating transactions (Saga pattern), idempotency keys for retries, command sourcing (store commands, not just state), and eventual consistency with event publishing.
Q3: When would you NOT use the Command pattern? A3: For simple CRUD operations without undo/logging needs, for read operations (use Query objects instead), or when the overhead of command objects isnβt justified by the benefits.
Q4: How does Command enable macro recording? A4: Store executed commands in sequence. To replay: iterate and execute. To save macros: serialize the command list. Commands encapsulate all needed data for replay.
Iterator
Intent: Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
π Theory & When to Use
When to Use
- Access an aggregateβs contents without exposing internal structure
- Support multiple traversals of aggregates
- Provide a uniform interface for traversing different structures
Real-World Analogy
A TV remoteβs channel buttons: you can go next/previous through channels without knowing how channels are stored internally (array, linked list, database).
Key Participants
- Iterator: Interface for accessing and traversing elements
- ConcreteIterator: Implements the Iterator interface, tracks current position
- Aggregate: Interface for creating an Iterator
- ConcreteAggregate: Returns an instance of ConcreteIterator
.NET Implementation
.NET has built-in iterator support via IEnumerable<T> and IEnumerator<T>. The yield keyword simplifies iterator creation.
π UML Diagram
βββββββββββββββββββββββ βββββββββββββββββββββββ
β <<interface>> β β <<interface>> β
β IEnumerable<T> β β IEnumerator<T> β
βββββββββββββββββββββββ€ βββββββββββββββββββββββ€
β + GetEnumerator() β β + Current: T β
ββββββββββββ²βββββββββββ β + MoveNext(): bool β
β β + Reset() β
ββββββββββββ΄βββββββββββ ββββββββββββ²βββββββββββ
β ConcreteAggregate β β
βββββββββββββββββββββββ€ ββββββββββββ΄βββββββββββ
β - items: T[] βββββ>β ConcreteIterator β
β + GetEnumerator() β βββββββββββββββββββββββ€
βββββββββββββββββββββββ β - position: int β
β - aggregate β
βββββββββββββββββββββββ
π» Short Example (~20 lines)
// Using yield return (the .NET way)
public class NumberRange : IEnumerable<int>
{
private readonly int _start, _end;
public NumberRange(int start, int end) => (_start, _end) = (start, end);
public IEnumerator<int> GetEnumerator()
{
for (int i = _start; i <= _end; i++)
yield return i;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
// Usage
var range = new NumberRange(1, 5);
foreach (var num in range)
Console.Write($"{num} "); // 1 2 3 4 5
// LINQ works automatically
Console.WriteLine(range.Where(x => x % 2 == 0).Sum()); // 6
π» Medium Example (~50 lines)
// Custom collection with multiple iteration strategies
public class BinaryTree<T> : IEnumerable<T>
{
public T Value { get; }
public BinaryTree<T>? Left { get; set; }
public BinaryTree<T>? Right { get; set; }
public BinaryTree(T value) => Value = value;
// Default: In-order traversal
public IEnumerator<T> GetEnumerator() => InOrder().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
// In-order: Left, Root, Right
public IEnumerable<T> InOrder()
{
if (Left != null)
foreach (var item in Left.InOrder())
yield return item;
yield return Value;
if (Right != null)
foreach (var item in Right.InOrder())
yield return item;
}
// Pre-order: Root, Left, Right
public IEnumerable<T> PreOrder()
{
yield return Value;
if (Left != null)
foreach (var item in Left.PreOrder())
yield return item;
if (Right != null)
foreach (var item in Right.PreOrder())
yield return item;
}
// Post-order: Left, Right, Root
public IEnumerable<T> PostOrder()
{
if (Left != null)
foreach (var item in Left.PostOrder())
yield return item;
if (Right != null)
foreach (var item in Right.PostOrder())
yield return item;
yield return Value;
}
// Level-order (BFS)
public IEnumerable<T> LevelOrder()
{
var queue = new Queue<BinaryTree<T>>();
queue.Enqueue(this);
while (queue.Count > 0)
{
var node = queue.Dequeue();
yield return node.Value;
if (node.Left != null) queue.Enqueue(node.Left);
if (node.Right != null) queue.Enqueue(node.Right);
}
}
}
// Usage
var tree = new BinaryTree<int>(4)
{
Left = new BinaryTree<int>(2) { Left = new BinaryTree<int>(1), Right = new BinaryTree<int>(3) },
Right = new BinaryTree<int>(6) { Left = new BinaryTree<int>(5), Right = new BinaryTree<int>(7) }
};
Console.WriteLine(string.Join(", ", tree.InOrder())); // 1, 2, 3, 4, 5, 6, 7
Console.WriteLine(string.Join(", ", tree.PreOrder())); // 4, 2, 1, 3, 6, 5, 7
Console.WriteLine(string.Join(", ", tree.LevelOrder())); // 4, 2, 6, 1, 3, 5, 7
π» Production-Grade Example
using System.Runtime.CompilerServices;
// Async iterator for paginated API results
public interface IPagedResult<T>
{
IReadOnlyList<T> Items { get; }
int TotalCount { get; }
string? ContinuationToken { get; }
bool HasMore { get; }
}
public record PagedResult<T>(
IReadOnlyList<T> Items,
int TotalCount,
string? ContinuationToken
) : IPagedResult<T>
{
public bool HasMore => ContinuationToken != null;
}
// Async enumerable wrapper for paginated APIs
public class PaginatedAsyncEnumerable<T> : IAsyncEnumerable<T>
{
private readonly Func<string?, CancellationToken, Task<IPagedResult<T>>> _pageLoader;
private readonly int _prefetchPages;
public PaginatedAsyncEnumerable(
Func<string?, CancellationToken, Task<IPagedResult<T>>> pageLoader,
int prefetchPages = 1)
{
_pageLoader = pageLoader;
_prefetchPages = prefetchPages;
}
public async IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken ct = default)
{
string? continuationToken = null;
do
{
var page = await _pageLoader(continuationToken, ct);
foreach (var item in page.Items)
{
yield return item;
}
continuationToken = page.ContinuationToken;
} while (continuationToken != null && !ct.IsCancellationRequested);
}
}
// Batched async enumerable for efficient processing
public static class AsyncEnumerableExtensions
{
public static async IAsyncEnumerable<IReadOnlyList<T>> BatchAsync<T>(
this IAsyncEnumerable<T> source,
int batchSize,
[EnumeratorCancellation] CancellationToken ct = default)
{
var batch = new List<T>(batchSize);
await foreach (var item in source.WithCancellation(ct))
{
batch.Add(item);
if (batch.Count >= batchSize)
{
yield return batch.ToList();
batch.Clear();
}
}
if (batch.Count > 0)
{
yield return batch;
}
}
public static async IAsyncEnumerable<T> WhereAsync<T>(
this IAsyncEnumerable<T> source,
Func<T, Task<bool>> predicate,
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var item in source.WithCancellation(ct))
{
if (await predicate(item))
{
yield return item;
}
}
}
public static async IAsyncEnumerable<TResult> SelectAsync<T, TResult>(
this IAsyncEnumerable<T> source,
Func<T, Task<TResult>> selector,
[EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var item in source.WithCancellation(ct))
{
yield return await selector(item);
}
}
public static async Task<List<T>> ToListAsync<T>(
this IAsyncEnumerable<T> source,
CancellationToken ct = default)
{
var list = new List<T>();
await foreach (var item in source.WithCancellation(ct))
{
list.Add(item);
}
return list;
}
}
// Repository with async iteration
public interface IRepository<T> where T : class
{
IAsyncEnumerable<T> GetAllAsync(CancellationToken ct = default);
IAsyncEnumerable<T> QueryAsync(Expression<Func<T, bool>> predicate, CancellationToken ct = default);
}
public class UserRepository : IRepository<User>
{
private readonly IDbConnection _connection;
private readonly ILogger<UserRepository> _logger;
private const int PageSize = 100;
public UserRepository(IDbConnection connection, ILogger<UserRepository> logger)
{
_connection = connection;
_logger = logger;
}
public async IAsyncEnumerable<User> GetAllAsync([EnumeratorCancellation] CancellationToken ct = default)
{
var offset = 0;
bool hasMore;
do
{
_logger.LogDebug("Fetching users, offset: {Offset}", offset);
var users = await _connection.QueryAsync<User>(
"SELECT * FROM Users ORDER BY Id OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY",
new { Offset = offset, PageSize },
ct);
var userList = users.ToList();
hasMore = userList.Count == PageSize;
foreach (var user in userList)
{
yield return user;
}
offset += PageSize;
} while (hasMore && !ct.IsCancellationRequested);
}
public async IAsyncEnumerable<User> QueryAsync(
Expression<Func<User, bool>> predicate,
[EnumeratorCancellation] CancellationToken ct = default)
{
// In real implementation, translate expression to SQL
await foreach (var user in GetAllAsync(ct))
{
if (predicate.Compile()(user))
{
yield return user;
}
}
}
}
// Cursor-based pagination iterator
public class CursorPaginatedIterator<T, TCursor> : IAsyncEnumerable<T>
where TCursor : IComparable<TCursor>
{
private readonly Func<TCursor?, int, CancellationToken, Task<(IReadOnlyList<T> Items, TCursor? NextCursor)>> _fetcher;
private readonly int _pageSize;
public CursorPaginatedIterator(
Func<TCursor?, int, CancellationToken, Task<(IReadOnlyList<T> Items, TCursor? NextCursor)>> fetcher,
int pageSize = 50)
{
_fetcher = fetcher;
_pageSize = pageSize;
}
public async IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken ct = default)
{
TCursor? cursor = default;
do
{
var (items, nextCursor) = await _fetcher(cursor, _pageSize, ct);
foreach (var item in items)
{
yield return item;
}
cursor = nextCursor;
} while (cursor != null && !ct.IsCancellationRequested);
}
}
// Usage example
public class UserService
{
private readonly IRepository<User> _repository;
private readonly IHttpClientFactory _httpClientFactory;
public UserService(IRepository<User> repository, IHttpClientFactory httpClientFactory)
{
_repository = repository;
_httpClientFactory = httpClientFactory;
}
public async Task ProcessAllUsersAsync(CancellationToken ct = default)
{
// Process users in batches of 10
await foreach (var batch in _repository.GetAllAsync(ct).BatchAsync(10, ct))
{
await ProcessBatchAsync(batch, ct);
}
}
public async IAsyncEnumerable<User> GetActiveUsersAsync([EnumeratorCancellation] CancellationToken ct = default)
{
await foreach (var user in _repository.GetAllAsync(ct))
{
if (user.IsActive)
{
yield return user;
}
}
}
// External API pagination
public IAsyncEnumerable<ExternalUser> GetExternalUsersAsync()
{
var client = _httpClientFactory.CreateClient("ExternalApi");
return new PaginatedAsyncEnumerable<ExternalUser>(async (token, ct) =>
{
var url = token == null
? "/api/users?limit=100"
: $"/api/users?limit=100&cursor={token}";
var response = await client.GetFromJsonAsync<ApiResponse<ExternalUser>>(url, ct);
return new PagedResult<ExternalUser>(
response!.Data,
response.Total,
response.NextCursor);
});
}
private Task ProcessBatchAsync(IReadOnlyList<User> batch, CancellationToken ct)
{
Console.WriteLine($"Processing batch of {batch.Count} users");
return Task.CompletedTask;
}
}
// Supporting types
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
public bool IsActive { get; set; }
}
public class ExternalUser
{
public string Id { get; set; } = "";
public string Email { get; set; } = "";
}
public class ApiResponse<T>
{
public List<T> Data { get; set; } = new();
public int Total { get; set; }
public string? NextCursor { get; set; }
}
β Interview Q&A
Q1: Why use Iterator instead of exposing internal collections? A1: Encapsulation (hide implementation details), flexibility (change storage without changing clients), multiple traversal algorithms, and lazy evaluation (generate items on demand).
Q2: Whatβs the difference between IEnumerable and IEnumerator?
A2: IEnumerable<T> is the collection (has GetEnumerator()). IEnumerator<T> is the iterator (has Current, MoveNext()). One collection can have multiple independent iterators.
Q3: When would you use IAsyncEnumerable<T>?
A3: For streaming data from databases, APIs, or files where you donβt want to load everything into memory. It enables pagination, backpressure handling, and cancellation.
Q4: How does yield return work?
A4: The compiler generates a state machine class. Each yield return saves state and returns. Next call resumes from that point. This enables lazy evaluation without manual iterator implementation.
Mediator
Intent: Define an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly.
π Theory & When to Use
When to Use
- A set of objects communicate in well-defined but complex ways
- Reusing an object is difficult because it refers to many other objects
- Behavior distributed between classes should be customizable without subclassing
Real-World Analogy
Air traffic control: Planes donβt communicate directly with each other. They all talk to the control tower (mediator), which coordinates takeoffs, landings, and flight paths.
Key Participants
- Mediator: Interface for communicating with Colleague objects
- ConcreteMediator: Implements coordination between Colleagues
- Colleague: Each Colleague knows its Mediator and communicates with it instead of other Colleagues
MediatR in .NET
MediatR is a popular implementation that decouples request sending from handling, commonly used with CQRS.
π UML Diagram
βββββββββββββββββββββββ
β <<interface>> β
β IMediator β
βββββββββββββββββββββββ€
β + Notify(sender, β
β event) β
ββββββββββββ²βββββββββββ
β
ββββββββββββ΄βββββββββββ
β ConcreteMediator β
βββββββββββββββββββββββ€
β - colleagueA β
β - colleagueB β
β + Notify() β
ββββββββββββ¬βββββββββββ
β coordinates
βββββββ΄ββββββ
βΌ βΌ
βββββββββββ βββββββββββ
βColleagueAβ βColleagueBβ
βββββββββββ€ βββββββββββ€
β- mediatorβ β- mediatorβ
βββββββββββ βββββββββββ
π» Short Example (~20 lines)
public interface IMediator
{
void Notify(object sender, string ev);
}
public class ConcreteMediator : IMediator
{
public Component1 C1 { get; set; }
public Component2 C2 { get; set; }
public void Notify(object sender, string ev)
{
if (ev == "A") C2.DoC();
if (ev == "D") { C1.DoB(); C2.DoC(); }
}
}
public class Component1 { private IMediator _m; public Component1(IMediator m) => _m = m; public void DoA() { _m.Notify(this, "A"); } public void DoB() => Console.WriteLine("B"); }
public class Component2 { private IMediator _m; public Component2(IMediator m) => _m = m; public void DoC() => Console.WriteLine("C"); public void DoD() { _m.Notify(this, "D"); } }
// Usage
var m = new ConcreteMediator();
m.C1 = new Component1(m);
m.C2 = new Component2(m);
m.C1.DoA(); // Triggers C2.DoC()
π» Medium Example (~50 lines)
// Chat room mediator
public interface IChatMediator
{
void SendMessage(string message, User sender);
void AddUser(User user);
}
public class ChatRoom : IChatMediator
{
private readonly List<User> _users = new();
public void AddUser(User user)
{
_users.Add(user);
user.SetMediator(this);
SendMessage($"{user.Name} joined the chat", user);
}
public void SendMessage(string message, User sender)
{
foreach (var user in _users)
{
if (user != sender)
user.Receive($"{sender.Name}: {message}");
}
}
}
public class User
{
public string Name { get; }
private IChatMediator? _mediator;
public User(string name) => Name = name;
public void SetMediator(IChatMediator mediator) => _mediator = mediator;
public void Send(string message)
{
Console.WriteLine($"{Name} sends: {message}");
_mediator?.SendMessage(message, this);
}
public void Receive(string message)
{
Console.WriteLine($"{Name} receives: {message}");
}
}
// Usage
var chatRoom = new ChatRoom();
var alice = new User("Alice");
var bob = new User("Bob");
var charlie = new User("Charlie");
chatRoom.AddUser(alice);
chatRoom.AddUser(bob);
chatRoom.AddUser(charlie);
alice.Send("Hello everyone!");
bob.Send("Hi Alice!");
π» Production-Grade Example
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using FluentValidation;
// MediatR-based CQRS implementation
// Commands & Queries
public record CreateUserCommand(string Email, string Name, string Password) : IRequest<Result<UserDto>>;
public record GetUserQuery(string UserId) : IRequest<Result<UserDto>>;
public record UserDto(string Id, string Email, string Name, DateTime CreatedAt);
// Result wrapper
public record Result<T>(bool Success, T? Data, string? Error = null)
{
public static Result<T> Ok(T data) => new(true, data);
public static Result<T> Fail(string error) => new(false, default, error);
}
// Validation behavior (pipeline)
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
private readonly ILogger<ValidationBehavior<TRequest, TResponse>> _logger;
public ValidationBehavior(
IEnumerable<IValidator<TRequest>> validators,
ILogger<ValidationBehavior<TRequest, TResponse>> logger)
{
_validators = validators;
_logger = logger;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var context = new ValidationContext<TRequest>(request);
var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(r => r.Errors)
.Where(f => f != null)
.ToList();
if (failures.Count > 0)
{
var errors = string.Join(", ", failures.Select(f => f.ErrorMessage));
_logger.LogWarning("Validation failed for {Request}: {Errors}", typeof(TRequest).Name, errors);
// Return failure result
var resultType = typeof(TResponse);
if (resultType.IsGenericType && resultType.GetGenericTypeDefinition() == typeof(Result<>))
{
var failMethod = resultType.GetMethod("Fail");
return (TResponse)failMethod!.Invoke(null, new object[] { errors })!;
}
throw new ValidationException(failures);
}
return await next();
}
}
// Logging behavior
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger) => _logger = logger;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var requestName = typeof(TRequest).Name;
_logger.LogInformation("Handling {Request}", requestName);
var sw = Stopwatch.StartNew();
var response = await next();
sw.Stop();
_logger.LogInformation("Handled {Request} in {ElapsedMs}ms", requestName, sw.ElapsedMilliseconds);
return response;
}
}
// Validators
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
public CreateUserCommandValidator()
{
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Invalid email format");
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.MinimumLength(2).WithMessage("Name must be at least 2 characters");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("Password is required")
.MinimumLength(8).WithMessage("Password must be at least 8 characters");
}
}
// Command handler
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Result<UserDto>>
{
private readonly IUserRepository _repository;
private readonly IPasswordHasher _hasher;
private readonly ILogger<CreateUserCommandHandler> _logger;
public CreateUserCommandHandler(
IUserRepository repository,
IPasswordHasher hasher,
ILogger<CreateUserCommandHandler> logger)
{
_repository = repository;
_hasher = hasher;
_logger = logger;
}
public async Task<Result<UserDto>> Handle(CreateUserCommand request, CancellationToken ct)
{
// Check for existing user
var existing = await _repository.GetByEmailAsync(request.Email, ct);
if (existing != null)
{
return Result<UserDto>.Fail("Email already registered");
}
var user = new User
{
Id = Guid.NewGuid().ToString(),
Email = request.Email,
Name = request.Name,
PasswordHash = _hasher.Hash(request.Password),
CreatedAt = DateTime.UtcNow
};
await _repository.CreateAsync(user, ct);
_logger.LogInformation("Created user {UserId}", user.Id);
return Result<UserDto>.Ok(new UserDto(user.Id, user.Email, user.Name, user.CreatedAt));
}
}
// Query handler
public class GetUserQueryHandler : IRequestHandler<GetUserQuery, Result<UserDto>>
{
private readonly IUserRepository _repository;
public GetUserQueryHandler(IUserRepository repository) => _repository = repository;
public async Task<Result<UserDto>> Handle(GetUserQuery request, CancellationToken ct)
{
var user = await _repository.GetByIdAsync(request.UserId, ct);
if (user == null)
return Result<UserDto>.Fail("User not found");
return Result<UserDto>.Ok(new UserDto(user.Id, user.Email, user.Name, user.CreatedAt));
}
}
// Notifications (events)
public record UserCreatedNotification(string UserId, string Email) : INotification;
public class SendWelcomeEmailHandler : INotificationHandler<UserCreatedNotification>
{
private readonly IEmailService _emailService;
public SendWelcomeEmailHandler(IEmailService emailService) => _emailService = emailService;
public async Task Handle(UserCreatedNotification notification, CancellationToken ct)
{
await _emailService.SendWelcomeAsync(notification.Email, ct);
}
}
public class LogUserCreationHandler : INotificationHandler<UserCreatedNotification>
{
private readonly ILogger<LogUserCreationHandler> _logger;
public LogUserCreationHandler(ILogger<LogUserCreationHandler> logger) => _logger = logger;
public Task Handle(UserCreatedNotification notification, CancellationToken ct)
{
_logger.LogInformation("User created: {UserId}", notification.UserId);
return Task.CompletedTask;
}
}
// Supporting types
public class User
{
public string Id { get; set; } = "";
public string Email { get; set; } = "";
public string Name { get; set; } = "";
public string PasswordHash { get; set; } = "";
public DateTime CreatedAt { get; set; }
}
public interface IUserRepository
{
Task<User?> GetByIdAsync(string id, CancellationToken ct = default);
Task<User?> GetByEmailAsync(string email, CancellationToken ct = default);
Task CreateAsync(User user, CancellationToken ct = default);
}
public interface IPasswordHasher { string Hash(string password); }
public interface IEmailService { Task SendWelcomeAsync(string email, CancellationToken ct = default); }
// DI Registration
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMediatrWithBehaviors(this IServiceCollection services)
{
services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(CreateUserCommand).Assembly);
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
});
services.AddValidatorsFromAssembly(typeof(CreateUserCommandValidator).Assembly);
return services;
}
}
// Usage in controller
public class UsersController
{
private readonly IMediator _mediator;
public UsersController(IMediator mediator) => _mediator = mediator;
public async Task<IActionResult> Create(CreateUserRequest request)
{
var result = await _mediator.Send(new CreateUserCommand(request.Email, request.Name, request.Password));
if (!result.Success)
return BadRequest(result.Error);
// Publish notification for side effects
await _mediator.Publish(new UserCreatedNotification(result.Data!.Id, result.Data.Email));
return Ok(result.Data);
}
public async Task<IActionResult> Get(string id)
{
var result = await _mediator.Send(new GetUserQuery(id));
return result.Success ? Ok(result.Data) : NotFound(result.Error);
}
}
β Interview Q&A
Q1: Whatβs the difference between Mediator and Observer? A1: Observer is one-to-many notification (subject notifies observers). Mediator is many-to-many coordination (colleagues communicate through mediator). Mediator centralizes control; Observer distributes it.
Q2: How does MediatR implement the Mediator pattern?
A2: MediatR decouples request senders from handlers. The IMediator.Send() routes requests to appropriate handlers without the sender knowing which handler processes it. Pipeline behaviors add cross-cutting concerns.
Q3: What are the drawbacks of the Mediator pattern? A3: The mediator can become a βgod objectβ if it handles too much. It centralizes logic, which can make it complex. For simple scenarios, direct communication might be clearer.
Q4: When would you choose Mediator over events? A4: When you need coordination logic (not just notification), when the interaction is complex and needs centralized management, or when you need request/response semantics rather than fire-and-forget.
Observer
Intent: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
π Theory & When to Use
When to Use
- When a change to one object requires changing others, and you donβt know how many objects need to change
- When an object should notify other objects without making assumptions about who these objects are
- When you want loose coupling between the subject and its observers
Real-World Analogy
Newsletter subscription: subscribers register with a publisher. When a new edition is released, all subscribers are notified automatically. Subscribers can unsubscribe at any time.
Key Participants
- Subject: Knows its observers; provides interface for attaching/detaching observers
- Observer: Interface for objects that should be notified of subject changes
- ConcreteSubject: Stores state; sends notification to observers when state changes
- ConcreteObserver: Maintains reference to ConcreteSubject; implements update interface
.NET Implementations
- Events and delegates (most common)
IObservable<T>/IObserver<T>(Reactive Extensions)INotifyPropertyChanged(WPF/MAUI data binding)
π UML Diagram
βββββββββββββββββββββββ βββββββββββββββββββββββ
β Subject β β <<interface>> β
βββββββββββββββββββββββ€ β IObserver β
β - observers: List β βββββββββββββββββββββββ€
β + Attach(IObserver) β<ββββββββ + Update(Subject) β
β + Detach(IObserver) β ββββββββββββ²βββββββββββ
β + Notify() β β
ββββββββββββ²βββββββββββ β
β βββββββββββ΄βββββββββββ
ββββββββββββ΄βββββββββββ β ConcreteObserver β
β ConcreteSubject β βββββββββββββββββββββββ€
βββββββββββββββββββββββ€ β - subject β
β - state β<ββββββββ - observerState β
β + GetState() β β + Update() β
β + SetState() β βββββββββββββββββββββββ
βββββββββββββββββββββββ
π» Short Example (~20 lines)
// Using C# events (the .NET way)
public class Stock
{
private decimal _price;
public event EventHandler<decimal>? PriceChanged;
public decimal Price
{
get => _price;
set { _price = value; PriceChanged?.Invoke(this, value); }
}
}
public class StockMonitor
{
public void OnPriceChanged(object? sender, decimal price) =>
Console.WriteLine($"Stock price changed to: ${price}");
}
// Usage
var stock = new Stock();
var monitor = new StockMonitor();
stock.PriceChanged += monitor.OnPriceChanged;
stock.Price = 100.50m; // Output: Stock price changed to: $100.50
stock.Price = 102.75m; // Output: Stock price changed to: $102.75
π» Medium Example (~50 lines)
// Classic Observer pattern implementation
public interface IObserver<T>
{
void Update(T data);
}
public interface ISubject<T>
{
void Attach(IObserver<T> observer);
void Detach(IObserver<T> observer);
void Notify();
}
public class WeatherStation : ISubject<WeatherData>
{
private readonly List<IObserver<WeatherData>> _observers = new();
private WeatherData _currentWeather = new(0, 0, 0);
public void Attach(IObserver<WeatherData> observer) => _observers.Add(observer);
public void Detach(IObserver<WeatherData> observer) => _observers.Remove(observer);
public void Notify()
{
foreach (var observer in _observers)
observer.Update(_currentWeather);
}
public void SetMeasurements(float temp, float humidity, float pressure)
{
_currentWeather = new WeatherData(temp, humidity, pressure);
Notify();
}
}
public record WeatherData(float Temperature, float Humidity, float Pressure);
// Concrete observers
public class CurrentConditionsDisplay : IObserver<WeatherData>
{
public void Update(WeatherData data) =>
Console.WriteLine($"Current: {data.Temperature}Β°F, {data.Humidity}% humidity");
}
public class StatisticsDisplay : IObserver<WeatherData>
{
private readonly List<float> _temperatures = new();
public void Update(WeatherData data)
{
_temperatures.Add(data.Temperature);
var avg = _temperatures.Average();
Console.WriteLine($"Avg/Max/Min: {avg:F1}/{_temperatures.Max()}/{_temperatures.Min()}");
}
}
// Usage
var station = new WeatherStation();
var current = new CurrentConditionsDisplay();
var stats = new StatisticsDisplay();
station.Attach(current);
station.Attach(stats);
station.SetMeasurements(80, 65, 30.4f);
station.SetMeasurements(82, 70, 29.2f);
station.Detach(current); // Unsubscribe
station.SetMeasurements(78, 90, 29.4f); // Only stats display updates
π» Production-Grade Example
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
// Domain events
public interface IDomainEvent
{
string EventId { get; }
DateTime OccurredAt { get; }
}
public abstract record DomainEvent : IDomainEvent
{
public string EventId { get; } = Guid.NewGuid().ToString("N")[..8];
public DateTime OccurredAt { get; } = DateTime.UtcNow;
}
public record OrderPlacedEvent(string OrderId, string CustomerId, decimal Total) : DomainEvent;
public record OrderShippedEvent(string OrderId, string TrackingNumber) : DomainEvent;
public record PaymentReceivedEvent(string OrderId, decimal Amount, string TransactionId) : DomainEvent;
// Event bus interface
public interface IEventBus
{
void Publish<TEvent>(TEvent @event) where TEvent : IDomainEvent;
IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : IDomainEvent;
IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler) where TEvent : IDomainEvent;
IObservable<TEvent> GetEventStream<TEvent>() where TEvent : IDomainEvent;
}
// Reactive event bus implementation
public class ReactiveEventBus : IEventBus, IDisposable
{
private readonly Subject<IDomainEvent> _eventStream = new();
private readonly ILogger<ReactiveEventBus> _logger;
public ReactiveEventBus(ILogger<ReactiveEventBus> logger) => _logger = logger;
public void Publish<TEvent>(TEvent @event) where TEvent : IDomainEvent
{
_logger.LogDebug("Publishing event: {EventType} ({EventId})",
typeof(TEvent).Name, @event.EventId);
_eventStream.OnNext(@event);
}
public IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : IDomainEvent
{
return _eventStream
.OfType<TEvent>()
.Subscribe(e =>
{
try
{
handler(e);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error handling event {EventType}", typeof(TEvent).Name);
}
});
}
public IDisposable Subscribe<TEvent>(Func<TEvent, Task> handler) where TEvent : IDomainEvent
{
return _eventStream
.OfType<TEvent>()
.SelectMany(async e =>
{
try
{
await handler(e);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error handling event {EventType}", typeof(TEvent).Name);
}
return System.Reactive.Unit.Default;
})
.Subscribe();
}
public IObservable<TEvent> GetEventStream<TEvent>() where TEvent : IDomainEvent
{
return _eventStream.OfType<TEvent>();
}
public void Dispose() => _eventStream.Dispose();
}
// Event handlers
public interface IEventHandler<TEvent> where TEvent : IDomainEvent
{
Task HandleAsync(TEvent @event, CancellationToken ct = default);
}
public class OrderPlacedHandler : IEventHandler<OrderPlacedEvent>
{
private readonly ILogger<OrderPlacedHandler> _logger;
private readonly IEmailService _emailService;
public OrderPlacedHandler(ILogger<OrderPlacedHandler> logger, IEmailService emailService)
{
_logger = logger;
_emailService = emailService;
}
public async Task HandleAsync(OrderPlacedEvent @event, CancellationToken ct = default)
{
_logger.LogInformation("Processing order placed: {OrderId}", @event.OrderId);
await _emailService.SendOrderConfirmationAsync(@event.CustomerId, @event.OrderId, ct);
}
}
public class InventoryUpdateHandler : IEventHandler<OrderPlacedEvent>
{
private readonly ILogger<InventoryUpdateHandler> _logger;
private readonly IInventoryService _inventory;
public InventoryUpdateHandler(ILogger<InventoryUpdateHandler> logger, IInventoryService inventory)
{
_logger = logger;
_inventory = inventory;
}
public async Task HandleAsync(OrderPlacedEvent @event, CancellationToken ct = default)
{
_logger.LogInformation("Reserving inventory for order: {OrderId}", @event.OrderId);
await _inventory.ReserveAsync(@event.OrderId, ct);
}
}
// INotifyPropertyChanged implementation
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class OrderViewModel : ObservableObject
{
private string _status = "";
private decimal _total;
public string Status
{
get => _status;
set => SetProperty(ref _status, value);
}
public decimal Total
{
get => _total;
set => SetProperty(ref _total, value);
}
}
// Typed event aggregator with weak references
public class EventAggregator
{
private readonly Dictionary<Type, List<WeakReference>> _subscribers = new();
private readonly object _lock = new();
public void Subscribe<TEvent>(Action<TEvent> handler)
{
lock (_lock)
{
var eventType = typeof(TEvent);
if (!_subscribers.ContainsKey(eventType))
_subscribers[eventType] = new List<WeakReference>();
_subscribers[eventType].Add(new WeakReference(handler));
}
}
public void Publish<TEvent>(TEvent @event)
{
lock (_lock)
{
var eventType = typeof(TEvent);
if (!_subscribers.TryGetValue(eventType, out var subscribers))
return;
var deadRefs = new List<WeakReference>();
foreach (var weakRef in subscribers)
{
if (weakRef.Target is Action<TEvent> handler)
handler(@event);
else
deadRefs.Add(weakRef);
}
// Clean up dead references
foreach (var dead in deadRefs)
subscribers.Remove(dead);
}
}
}
// Observable collection with change notifications
public class ObservableList<T> : INotifyCollectionChanged, IList<T>
{
private readonly List<T> _items = new();
public event NotifyCollectionChangedEventHandler? CollectionChanged;
public void Add(T item)
{
_items.Add(item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add, item, _items.Count - 1));
}
public bool Remove(T item)
{
var index = _items.IndexOf(item);
if (index < 0) return false;
_items.RemoveAt(index);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove, item, index));
return true;
}
protected void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
CollectionChanged?.Invoke(this, e);
}
// IList<T> implementation
public T this[int index] { get => _items[index]; set => _items[index] = value; }
public int Count => _items.Count;
public bool IsReadOnly => false;
public void Clear() { _items.Clear(); OnCollectionChanged(new(NotifyCollectionChangedAction.Reset)); }
public bool Contains(T item) => _items.Contains(item);
public void CopyTo(T[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex);
public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();
public int IndexOf(T item) => _items.IndexOf(item);
public void Insert(int index, T item) { _items.Insert(index, item); OnCollectionChanged(new(NotifyCollectionChangedAction.Add, item, index)); }
public void RemoveAt(int index) { var item = _items[index]; _items.RemoveAt(index); OnCollectionChanged(new(NotifyCollectionChangedAction.Remove, item, index)); }
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
// Interfaces
public interface IEmailService
{
Task SendOrderConfirmationAsync(string customerId, string orderId, CancellationToken ct = default);
}
public interface IInventoryService
{
Task ReserveAsync(string orderId, CancellationToken ct = default);
}
// DI Registration
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddEventBus(this IServiceCollection services)
{
services.AddSingleton<IEventBus, ReactiveEventBus>();
services.AddSingleton<EventAggregator>();
// Register event handlers
services.AddScoped<IEventHandler<OrderPlacedEvent>, OrderPlacedHandler>();
services.AddScoped<IEventHandler<OrderPlacedEvent>, InventoryUpdateHandler>();
return services;
}
}
// Usage
public class OrderService
{
private readonly IEventBus _eventBus;
public OrderService(IEventBus eventBus) => _eventBus = eventBus;
public async Task<string> PlaceOrderAsync(string customerId, List<OrderItem> items)
{
var orderId = Guid.NewGuid().ToString("N")[..8];
var total = items.Sum(i => i.Quantity * i.UnitPrice);
// Save order to database...
// Publish event - all subscribers will be notified
_eventBus.Publish(new OrderPlacedEvent(orderId, customerId, total));
return orderId;
}
}
public record OrderItem(string ProductId, int Quantity, decimal UnitPrice);
β Interview Q&A
Q1: Whatβs the difference between events/delegates and IObservable?
A1: Events are simpler, synchronous, and tightly coupled to .NET. IObservable<T> (Rx) provides operators for filtering, transforming, combining streams, and handles backpressure. Use Rx for complex async event processing.
Q2: How do you prevent memory leaks with Observer pattern?
A2: Always unsubscribe when done (use IDisposable), use weak references for long-lived subjects, implement IDisposable in observers, and consider weak event patterns for UI scenarios.
Q3: When would you use Observer vs Mediator? A3: Observer for one-to-many notifications where observers act independently. Mediator when objects need coordinated interaction or when communication logic is complex. Observer is decentralized; Mediator centralizes.
Q4: How does INotifyPropertyChanged relate to Observer? A4: Itβs a specialized Observer for property changes, used in MVVM data binding. The View observes ViewModel properties. When properties change, the binding system updates the UI automatically.
Memento
Intent: Capture and externalize an objectβs internal state without violating encapsulation, so the object can be restored to this state later.
π Theory & When to Use
When to Use
- Implement undo/redo functionality
- Save and restore checkpoints (games, editors)
- Transaction rollback scenarios
- State history and time-travel debugging
Real-World Analogy
Like saving a game - you capture the entire game state at a checkpoint. If you fail later, you restore to that saved state without knowing all the internal details of how the game works.
Key Participants
- Originator: Creates memento containing snapshot of its state, uses memento to restore
- Memento: Stores internal state of Originator, protects against access by other objects
- Caretaker: Keeps mementos, never operates on or examines their contents
π UML Diagram
βββββββββββββββββββ βββββββββββββββββββ
β Caretaker β β Originator β
βββββββββββββββββββ€ βββββββββββββββββββ€
β - mementos[] β β - state β
βββββββββββββββββββ€ βββββββββββββββββββ€
β + Backup() βββββββ>β + Save() β
β + Undo() β β + Restore() β
βββββββββββββββββββ ββββββββββ¬βββββββββ
β creates
βΌ
βββββββββββββββββββ
β Memento β
βββββββββββββββββββ€
β - state β
β - date β
βββββββββββββββββββ€
β + GetState() β
βββββββββββββββββββ
π» Short Example (~20 lines)
// Memento stores state
public record Memento(string State);
// Originator creates and restores from mementos
public class Editor
{
public string Content { get; set; } = "";
public Memento Save() => new(Content);
public void Restore(Memento memento) => Content = memento.State;
}
// Usage
var editor = new Editor { Content = "Hello" };
var snapshot = editor.Save();
editor.Content = "Hello World";
editor.Restore(snapshot); // Content is "Hello" again
π» Medium Example (~50 lines)
public record EditorMemento(string Content, int CursorPosition, DateTime SavedAt);
public class TextEditor
{
public string Content { get; set; } = "";
public int CursorPosition { get; set; }
public EditorMemento Save() => new(Content, CursorPosition, DateTime.Now);
public void Restore(EditorMemento memento)
{
Content = memento.Content;
CursorPosition = memento.CursorPosition;
}
}
public class EditorHistory
{
private readonly Stack<EditorMemento> _history = new();
private readonly TextEditor _editor;
public EditorHistory(TextEditor editor) => _editor = editor;
public void Backup()
{
_history.Push(_editor.Save());
Console.WriteLine($"Saved: '{_editor.Content}' at position {_editor.CursorPosition}");
}
public void Undo()
{
if (_history.Count == 0) return;
var memento = _history.Pop();
_editor.Restore(memento);
Console.WriteLine($"Restored: '{_editor.Content}' from {memento.SavedAt}");
}
}
// Usage
var editor = new TextEditor();
var history = new EditorHistory(editor);
editor.Content = "First draft";
editor.CursorPosition = 5;
history.Backup();
editor.Content = "Second draft";
history.Backup();
editor.Content = "Third draft";
history.Undo(); // Back to "Second draft"
history.Undo(); // Back to "First draft"
π» Production-Grade Example
// Generic memento interface for type safety
public interface IMemento<T>
{
T State { get; }
DateTime CreatedAt { get; }
string Description { get; }
}
// Immutable memento implementation
public sealed class Memento<T> : IMemento<T>
{
public T State { get; }
public DateTime CreatedAt { get; }
public string Description { get; }
public Memento(T state, string description = "")
{
State = state;
CreatedAt = DateTime.UtcNow;
Description = description;
}
}
// Interface for objects that support memento
public interface IOriginator<T>
{
IMemento<T> CreateMemento(string description = "");
void RestoreFromMemento(IMemento<T> memento);
}
// Document with complex state
public class Document : IOriginator<DocumentState>
{
public string Title { get; set; } = "";
public string Content { get; set; } = "";
public List<string> Tags { get; } = new();
public Dictionary<string, string> Metadata { get; } = new();
public IMemento<DocumentState> CreateMemento(string description = "")
{
var state = new DocumentState(
Title,
Content,
Tags.ToList(),
new Dictionary<string, string>(Metadata)
);
return new Memento<DocumentState>(state, description);
}
public void RestoreFromMemento(IMemento<DocumentState> memento)
{
var state = memento.State;
Title = state.Title;
Content = state.Content;
Tags.Clear();
Tags.AddRange(state.Tags);
Metadata.Clear();
foreach (var kvp in state.Metadata)
Metadata[kvp.Key] = kvp.Value;
}
}
public record DocumentState(
string Title,
string Content,
List<string> Tags,
Dictionary<string, string> Metadata
);
// History manager with undo/redo support
public class HistoryManager<T>
{
private readonly Stack<IMemento<T>> _undoStack = new();
private readonly Stack<IMemento<T>> _redoStack = new();
private readonly IOriginator<T> _originator;
private readonly int _maxHistory;
public event EventHandler? HistoryChanged;
public HistoryManager(IOriginator<T> originator, int maxHistory = 100)
{
_originator = originator;
_maxHistory = maxHistory;
}
public bool CanUndo => _undoStack.Count > 0;
public bool CanRedo => _redoStack.Count > 0;
public int UndoCount => _undoStack.Count;
public int RedoCount => _redoStack.Count;
public void SaveState(string description = "")
{
// Clear redo stack when new state is saved
_redoStack.Clear();
var memento = _originator.CreateMemento(description);
_undoStack.Push(memento);
// Limit history size
if (_undoStack.Count > _maxHistory)
{
var temp = _undoStack.ToArray().Reverse().Skip(1).Take(_maxHistory);
_undoStack.Clear();
foreach (var m in temp.Reverse())
_undoStack.Push(m);
}
HistoryChanged?.Invoke(this, EventArgs.Empty);
}
public bool Undo()
{
if (!CanUndo) return false;
// Save current state to redo stack
_redoStack.Push(_originator.CreateMemento("Before undo"));
var memento = _undoStack.Pop();
_originator.RestoreFromMemento(memento);
HistoryChanged?.Invoke(this, EventArgs.Empty);
return true;
}
public bool Redo()
{
if (!CanRedo) return false;
// Save current state to undo stack
_undoStack.Push(_originator.CreateMemento("Before redo"));
var memento = _redoStack.Pop();
_originator.RestoreFromMemento(memento);
HistoryChanged?.Invoke(this, EventArgs.Empty);
return true;
}
public IEnumerable<(DateTime Time, string Description)> GetHistory()
{
return _undoStack.Select(m => (m.CreatedAt, m.Description));
}
public void Clear()
{
_undoStack.Clear();
_redoStack.Clear();
HistoryChanged?.Invoke(this, EventArgs.Empty);
}
}
// Usage with dependency injection
public class DocumentEditor
{
private readonly Document _document;
private readonly HistoryManager<DocumentState> _history;
public DocumentEditor()
{
_document = new Document();
_history = new HistoryManager<DocumentState>(_document);
_history.HistoryChanged += (s, e) =>
Console.WriteLine($"History: {_history.UndoCount} undo, {_history.RedoCount} redo");
}
public void EditTitle(string title)
{
_history.SaveState($"Changed title to '{title}'");
_document.Title = title;
}
public void EditContent(string content)
{
_history.SaveState("Content edit");
_document.Content = content;
}
public void Undo() => _history.Undo();
public void Redo() => _history.Redo();
public void ShowDocument()
{
Console.WriteLine($"Title: {_document.Title}");
Console.WriteLine($"Content: {_document.Content}");
}
}
β Interview Q&A
Q1: How does Memento differ from simply cloning an object? A1: Memento encapsulates state without exposing internal structure. Cloning copies the entire object. Memento allows selective state capture and keeps the state private, preserving encapsulation.
Q2: How do you handle large state objects efficiently? A2: Use incremental/delta mementos storing only changes, implement lazy loading, compress state, or use copy-on-write. For very large states, consider storing mementos on disk.
Q3: Whatβs the relationship between Memento and Command patterns? A3: Often used together - Command executes operations and can store a Memento for undo. Command handles βwhat to doβ, Memento handles βstate to restoreβ. Together they implement robust undo/redo.
Q4: How do you implement redo functionality? A4: Maintain two stacks: undo and redo. When undoing, push current state to redo stack before restoring. When redoing, push current state to undo stack before restoring. Clear redo stack on new edits.
State
Intent: Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.
π Theory & When to Use
When to Use
- Object behavior depends on its state and must change at runtime
- Operations have large conditional statements based on state
- Implementing state machines (workflows, game states, UI modes)
- State transitions have complex rules
Real-World Analogy
Like a vending machine - its behavior changes based on state: βNo Coinβ (waits for coin), βHas Coinβ (allows selection), βDispensingβ (releases product), βSold Outβ (refuses transactions). Same buttons, different behavior.
Key Participants
- Context: Maintains current state, delegates state-specific behavior
- State: Interface declaring state-specific behavior
- ConcreteState: Implements behavior for a particular state, may trigger transitions
π UML Diagram
ββββββββββββββββββββ βββββββββββββββββββββββ
β Context β β <<interface>> β
ββββββββββββββββββββ€ β IState β
β - state: IState βββββββββ>βββββββββββββββββββββββ€
ββββββββββββββββββββ€ β + Handle(context) β
β + Request() β βββββββββββββββββββββββ
β + SetState() β β³
ββββββββββββββββββββ β
βββββββββββββΌββββββββββββ
β β β
ββββββββββ΄βββ ββββββββ΄βββββ ββββββ΄βββββββββ
β StateA β β StateB β β StateC β
βββββββββββββ€ βββββββββββββ€ βββββββββββββββ€
β +Handle() β β +Handle() β β +Handle() β
βββββββββββββ βββββββββββββ βββββββββββββββ
π» Short Example (~20 lines)
public interface IState
{
void Handle(Context context);
}
public class Context
{
public IState State { get; set; }
public void Request() => State?.Handle(this);
}
public class ConcreteStateA : IState
{
public void Handle(Context context)
{
Console.WriteLine("State A handling, transitioning to B");
context.State = new ConcreteStateB();
}
}
public class ConcreteStateB : IState
{
public void Handle(Context context) => Console.WriteLine("State B handling");
}
π» Medium Example (~50 lines)
// Order state machine
public interface IOrderState
{
void Proceed(Order order);
void Cancel(Order order);
string Status { get; }
}
public class Order
{
public IOrderState State { get; set; } = new PendingState();
public string OrderId { get; } = Guid.NewGuid().ToString()[..8];
public void Proceed() => State.Proceed(this);
public void Cancel() => State.Cancel(this);
}
public class PendingState : IOrderState
{
public string Status => "Pending";
public void Proceed(Order order)
{
Console.WriteLine("Order confirmed, processing...");
order.State = new ProcessingState();
}
public void Cancel(Order order)
{
Console.WriteLine("Order cancelled.");
order.State = new CancelledState();
}
}
public class ProcessingState : IOrderState
{
public string Status => "Processing";
public void Proceed(Order order)
{
Console.WriteLine("Order shipped!");
order.State = new ShippedState();
}
public void Cancel(Order order) => Console.WriteLine("Cannot cancel - already processing");
}
public class ShippedState : IOrderState
{
public string Status => "Shipped";
public void Proceed(Order order) => Console.WriteLine("Already shipped");
public void Cancel(Order order) => Console.WriteLine("Cannot cancel shipped order");
}
public class CancelledState : IOrderState
{
public string Status => "Cancelled";
public void Proceed(Order order) => Console.WriteLine("Cannot proceed - cancelled");
public void Cancel(Order order) => Console.WriteLine("Already cancelled");
}
// Usage
var order = new Order();
order.Proceed(); // Pending -> Processing
order.Proceed(); // Processing -> Shipped
order.Cancel(); // Cannot cancel shipped order
π» Production-Grade Example
// Generic state machine with async support
public interface IState<TContext> where TContext : class
{
string Name { get; }
Task OnEnterAsync(TContext context, CancellationToken ct = default);
Task OnExitAsync(TContext context, CancellationToken ct = default);
Task<IState<TContext>?> HandleAsync(TContext context, string trigger, CancellationToken ct = default);
IEnumerable<string> GetAllowedTriggers();
}
public abstract class StateBase<TContext> : IState<TContext> where TContext : class
{
public abstract string Name { get; }
public virtual Task OnEnterAsync(TContext context, CancellationToken ct = default)
=> Task.CompletedTask;
public virtual Task OnExitAsync(TContext context, CancellationToken ct = default)
=> Task.CompletedTask;
public abstract Task<IState<TContext>?> HandleAsync(
TContext context, string trigger, CancellationToken ct = default);
public abstract IEnumerable<string> GetAllowedTriggers();
}
// State machine with logging and events
public class StateMachine<TContext> where TContext : class
{
private IState<TContext> _currentState;
private readonly TContext _context;
private readonly ILogger<StateMachine<TContext>>? _logger;
public event Func<StateTransition<TContext>, Task>? OnTransition;
public IState<TContext> CurrentState => _currentState;
public string CurrentStateName => _currentState.Name;
public StateMachine(TContext context, IState<TContext> initialState,
ILogger<StateMachine<TContext>>? logger = null)
{
_context = context;
_currentState = initialState;
_logger = logger;
}
public async Task InitializeAsync(CancellationToken ct = default)
{
_logger?.LogInformation("Initializing state machine in state: {State}", _currentState.Name);
await _currentState.OnEnterAsync(_context, ct);
}
public async Task<bool> TriggerAsync(string trigger, CancellationToken ct = default)
{
if (!_currentState.GetAllowedTriggers().Contains(trigger))
{
_logger?.LogWarning("Trigger {Trigger} not allowed in state {State}",
trigger, _currentState.Name);
return false;
}
var previousState = _currentState;
var newState = await _currentState.HandleAsync(_context, trigger, ct);
if (newState != null && newState != _currentState)
{
await _currentState.OnExitAsync(_context, ct);
_currentState = newState;
await _currentState.OnEnterAsync(_context, ct);
var transition = new StateTransition<TContext>(
previousState, _currentState, trigger, DateTime.UtcNow);
_logger?.LogInformation("Transition: {From} -> {To} via {Trigger}",
previousState.Name, _currentState.Name, trigger);
if (OnTransition != null)
await OnTransition(transition);
}
return true;
}
public IEnumerable<string> GetAllowedTriggers() => _currentState.GetAllowedTriggers();
}
public record StateTransition<TContext>(
IState<TContext> FromState,
IState<TContext> ToState,
string Trigger,
DateTime Timestamp
) where TContext : class;
// Document workflow example
public class Document
{
public string Id { get; } = Guid.NewGuid().ToString()[..8];
public string Content { get; set; } = "";
public string? ApprovedBy { get; set; }
public string? RejectionReason { get; set; }
public List<string> Comments { get; } = new();
}
public static class DocumentTriggers
{
public const string Submit = "submit";
public const string Approve = "approve";
public const string Reject = "reject";
public const string Revise = "revise";
public const string Publish = "publish";
public const string Archive = "archive";
}
public class DraftState : StateBase<Document>
{
public override string Name => "Draft";
public override Task<IState<Document>?> HandleAsync(
Document context, string trigger, CancellationToken ct = default)
{
return trigger switch
{
DocumentTriggers.Submit => Task.FromResult<IState<Document>?>(new PendingReviewState()),
_ => Task.FromResult<IState<Document>?>(null)
};
}
public override IEnumerable<string> GetAllowedTriggers()
=> new[] { DocumentTriggers.Submit };
}
public class PendingReviewState : StateBase<Document>
{
public override string Name => "Pending Review";
public override Task<IState<Document>?> HandleAsync(
Document context, string trigger, CancellationToken ct = default)
{
return trigger switch
{
DocumentTriggers.Approve => Task.FromResult<IState<Document>?>(new ApprovedState()),
DocumentTriggers.Reject => Task.FromResult<IState<Document>?>(new DraftState()),
_ => Task.FromResult<IState<Document>?>(null)
};
}
public override IEnumerable<string> GetAllowedTriggers()
=> new[] { DocumentTriggers.Approve, DocumentTriggers.Reject };
}
public class ApprovedState : StateBase<Document>
{
public override string Name => "Approved";
public override Task OnEnterAsync(Document context, CancellationToken ct = default)
{
context.ApprovedBy = "Reviewer";
return Task.CompletedTask;
}
public override Task<IState<Document>?> HandleAsync(
Document context, string trigger, CancellationToken ct = default)
{
return trigger switch
{
DocumentTriggers.Publish => Task.FromResult<IState<Document>?>(new PublishedState()),
DocumentTriggers.Revise => Task.FromResult<IState<Document>?>(new DraftState()),
_ => Task.FromResult<IState<Document>?>(null)
};
}
public override IEnumerable<string> GetAllowedTriggers()
=> new[] { DocumentTriggers.Publish, DocumentTriggers.Revise };
}
public class PublishedState : StateBase<Document>
{
public override string Name => "Published";
public override Task<IState<Document>?> HandleAsync(
Document context, string trigger, CancellationToken ct = default)
{
return trigger switch
{
DocumentTriggers.Archive => Task.FromResult<IState<Document>?>(new ArchivedState()),
_ => Task.FromResult<IState<Document>?>(null)
};
}
public override IEnumerable<string> GetAllowedTriggers()
=> new[] { DocumentTriggers.Archive };
}
public class ArchivedState : StateBase<Document>
{
public override string Name => "Archived";
public override Task<IState<Document>?> HandleAsync(
Document context, string trigger, CancellationToken ct = default)
=> Task.FromResult<IState<Document>?>(null);
public override IEnumerable<string> GetAllowedTriggers()
=> Enumerable.Empty<string>();
}
// Usage
public class DocumentWorkflow
{
public async Task RunAsync()
{
var document = new Document { Content = "My document content" };
var stateMachine = new StateMachine<Document>(document, new DraftState());
stateMachine.OnTransition += async transition =>
{
Console.WriteLine($"Document {document.Id}: {transition.FromState.Name} -> {transition.ToState.Name}");
await Task.CompletedTask;
};
await stateMachine.InitializeAsync();
Console.WriteLine($"Allowed: {string.Join(", ", stateMachine.GetAllowedTriggers())}");
await stateMachine.TriggerAsync(DocumentTriggers.Submit);
await stateMachine.TriggerAsync(DocumentTriggers.Approve);
await stateMachine.TriggerAsync(DocumentTriggers.Publish);
Console.WriteLine($"Final state: {stateMachine.CurrentStateName}");
}
}
β Interview Q&A
Q1: How does State pattern differ from Strategy pattern? A1: State changes behavior based on internal state and states know about each other (for transitions). Strategy swaps algorithms externally without states knowing about each other. State manages transitions internally; Strategy is set externally.
Q2: Where should transition logic live - in states or context? A2: It depends. States owning transitions is more decentralized and encapsulated. Context managing transitions centralizes logic but couples states to context. For complex rules, consider a separate transition table.
Q3: How do you handle shared state between state objects? A3: Pass context to state methods, use Flyweight for stateless state objects, or make states singletons if they donβt hold instance state. The Context typically holds all shared data.
Q4: When would you use a state machine library vs implementing State pattern? A4: Use libraries (Stateless, Automatonymous) for complex workflows with many states, guards, hierarchical states, or persistence needs. Implement manually for simple state machines with few states and straightforward transitions.
Strategy
Intent: Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.
π Theory & When to Use
When to Use
- Need different variants of an algorithm
- Have conditional statements selecting behavior at runtime
- Algorithm implementation details should be hidden from clients
- Want to swap algorithms without modifying clients (payment methods, sorting, compression)
Real-World Analogy
Like choosing transportation to the airport - drive, taxi, bus, or train. The goal (reach airport) is the same, but the strategy varies. You pick based on cost, time, or convenience without changing your destination.
Key Participants
- Strategy: Interface declaring algorithm operations
- ConcreteStrategy: Implements algorithm using Strategy interface
- Context: Configured with ConcreteStrategy, maintains reference, may define interface for Strategy to access its data
π UML Diagram
ββββββββββββββββββββββββ βββββββββββββββββββββββ
β Context β β <<interface>> β
ββββββββββββββββββββββββ€ β IStrategy β
β - strategy: IStrategyβββββββ>βββββββββββββββββββββββ€
ββββββββββββββββββββββββ€ β + Execute() β
β + SetStrategy() β βββββββββββββββββββββββ
β + ExecuteStrategy() β β³
ββββββββββββββββββββββββ β
βββββββββββββΌββββββββββββ
β β β
ββββββββββ΄ββββ βββββββ΄ββββββ βββββ΄ββββββββββ
β StrategyA β β StrategyB β β StrategyC β
ββββββββββββββ€ βββββββββββββ€ βββββββββββββββ€
β +Execute() β β +Execute()β β +Execute() β
ββββββββββββββ βββββββββββββ βββββββββββββββ
π» Short Example (~20 lines)
public interface ISortStrategy
{
void Sort(List<int> list);
}
public class QuickSort : ISortStrategy
{
public void Sort(List<int> list) => list.Sort(); // Uses built-in QuickSort
}
public class BubbleSort : ISortStrategy
{
public void Sort(List<int> list)
{
for (int i = 0; i < list.Count - 1; i++)
for (int j = 0; j < list.Count - i - 1; j++)
if (list[j] > list[j + 1])
(list[j], list[j + 1]) = (list[j + 1], list[j]);
}
}
// Usage
var numbers = new List<int> { 3, 1, 4, 1, 5 };
ISortStrategy strategy = numbers.Count > 1000 ? new QuickSort() : new BubbleSort();
strategy.Sort(numbers);
π» Medium Example (~50 lines)
// Payment processing strategies
public interface IPaymentStrategy
{
bool Pay(decimal amount);
string Name { get; }
}
public class CreditCardPayment : IPaymentStrategy
{
private readonly string _cardNumber;
public string Name => "Credit Card";
public CreditCardPayment(string cardNumber) => _cardNumber = cardNumber;
public bool Pay(decimal amount)
{
Console.WriteLine($"Paid ${amount} with credit card ending in {_cardNumber[^4..]}");
return true;
}
}
public class PayPalPayment : IPaymentStrategy
{
private readonly string _email;
public string Name => "PayPal";
public PayPalPayment(string email) => _email = email;
public bool Pay(decimal amount)
{
Console.WriteLine($"Paid ${amount} via PayPal ({_email})");
return true;
}
}
public class CryptoPayment : IPaymentStrategy
{
private readonly string _walletAddress;
public string Name => "Cryptocurrency";
public CryptoPayment(string walletAddress) => _walletAddress = walletAddress;
public bool Pay(decimal amount)
{
Console.WriteLine($"Paid ${amount} in crypto to {_walletAddress[..8]}...");
return true;
}
}
public class ShoppingCart
{
private readonly List<(string Item, decimal Price)> _items = new();
public void AddItem(string item, decimal price) => _items.Add((item, price));
public bool Checkout(IPaymentStrategy paymentStrategy)
{
var total = _items.Sum(i => i.Price);
Console.WriteLine($"Total: ${total} - Using {paymentStrategy.Name}");
return paymentStrategy.Pay(total);
}
}
// Usage
var cart = new ShoppingCart();
cart.AddItem("Book", 29.99m);
cart.AddItem("Headphones", 199.99m);
cart.Checkout(new CreditCardPayment("4111111111111111"));
cart.Checkout(new PayPalPayment("user@email.com"));
π» Production-Grade Example
// Async strategy with validation and DI support
public interface IShippingStrategy
{
string Name { get; }
Task<ShippingResult> CalculateAsync(ShippingRequest request, CancellationToken ct = default);
bool CanHandle(ShippingRequest request);
}
public record ShippingRequest(
string OriginZip,
string DestinationZip,
decimal WeightKg,
decimal LengthCm,
decimal WidthCm,
decimal HeightCm,
bool RequiresSignature = false,
bool IsFragile = false
);
public record ShippingResult(
string Carrier,
decimal Cost,
int EstimatedDays,
string TrackingPrefix,
bool Success,
string? Error = null
);
public class StandardShipping : IShippingStrategy
{
public string Name => "Standard Ground";
public bool CanHandle(ShippingRequest request)
=> request.WeightKg <= 30 && !request.IsFragile;
public Task<ShippingResult> CalculateAsync(ShippingRequest request, CancellationToken ct)
{
var baseCost = 5.99m + (request.WeightKg * 0.50m);
var result = new ShippingResult("USPS", baseCost, 5, "STD", true);
return Task.FromResult(result);
}
}
public class ExpressShipping : IShippingStrategy
{
private readonly IHttpClientFactory _httpClientFactory;
public ExpressShipping(IHttpClientFactory httpClientFactory)
=> _httpClientFactory = httpClientFactory;
public string Name => "Express (2-Day)";
public bool CanHandle(ShippingRequest request) => request.WeightKg <= 50;
public async Task<ShippingResult> CalculateAsync(ShippingRequest request, CancellationToken ct)
{
// Simulate API call to carrier
await Task.Delay(100, ct);
var baseCost = 15.99m + (request.WeightKg * 1.25m);
if (request.RequiresSignature) baseCost += 3.00m;
return new ShippingResult("FedEx", baseCost, 2, "EXP", true);
}
}
public class FreightShipping : IShippingStrategy
{
public string Name => "Freight";
public bool CanHandle(ShippingRequest request) => request.WeightKg > 30;
public async Task<ShippingResult> CalculateAsync(ShippingRequest request, CancellationToken ct)
{
await Task.Delay(200, ct); // Simulate calculation
var volume = request.LengthCm * request.WidthCm * request.HeightCm / 1_000_000m;
var cost = Math.Max(request.WeightKg * 2.50m, volume * 150m);
return new ShippingResult("FreightCo", cost, 7, "FRT", true);
}
}
// Strategy selector/factory
public interface IShippingStrategySelector
{
IShippingStrategy SelectStrategy(ShippingRequest request);
IEnumerable<IShippingStrategy> GetAvailableStrategies(ShippingRequest request);
}
public class ShippingStrategySelector : IShippingStrategySelector
{
private readonly IEnumerable<IShippingStrategy> _strategies;
private readonly ILogger<ShippingStrategySelector> _logger;
public ShippingStrategySelector(
IEnumerable<IShippingStrategy> strategies,
ILogger<ShippingStrategySelector> logger)
{
_strategies = strategies;
_logger = logger;
}
public IShippingStrategy SelectStrategy(ShippingRequest request)
{
var strategy = _strategies.FirstOrDefault(s => s.CanHandle(request))
?? throw new InvalidOperationException("No shipping strategy available for request");
_logger.LogInformation("Selected shipping strategy: {Strategy}", strategy.Name);
return strategy;
}
public IEnumerable<IShippingStrategy> GetAvailableStrategies(ShippingRequest request)
=> _strategies.Where(s => s.CanHandle(request));
}
// Context that uses strategies
public class ShippingService
{
private readonly IShippingStrategySelector _selector;
private readonly ILogger<ShippingService> _logger;
public ShippingService(
IShippingStrategySelector selector,
ILogger<ShippingService> logger)
{
_selector = selector;
_logger = logger;
}
public async Task<ShippingResult> GetShippingQuoteAsync(
ShippingRequest request,
CancellationToken ct = default)
{
var strategy = _selector.SelectStrategy(request);
return await strategy.CalculateAsync(request, ct);
}
public async Task<IEnumerable<ShippingResult>> GetAllQuotesAsync(
ShippingRequest request,
CancellationToken ct = default)
{
var strategies = _selector.GetAvailableStrategies(request);
var tasks = strategies.Select(s => s.CalculateAsync(request, ct));
return await Task.WhenAll(tasks);
}
}
// DI Registration
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddShippingStrategies(this IServiceCollection services)
{
services.AddTransient<IShippingStrategy, StandardShipping>();
services.AddTransient<IShippingStrategy, ExpressShipping>();
services.AddTransient<IShippingStrategy, FreightShipping>();
services.AddTransient<IShippingStrategySelector, ShippingStrategySelector>();
services.AddTransient<ShippingService>();
return services;
}
}
// Usage in controller/endpoint
public class ShippingController
{
private readonly ShippingService _shippingService;
public ShippingController(ShippingService shippingService)
=> _shippingService = shippingService;
public async Task<IActionResult> GetQuotes(ShippingRequest request, CancellationToken ct)
{
var quotes = await _shippingService.GetAllQuotesAsync(request, ct);
return Ok(quotes.OrderBy(q => q.Cost));
}
}
β Interview Q&A
Q1: How does Strategy differ from Factory pattern? A1: Strategy defines how to do something (interchangeable algorithms). Factory defines what to create (object construction). You might use Factory to create Strategy objects, but they solve different problems.
Q2: Can you use lambdas/delegates instead of Strategy classes?
A2: Yes, for simple strategies. Func<T, TResult> can replace single-method strategy interfaces. Use classes when strategies need state, multiple methods, or dependency injection.
Q3: How do you choose between Strategy and Template Method? A3: Strategy uses composition (swappable objects), Template Method uses inheritance (override steps). Strategy is more flexible; Template Method better when algorithm structure is fixed. Favor Strategy for runtime flexibility.
Q4: How do you handle strategy selection logic? A4: Use a selector/factory that evaluates criteria, chain-of-responsibility for complex rules, or configuration-based selection. Keep selection logic separate from strategies for SRP compliance.
Template Method
Intent: Define the skeleton of an algorithm in a base class, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps without changing the algorithmβs structure.
π Theory & When to Use
When to Use
- Have an algorithm with invariant structure but varying steps
- Want to control extension points (what can be customized)
- Need to share common behavior while allowing customization
- Implementing frameworks with hooks for client code
Real-World Analogy
Like a recipe template - βmake soupβ has fixed steps: boil water, add ingredients, simmer, serve. But what ingredients and how long to simmer varies. The template defines the process; subclasses provide details.
Key Participants
- AbstractClass: Defines template method with algorithm skeleton, declares abstract/virtual methods for steps
- ConcreteClass: Implements abstract steps, may override hooks
π UML Diagram
βββββββββββββββββββββββββββββ
β <<abstract>> β
β AbstractClass β
βββββββββββββββββββββββββββββ€
β + TemplateMethod() β βββ Calls primitives in order
β # PrimitiveOperation1() β βββ Abstract (must override)
β # PrimitiveOperation2() β βββ Abstract (must override)
β # Hook() β βββ Virtual (may override)
βββββββββββββββββββββββββββββ
β³
β
βββββββββββ΄ββββββββββ
β β
βββ΄ββββββββββββββ βββββ΄ββββββββββββ
β ConcreteClassAβ β ConcreteClassBβ
βββββββββββββββββ€ βββββββββββββββββ€
β +Primitive1() β β +Primitive1() β
β +Primitive2() β β +Primitive2() β
βββββββββββββββββ βββββββββββββββββ
π» Short Example (~20 lines)
public abstract class DataProcessor
{
// Template method - defines algorithm skeleton
public void Process()
{
ReadData();
ProcessData();
SaveData();
}
protected abstract void ReadData();
protected abstract void ProcessData();
protected virtual void SaveData() => Console.WriteLine("Saving to default location");
}
public class CsvProcessor : DataProcessor
{
protected override void ReadData() => Console.WriteLine("Reading CSV file");
protected override void ProcessData() => Console.WriteLine("Parsing CSV rows");
}
public class JsonProcessor : DataProcessor
{
protected override void ReadData() => Console.WriteLine("Reading JSON file");
protected override void ProcessData() => Console.WriteLine("Parsing JSON objects");
protected override void SaveData() => Console.WriteLine("Saving to NoSQL database");
}
π» Medium Example (~50 lines)
// Report generation template
public abstract class ReportGenerator
{
// Template method
public string GenerateReport()
{
var data = FetchData();
var processed = ProcessData(data);
var formatted = FormatReport(processed);
if (ShouldAddHeader())
formatted = AddHeader() + formatted;
if (ShouldAddFooter())
formatted += AddFooter();
return formatted;
}
// Required steps (abstract)
protected abstract IEnumerable<object> FetchData();
protected abstract IEnumerable<object> ProcessData(IEnumerable<object> data);
protected abstract string FormatReport(IEnumerable<object> data);
// Optional hooks with defaults
protected virtual bool ShouldAddHeader() => true;
protected virtual bool ShouldAddFooter() => true;
protected virtual string AddHeader() => $"Report generated: {DateTime.Now}\n---\n";
protected virtual string AddFooter() => "\n---\nEnd of Report";
}
public class SalesReport : ReportGenerator
{
protected override IEnumerable<object> FetchData()
=> new[] { new { Product = "Widget", Sales = 100 }, new { Product = "Gadget", Sales = 50 } };
protected override IEnumerable<object> ProcessData(IEnumerable<object> data)
=> data; // No additional processing
protected override string FormatReport(IEnumerable<object> data)
=> string.Join("\n", data.Select(d => d.ToString()));
}
public class InventoryReport : ReportGenerator
{
protected override IEnumerable<object> FetchData()
=> new[] { new { Item = "Widget", Stock = 500 } };
protected override IEnumerable<object> ProcessData(IEnumerable<object> data)
=> data;
protected override string FormatReport(IEnumerable<object> data)
=> "INVENTORY:\n" + string.Join("\n", data.Select(d => $" - {d}"));
protected override bool ShouldAddFooter() => false; // No footer for inventory
}
// Usage
var salesReport = new SalesReport();
Console.WriteLine(salesReport.GenerateReport());
π» Production-Grade Example
// Async template method with DI, logging, and error handling
public abstract class DataPipelineBase<TInput, TOutput>
{
protected readonly ILogger Logger;
private readonly IMetricsCollector _metrics;
protected DataPipelineBase(ILogger logger, IMetricsCollector metrics)
{
Logger = logger;
_metrics = metrics;
}
// Template method with full lifecycle
public async Task<PipelineResult<TOutput>> ExecuteAsync(
TInput input,
CancellationToken ct = default)
{
var stopwatch = Stopwatch.StartNew();
var result = new PipelineResult<TOutput>();
try
{
Logger.LogInformation("Pipeline {Name} starting", GetType().Name);
// Pre-processing hook
await OnBeforeExecuteAsync(input, ct);
// Validate input
var validationResult = await ValidateInputAsync(input, ct);
if (!validationResult.IsValid)
{
result.Success = false;
result.Errors.AddRange(validationResult.Errors);
return result;
}
// Core pipeline steps
var rawData = await FetchDataAsync(input, ct);
var transformed = await TransformDataAsync(rawData, ct);
var validated = await ValidateOutputAsync(transformed, ct);
if (!validated.IsValid)
{
result.Success = false;
result.Errors.AddRange(validated.Errors);
return result;
}
// Save/output step
await SaveResultAsync(transformed, ct);
result.Data = transformed;
result.Success = true;
// Post-processing hook
await OnAfterExecuteAsync(result, ct);
}
catch (Exception ex)
{
Logger.LogError(ex, "Pipeline {Name} failed", GetType().Name);
result.Success = false;
result.Errors.Add(ex.Message);
await OnErrorAsync(ex, ct);
}
finally
{
stopwatch.Stop();
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
_metrics.RecordPipelineExecution(GetType().Name, result.Success, result.ExecutionTimeMs);
Logger.LogInformation("Pipeline {Name} completed in {Ms}ms",
GetType().Name, result.ExecutionTimeMs);
}
return result;
}
// Abstract methods - must be implemented
protected abstract Task<object> FetchDataAsync(TInput input, CancellationToken ct);
protected abstract Task<TOutput> TransformDataAsync(object data, CancellationToken ct);
// Virtual methods - can be overridden
protected virtual Task<ValidationResult> ValidateInputAsync(TInput input, CancellationToken ct)
=> Task.FromResult(ValidationResult.Valid());
protected virtual Task<ValidationResult> ValidateOutputAsync(TOutput output, CancellationToken ct)
=> Task.FromResult(ValidationResult.Valid());
protected virtual Task SaveResultAsync(TOutput result, CancellationToken ct)
=> Task.CompletedTask;
// Hooks - optional extension points
protected virtual Task OnBeforeExecuteAsync(TInput input, CancellationToken ct)
=> Task.CompletedTask;
protected virtual Task OnAfterExecuteAsync(PipelineResult<TOutput> result, CancellationToken ct)
=> Task.CompletedTask;
protected virtual Task OnErrorAsync(Exception ex, CancellationToken ct)
=> Task.CompletedTask;
}
public class PipelineResult<T>
{
public bool Success { get; set; }
public T? Data { get; set; }
public List<string> Errors { get; } = new();
public long ExecutionTimeMs { get; set; }
}
public class ValidationResult
{
public bool IsValid { get; set; }
public List<string> Errors { get; } = new();
public static ValidationResult Valid() => new() { IsValid = true };
public static ValidationResult Invalid(params string[] errors)
=> new() { IsValid = false, Errors = { errors.ToList() } };
}
public interface IMetricsCollector
{
void RecordPipelineExecution(string name, bool success, long ms);
}
// Concrete implementation: Order processing pipeline
public class OrderProcessingPipeline : DataPipelineBase<OrderRequest, ProcessedOrder>
{
private readonly IOrderRepository _orderRepo;
private readonly IInventoryService _inventory;
private readonly INotificationService _notifications;
public OrderProcessingPipeline(
IOrderRepository orderRepo,
IInventoryService inventory,
INotificationService notifications,
ILogger<OrderProcessingPipeline> logger,
IMetricsCollector metrics) : base(logger, metrics)
{
_orderRepo = orderRepo;
_inventory = inventory;
_notifications = notifications;
}
protected override async Task<ValidationResult> ValidateInputAsync(
OrderRequest input, CancellationToken ct)
{
if (input.Items.Count == 0)
return ValidationResult.Invalid("Order must have at least one item");
foreach (var item in input.Items)
{
var available = await _inventory.CheckStockAsync(item.ProductId, ct);
if (available < item.Quantity)
return ValidationResult.Invalid($"Insufficient stock for {item.ProductId}");
}
return ValidationResult.Valid();
}
protected override async Task<object> FetchDataAsync(OrderRequest input, CancellationToken ct)
{
// Fetch product details, pricing, etc.
var products = new List<ProductInfo>();
foreach (var item in input.Items)
{
var product = await _inventory.GetProductAsync(item.ProductId, ct);
products.Add(product);
}
return new { Request = input, Products = products };
}
protected override Task<ProcessedOrder> TransformDataAsync(object data, CancellationToken ct)
{
dynamic d = data;
var request = (OrderRequest)d.Request;
var products = (List<ProductInfo>)d.Products;
var order = new ProcessedOrder
{
OrderId = Guid.NewGuid().ToString(),
CustomerId = request.CustomerId,
Items = request.Items.Zip(products, (item, product) => new ProcessedOrderItem
{
ProductId = item.ProductId,
ProductName = product.Name,
Quantity = item.Quantity,
UnitPrice = product.Price,
Total = item.Quantity * product.Price
}).ToList(),
CreatedAt = DateTime.UtcNow
};
order.GrandTotal = order.Items.Sum(i => i.Total);
return Task.FromResult(order);
}
protected override async Task SaveResultAsync(ProcessedOrder result, CancellationToken ct)
{
await _orderRepo.SaveAsync(result, ct);
foreach (var item in result.Items)
{
await _inventory.ReserveStockAsync(item.ProductId, item.Quantity, ct);
}
}
protected override async Task OnAfterExecuteAsync(
PipelineResult<ProcessedOrder> result, CancellationToken ct)
{
if (result.Success && result.Data != null)
{
await _notifications.SendOrderConfirmationAsync(result.Data, ct);
}
}
protected override async Task OnErrorAsync(Exception ex, CancellationToken ct)
{
// Compensating action - release any reserved inventory
Logger.LogWarning("Rolling back inventory reservations due to error");
await Task.CompletedTask;
}
}
// Supporting types
public record OrderRequest(string CustomerId, List<OrderItem> Items);
public record OrderItem(string ProductId, int Quantity);
public record ProductInfo(string Id, string Name, decimal Price);
public record ProcessedOrder
{
public string OrderId { get; set; } = "";
public string CustomerId { get; set; } = "";
public List<ProcessedOrderItem> Items { get; set; } = new();
public decimal GrandTotal { get; set; }
public DateTime CreatedAt { get; set; }
}
public record ProcessedOrderItem
{
public string ProductId { get; set; } = "";
public string ProductName { get; set; } = "";
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Total { get; set; }
}
public interface IOrderRepository
{
Task SaveAsync(ProcessedOrder order, CancellationToken ct);
}
public interface IInventoryService
{
Task<int> CheckStockAsync(string productId, CancellationToken ct);
Task<ProductInfo> GetProductAsync(string productId, CancellationToken ct);
Task ReserveStockAsync(string productId, int quantity, CancellationToken ct);
}
public interface INotificationService
{
Task SendOrderConfirmationAsync(ProcessedOrder order, CancellationToken ct);
}
β Interview Q&A
Q1: Whatβs the Hollywood Principle and how does Template Method implement it? A1: βDonβt call us, weβll call youβ - the base class calls subclass methods, not vice versa. Template Method controls flow; subclasses just provide implementations. This inverts control compared to traditional inheritance.
Q2: How do hooks differ from abstract methods in Template Method? A2: Abstract methods must be overridden (required customization points). Hooks are virtual with default implementations (optional customization). Use abstract for essential steps, hooks for optional behavior or notifications.
Q3: What are the drawbacks of Template Method pattern? A3: Relies on inheritance (limits flexibility), subclasses tightly coupled to base, harder to test in isolation, algorithm structure is fixed. Consider Strategy pattern for more flexibility with composition.
Q4: Can you combine Template Method with other patterns? A4: Yes - use Factory Method to create objects in steps, Strategy to swap algorithm parts via composition, Hook methods for Observer-style notifications. Template Method often appears in frameworks alongside these patterns.
Visitor
Intent: Represent an operation to be performed on elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
π Theory & When to Use
When to Use
- Need to perform many unrelated operations on object structure
- Object structure classes rarely change but operations change frequently
- Want to gather related operations in one class rather than scattering across many
- Add operations to classes without modifying them (Open/Closed Principle)
Real-World Analogy
Like a tax inspector visiting different businesses. Each business type (restaurant, store, factory) accepts the inspector, who applies different tax calculation rules. The businesses donβt change; only the inspectorβs operations vary.
Key Participants
- Visitor: Interface declaring Visit method for each ConcreteElement type
- ConcreteVisitor: Implements operations for each element type
- Element: Interface declaring Accept(visitor) method
- ConcreteElement: Implements Accept, calls visitorβs corresponding Visit method
π UML Diagram
βββββββββββββββββββββββ βββββββββββββββββββββββ
β <<interface>> β β <<interface>> β
β IVisitor β β IElement β
βββββββββββββββββββββββ€ βββββββββββββββββββββββ€
β + VisitA(ElementA) β<βββββββββ + Accept(IVisitor) β
β + VisitB(ElementB) β βββββββββββββββββββββββ
βββββββββββββββββββββββ β³
β³ β
β ββββββββββββΌβββββββββββ
βββββββ΄ββββββ β β
β β βββββββ΄ββββββββ βββββββββ΄ββββββ
βββββ΄βββββ ββββββ΄ββββ β ElementA β β ElementB β
βVisitor1β βVisitor2β βββββββββββββββ€ βββββββββββββββ€
ββββββββββ€ ββββββββββ€ β +Accept(v) β β +Accept(v) β
β+VisitA β β+VisitA β β v.VisitA() β β v.VisitB() β
β+VisitB β β+VisitB β βββββββββββββββ βββββββββββββββ
ββββββββββ ββββββββββ
π» Short Example (~20 lines)
public interface IVisitor
{
void Visit(Circle circle);
void Visit(Rectangle rectangle);
}
public interface IShape
{
void Accept(IVisitor visitor);
}
public class Circle : IShape
{
public double Radius { get; set; }
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
public class Rectangle : IShape
{
public double Width { get; set; }
public double Height { get; set; }
public void Accept(IVisitor visitor) => visitor.Visit(this);
}
public class AreaCalculator : IVisitor
{
public double TotalArea { get; private set; }
public void Visit(Circle c) => TotalArea += Math.PI * c.Radius * c.Radius;
public void Visit(Rectangle r) => TotalArea += r.Width * r.Height;
}
π» Medium Example (~50 lines)
// Document elements
public interface IDocumentElement
{
void Accept(IDocumentVisitor visitor);
}
public interface IDocumentVisitor
{
void Visit(Paragraph paragraph);
void Visit(Image image);
void Visit(Table table);
}
public class Paragraph : IDocumentElement
{
public string Text { get; set; } = "";
public void Accept(IDocumentVisitor visitor) => visitor.Visit(this);
}
public class Image : IDocumentElement
{
public string Url { get; set; } = "";
public int Width { get; set; }
public void Accept(IDocumentVisitor visitor) => visitor.Visit(this);
}
public class Table : IDocumentElement
{
public int Rows { get; set; }
public int Columns { get; set; }
public void Accept(IDocumentVisitor visitor) => visitor.Visit(this);
}
// HTML export visitor
public class HtmlExportVisitor : IDocumentVisitor
{
public StringBuilder Html { get; } = new();
public void Visit(Paragraph p) => Html.AppendLine($"<p>{p.Text}</p>");
public void Visit(Image img) => Html.AppendLine($"<img src=\"{img.Url}\" width=\"{img.Width}\" />");
public void Visit(Table t) => Html.AppendLine($"<table><!-- {t.Rows}x{t.Columns} --></table>");
}
// Word count visitor
public class WordCountVisitor : IDocumentVisitor
{
public int WordCount { get; private set; }
public void Visit(Paragraph p) => WordCount += p.Text.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
public void Visit(Image img) => { } // Images don't have words
public void Visit(Table t) => { } // Simplified - tables might have content
}
// Usage
var document = new List<IDocumentElement>
{
new Paragraph { Text = "Hello world from visitor pattern" },
new Image { Url = "photo.jpg", Width = 800 },
new Table { Rows = 3, Columns = 4 }
};
var htmlExporter = new HtmlExportVisitor();
var wordCounter = new WordCountVisitor();
foreach (var element in document)
{
element.Accept(htmlExporter);
element.Accept(wordCounter);
}
Console.WriteLine(htmlExporter.Html);
Console.WriteLine($"Words: {wordCounter.WordCount}");
π» Production-Grade Example
// Expression tree with visitor for different operations
public interface IExpression
{
T Accept<T>(IExpressionVisitor<T> visitor);
}
public interface IExpressionVisitor<T>
{
T VisitNumber(NumberExpression expr);
T VisitBinary(BinaryExpression expr);
T VisitUnary(UnaryExpression expr);
T VisitVariable(VariableExpression expr);
T VisitFunction(FunctionCallExpression expr);
}
// Expression types
public class NumberExpression : IExpression
{
public double Value { get; }
public NumberExpression(double value) => Value = value;
public T Accept<T>(IExpressionVisitor<T> visitor) => visitor.VisitNumber(this);
}
public class VariableExpression : IExpression
{
public string Name { get; }
public VariableExpression(string name) => Name = name;
public T Accept<T>(IExpressionVisitor<T> visitor) => visitor.VisitVariable(this);
}
public class BinaryExpression : IExpression
{
public IExpression Left { get; }
public IExpression Right { get; }
public string Operator { get; }
public BinaryExpression(IExpression left, string op, IExpression right)
{
Left = left;
Operator = op;
Right = right;
}
public T Accept<T>(IExpressionVisitor<T> visitor) => visitor.VisitBinary(this);
}
public class UnaryExpression : IExpression
{
public string Operator { get; }
public IExpression Operand { get; }
public UnaryExpression(string op, IExpression operand)
{
Operator = op;
Operand = operand;
}
public T Accept<T>(IExpressionVisitor<T> visitor) => visitor.VisitUnary(this);
}
public class FunctionCallExpression : IExpression
{
public string FunctionName { get; }
public IReadOnlyList<IExpression> Arguments { get; }
public FunctionCallExpression(string name, params IExpression[] args)
{
FunctionName = name;
Arguments = args;
}
public T Accept<T>(IExpressionVisitor<T> visitor) => visitor.VisitFunction(this);
}
// Evaluator visitor
public class EvaluatorVisitor : IExpressionVisitor<double>
{
private readonly Dictionary<string, double> _variables;
private readonly Dictionary<string, Func<double[], double>> _functions;
public EvaluatorVisitor(Dictionary<string, double>? variables = null)
{
_variables = variables ?? new Dictionary<string, double>();
_functions = new Dictionary<string, Func<double[], double>>
{
["sin"] = args => Math.Sin(args[0]),
["cos"] = args => Math.Cos(args[0]),
["sqrt"] = args => Math.Sqrt(args[0]),
["pow"] = args => Math.Pow(args[0], args[1]),
["max"] = args => args.Max(),
["min"] = args => args.Min()
};
}
public double VisitNumber(NumberExpression expr) => expr.Value;
public double VisitVariable(VariableExpression expr)
{
if (_variables.TryGetValue(expr.Name, out var value))
return value;
throw new InvalidOperationException($"Undefined variable: {expr.Name}");
}
public double VisitBinary(BinaryExpression expr)
{
var left = expr.Left.Accept(this);
var right = expr.Right.Accept(this);
return expr.Operator switch
{
"+" => left + right,
"-" => left - right,
"*" => left * right,
"/" => right != 0 ? left / right : throw new DivideByZeroException(),
"^" => Math.Pow(left, right),
"%" => left % right,
_ => throw new NotSupportedException($"Unknown operator: {expr.Operator}")
};
}
public double VisitUnary(UnaryExpression expr)
{
var operand = expr.Operand.Accept(this);
return expr.Operator switch
{
"-" => -operand,
"+" => operand,
_ => throw new NotSupportedException($"Unknown unary operator: {expr.Operator}")
};
}
public double VisitFunction(FunctionCallExpression expr)
{
if (!_functions.TryGetValue(expr.FunctionName, out var func))
throw new InvalidOperationException($"Unknown function: {expr.FunctionName}");
var args = expr.Arguments.Select(a => a.Accept(this)).ToArray();
return func(args);
}
}
// Pretty printer visitor
public class PrettyPrintVisitor : IExpressionVisitor<string>
{
public string VisitNumber(NumberExpression expr) => expr.Value.ToString("G");
public string VisitVariable(VariableExpression expr) => expr.Name;
public string VisitBinary(BinaryExpression expr)
{
var left = expr.Left.Accept(this);
var right = expr.Right.Accept(this);
return $"({left} {expr.Operator} {right})";
}
public string VisitUnary(UnaryExpression expr)
{
var operand = expr.Operand.Accept(this);
return $"{expr.Operator}{operand}";
}
public string VisitFunction(FunctionCallExpression expr)
{
var args = string.Join(", ", expr.Arguments.Select(a => a.Accept(this)));
return $"{expr.FunctionName}({args})";
}
}
// Complexity analyzer visitor
public class ComplexityVisitor : IExpressionVisitor<int>
{
public int VisitNumber(NumberExpression expr) => 1;
public int VisitVariable(VariableExpression expr) => 1;
public int VisitBinary(BinaryExpression expr)
=> 1 + expr.Left.Accept(this) + expr.Right.Accept(this);
public int VisitUnary(UnaryExpression expr)
=> 1 + expr.Operand.Accept(this);
public int VisitFunction(FunctionCallExpression expr)
=> 1 + expr.Arguments.Sum(a => a.Accept(this));
}
// Variable collector visitor
public class VariableCollectorVisitor : IExpressionVisitor<HashSet<string>>
{
public HashSet<string> VisitNumber(NumberExpression expr) => new();
public HashSet<string> VisitVariable(VariableExpression expr) => new() { expr.Name };
public HashSet<string> VisitBinary(BinaryExpression expr)
{
var vars = expr.Left.Accept(this);
vars.UnionWith(expr.Right.Accept(this));
return vars;
}
public HashSet<string> VisitUnary(UnaryExpression expr) => expr.Operand.Accept(this);
public HashSet<string> VisitFunction(FunctionCallExpression expr)
{
var vars = new HashSet<string>();
foreach (var arg in expr.Arguments)
vars.UnionWith(arg.Accept(this));
return vars;
}
}
// Usage example
public class ExpressionDemo
{
public void Run()
{
// Build expression: sqrt(x^2 + y^2)
IExpression expr = new FunctionCallExpression("sqrt",
new BinaryExpression(
new BinaryExpression(new VariableExpression("x"), "^", new NumberExpression(2)),
"+",
new BinaryExpression(new VariableExpression("y"), "^", new NumberExpression(2))
)
);
// Different visitors for different operations
var printer = new PrettyPrintVisitor();
Console.WriteLine($"Expression: {expr.Accept(printer)}");
var complexity = new ComplexityVisitor();
Console.WriteLine($"Complexity: {expr.Accept(complexity)} nodes");
var varCollector = new VariableCollectorVisitor();
Console.WriteLine($"Variables: {string.Join(", ", expr.Accept(varCollector))}");
var evaluator = new EvaluatorVisitor(new Dictionary<string, double> { ["x"] = 3, ["y"] = 4 });
Console.WriteLine($"Result: {expr.Accept(evaluator)}"); // 5 (3-4-5 triangle)
}
}
// AST visitor base with default implementations
public abstract class ExpressionVisitorBase<T> : IExpressionVisitor<T>
{
public virtual T VisitNumber(NumberExpression expr) => Default(expr);
public virtual T VisitVariable(VariableExpression expr) => Default(expr);
public virtual T VisitBinary(BinaryExpression expr) => Default(expr);
public virtual T VisitUnary(UnaryExpression expr) => Default(expr);
public virtual T VisitFunction(FunctionCallExpression expr) => Default(expr);
protected virtual T Default(IExpression expr)
=> throw new NotImplementedException($"Visit not implemented for {expr.GetType().Name}");
}
β Interview Q&A
Q1: What problem does the Visitor pattern solve? A1: It allows adding new operations to existing class hierarchies without modifying them. Operations are encapsulated in visitor classes, keeping element classes stable while freely extending behavior.
Q2: Whatβs βdouble dispatchβ in the Visitor pattern? A2: The operation depends on both the visitor type and element type. First dispatch: element.Accept(visitor) selects based on element. Second dispatch: visitor.Visit(element) selects based on visitor. This achieves polymorphism on two dimensions.
Q3: What are the drawbacks of Visitor pattern? A3: Adding new element types requires changing all visitors (breaks Open/Closed for elements). Visitors may need access to element internals (breaks encapsulation). Can be complex for simple scenarios. Not suitable when element hierarchy changes frequently.
Q4: How does Visitor compare to pattern matching in modern C#?
A4: C# switch expressions with pattern matching (switch (expr) { case Circle c => ... }) can replace simple visitors. Visitor is still better for complex operations, multiple related operations, or when you need to accumulate state across visits.
Quick Reference
| Pattern | Use Case | Key Benefit |
|---|---|---|
| Chain of Responsibility | Pass request along handlers | Decouples sender from receivers |
| Command | Encapsulate requests as objects | Undo/redo, logging, queuing |
| Iterator | Sequential access to collection | Hide internal structure |
| Mediator | Coordinate object interaction | Reduces coupling between objects |
| Observer | One-to-many change notification | Loose coupling, reactive updates |
| Memento | Capture and restore object state | Undo/redo without exposing internals |
| State | Behavior varies with internal state | Clean state machine implementation |
| Strategy | Swap algorithms at runtime | Algorithm flexibility without subclassing |
| Template Method | Algorithm skeleton with custom steps | Reuse structure, vary details |
| Visitor | Add operations without changing classes | Separate algorithms from object structure |
See Also
- Creational Patterns - Factory, Abstract Factory, Builder, Prototype, Singleton
- Structural Patterns - Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy