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!