Modern C# Design Patterns
Modern C# Design Patterns
C# has evolved significantly over the years, and with it, the way we implement design patterns. Let's explore some common patterns with modern C# syntax.
Repository Pattern
The Repository pattern abstracts data access, making your code more testable:
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
}
public class UserRepository : IRepository<User>
{
private readonly AppDbContext _context;
public UserRepository(AppDbContext context)
{
_context = context;
}
public async Task<User?> GetByIdAsync(int id)
{
return await _context.Users
.FirstOrDefaultAsync(u => u.Id == id);
}
public async Task<IEnumerable<User>> GetAllAsync()
{
return await _context.Users.ToListAsync();
}
public async Task AddAsync(User entity)
{
await _context.Users.AddAsync(entity);
await _context.SaveChangesAsync();
}
}
Builder Pattern with Fluent API
The Builder pattern is perfect for creating complex objects step by step:
public class EmailBuilder
{
private readonly Email _email = new();
public EmailBuilder To(string recipient)
{
_email.Recipients.Add(recipient);
return this;
}
public EmailBuilder WithSubject(string subject)
{
_email.Subject = subject;
return this;
}
public EmailBuilder WithBody(string body)
{
_email.Body = body;
return this;
}
public EmailBuilder WithAttachment(string path)
{
_email.Attachments.Add(path);
return this;
}
public Email Build() => _email;
}
// Usage
var email = new EmailBuilder()
.To("user@example.com")
.WithSubject("Hello!")
.WithBody("This is a test email.")
.WithAttachment("/docs/report.pdf")
.Build();
Strategy Pattern
The Strategy pattern defines a family of algorithms and makes them interchangeable:
public interface IPaymentStrategy
{
Task<PaymentResult> ProcessPaymentAsync(decimal amount);
}
public class CreditCardPayment : IPaymentStrategy
{
private readonly string _cardNumber;
public CreditCardPayment(string cardNumber)
{
_cardNumber = cardNumber;
}
public async Task<PaymentResult> ProcessPaymentAsync(decimal amount)
{
// Process credit card payment
await Task.Delay(100); // Simulate API call
return new PaymentResult { Success = true, TransactionId = Guid.NewGuid().ToString() };
}
}
public class PayPalPayment : IPaymentStrategy
{
private readonly string _email;
public PayPalPayment(string email)
{
_email = email;
}
public async Task<PaymentResult> ProcessPaymentAsync(decimal amount)
{
// Process PayPal payment
await Task.Delay(100);
return new PaymentResult { Success = true, TransactionId = Guid.NewGuid().ToString() };
}
}
// Usage with Dependency Injection
public class CheckoutService
{
private readonly IPaymentStrategy _paymentStrategy;
public CheckoutService(IPaymentStrategy paymentStrategy)
{
_paymentStrategy = paymentStrategy;
}
public async Task<PaymentResult> ProcessOrderAsync(Order order)
{
return await _paymentStrategy.ProcessPaymentAsync(order.Total);
}
}
Observer Pattern with Events
C# makes the Observer pattern easy with built-in events:
public class StockPriceMonitor
{
public event EventHandler<StockPriceChangedEventArgs>? PriceChanged;
private decimal _price;
public decimal Price
{
get => _price;
set
{
if (_price != value)
{
var oldPrice = _price;
_price = value;
OnPriceChanged(oldPrice, value);
}
}
}
protected virtual void OnPriceChanged(decimal oldPrice, decimal newPrice)
{
PriceChanged?.Invoke(this, new StockPriceChangedEventArgs
{
OldPrice = oldPrice,
NewPrice = newPrice,
Change = newPrice - oldPrice
});
}
}
public class StockPriceChangedEventArgs : EventArgs
{
public decimal OldPrice { get; init; }
public decimal NewPrice { get; init; }
public decimal Change { get; init; }
}
// Usage
var monitor = new StockPriceMonitor();
monitor.PriceChanged += (sender, e) =>
{
Console.WriteLine($"Price changed: {e.OldPrice:C} -> {e.NewPrice:C} ({e.Change:+0.00;-0.00})");
};
monitor.Price = 150.50m;
monitor.Price = 152.75m;
Conclusion
These patterns help create maintainable, testable, and scalable C# applications. The key is knowing when to apply them - don't over-engineer simple solutions!