C# Advanced Patterns

Once the fundamentals of C# are mastered, exploring advanced patterns opens up powerful capabilities for building robust, maintainable, and scalable applications. This post covers advanced C# features and patterns that are essential for professional software development and platform engineering.

Generics

Generics enable writing type-safe code that works with any data type while maintaining compile-time type checking.

Generic Classes

public class GenericRepository<T> where T : class { private List<T> items = new List<T>(); public void Add(T item) { items.Add(item); } public T GetById(int id) { return items[id]; } public IEnumerable<T> GetAll() { return items; } } // Usage var userRepository = new GenericRepository<User>(); userRepository.Add(new User { Id = 1, Name = "Alice" });

Generic Methods

public class DataProcessor { public T ProcessData<T>(T input) where T : IComparable<T> { // Process data logic return input; } public void Swap<T>(ref T a, ref T b) { T temp = a; a = b; b = temp; } } // Usage int x = 5, y = 10; processor.Swap(ref x, ref y);

Generic Constraints

// Class constraint public class Manager<T> where T : class { } // Interface constraint public class Processor<T> where T : IDisposable { } // Base class constraint public class EntityManager<T> where T : BaseEntity { } // Constructor constraint public class Factory<T> where T : new() { public T CreateInstance() { return new T(); } } // Multiple constraints public class Service<T> where T : class, IDisposable, new() { }
graph TD A[Generic Type T] --> B{Constraints?} B -->|where T : class| C[Must be reference type] B -->|where T : struct| D[Must be value type] B -->|where T : Interface| E[Must implement interface] B -->|where T : new| F[Must have parameterless constructor] B -->|No constraint| G[Any type allowed]

Delegates and Events

Delegates are type-safe function pointers that enable callback methods and event-driven programming.

Delegates

// Delegate declaration public delegate void NotificationHandler(string message); public class Notifier { public NotificationHandler OnNotify; public void Notify(string message) { OnNotify?.Invoke(message); } } // Usage var notifier = new Notifier(); notifier.OnNotify += (msg) => Console.WriteLine($"Received: {msg}"); notifier.Notify("Hello World"); // Built-in delegates Func<int, int, int> add = (a, b) => a + b; Action<string> print = (msg) => Console.WriteLine(msg); Predicate<int> isEven = (n) => n % 2 == 0;

Events

public class OrderProcessor { // Event declaration public event EventHandler<OrderEventArgs> OrderPlaced; public event EventHandler OrderCancelled; public void PlaceOrder(Order order) { // Process order OnOrderPlaced(new OrderEventArgs { Order = order }); } protected virtual void OnOrderPlaced(OrderEventArgs e) { OrderPlaced?.Invoke(this, e); } } public class OrderEventArgs : EventArgs { public Order Order { get; set; } } // Subscriber var processor = new OrderProcessor(); processor.OrderPlaced += (sender, e) => { Console.WriteLine($"Order placed: {e.Order.Id}"); };
sequenceDiagram participant Publisher participant Event participant Subscriber1 participant Subscriber2 Subscriber1->>Event: Subscribe Subscriber2->>Event: Subscribe Publisher->>Event: Raise Event Event->>Subscriber1: Notify Event->>Subscriber2: Notify

Extension Methods

Extension methods add functionality to existing types without modifying them.

public static class StringExtensions { public static bool IsValidEmail(this string email) { return email.Contains("@") && email.Contains("."); } public static string Truncate(this string value, int maxLength) { if (string.IsNullOrEmpty(value)) return value; return value.Length <= maxLength ? value : value.Substring(0, maxLength); } } public static class CollectionExtensions { public static void AddRange<T>(this ICollection<T> collection, IEnumerable<T> items) { foreach (var item in items) { collection.Add(item); } } } // Usage string email = "user@example.com"; bool isValid = email.IsValidEmail(); string text = "Long text here".Truncate(10);

LINQ Advanced

LINQ provides powerful query capabilities beyond basic filtering.

var orders = new List<Order> { new Order { Id = 1, CustomerId = 1, Amount = 100 }, new Order { Id = 2, CustomerId = 2, Amount = 200 }, new Order { Id = 3, CustomerId = 1, Amount = 150 } }; // Grouping var ordersByCustomer = orders .GroupBy(o => o.CustomerId) .Select(g => new { CustomerId = g.Key, TotalAmount = g.Sum(o => o.Amount), OrderCount = g.Count() }); // Joins var customers = new List<Customer>(); var customerOrders = from c in customers join o in orders on c.Id equals o.CustomerId select new { c.Name, o.Amount }; // Projection var summary = orders.Select(o => new OrderSummary { OrderId = o.Id, Total = o.Amount * 1.1m // Add 10% tax }); // Aggregation var stats = new { Total = orders.Sum(o => o.Amount), Average = orders.Average(o => o.Amount), Max = orders.Max(o => o.Amount), Count = orders.Count() }; // Paging int pageSize = 10; int pageNumber = 2; var pagedResults = orders .Skip((pageNumber - 1) * pageSize) .Take(pageSize) .ToList();

Nullable Reference Types (C# 8.0+)

Nullable reference types help prevent null reference exceptions.

#nullable enable public class User { // Non-nullable reference type (must not be null) public string Name { get; set; } = string.Empty; // Nullable reference type (can be null) public string? MiddleName { get; set; } // Non-nullable with constructor public User(string name) { Name = name; } } // Usage User user = new User("Alice"); string name = user.Name; // Safe, cannot be null string? middle = user.MiddleName; // May be null // Null-forgiving operator string middleUpper = user.MiddleName!.ToUpper(); // Tells compiler it's not null // Null-conditional operators int? length = user.MiddleName?.Length; string result = user.MiddleName ?? "No middle name";

Pattern Matching

Pattern matching provides concise syntax for type checking and value extraction.

// Type patterns object obj = "Hello"; if (obj is string str) { Console.WriteLine($"String length: {str.Length}"); } // Property patterns public decimal CalculateDiscount(Order order) => order switch { { Amount: > 1000 } => order.Amount * 0.1m, { Amount: > 500 } => order.Amount * 0.05m, { Amount: > 100 } => order.Amount * 0.02m, _ => 0 }; // Tuple patterns public string GetQuadrant(int x, int y) => (x, y) switch { (> 0, > 0) => "Quadrant I", (< 0, > 0) => "Quadrant II", (< 0, < 0) => "Quadrant III", (> 0, < 0) => "Quadrant IV", (0, 0) => "Origin", _ => "On axis" }; // Relational patterns public string ClassifyAge(int age) => age switch { < 13 => "Child", >= 13 and < 20 => "Teenager", >= 20 and < 65 => "Adult", >= 65 => "Senior", _ => "Unknown" };

Records (C# 9.0+)

Records provide immutable reference types with value-based equality.

// Record declaration public record Person(string FirstName, string LastName, int Age); // Usage var person1 = new Person("Alice", "Smith", 30); var person2 = person1 with { Age = 31 }; // Non-destructive mutation // Value-based equality var person3 = new Person("Alice", "Smith", 30); bool isEqual = person1 == person3; // True (same values) // Record with custom members public record Product(int Id, string Name, decimal Price) { public decimal DiscountedPrice => Price * 0.9m; public void PrintInfo() { Console.WriteLine($"{Name}: ${Price}"); } } // Record inheritance public record Customer(string FirstName, string LastName, int Age, string Email) : Person(FirstName, LastName, Age);

Dependency Injection Pattern

Dependency injection promotes loose coupling and testability.

// Interface definition public interface IEmailService { void SendEmail(string to, string subject, string body); } // Implementation public class SmtpEmailService : IEmailService { private readonly string smtpServer; public SmtpEmailService(string smtpServer) { this.smtpServer = smtpServer; } public void SendEmail(string to, string subject, string body) { // SMTP implementation } } // Consumer with dependency injection public class UserService { private readonly IEmailService emailService; public UserService(IEmailService emailService) { this.emailService = emailService; } public void RegisterUser(User user) { // Save user emailService.SendEmail(user.Email, "Welcome", "Welcome to our service"); } } // Setup with DI container (using Microsoft.Extensions.DependencyInjection) var services = new ServiceCollection(); services.AddSingleton<IEmailService>(new SmtpEmailService("smtp.example.com")); services.AddTransient<UserService>(); var provider = services.BuildServiceProvider(); var userService = provider.GetService<UserService>();
graph LR A[Client] --> B[Interface] C[Implementation 1] -.implements.-> B D[Implementation 2] -.implements.-> B E[DI Container] --> C E --> D A -.depends on.-> E

Repository Pattern

The repository pattern abstracts data access logic.

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 DbContext context; public UserRepository(DbContext context) { this.context = context; } public async Task<User> GetByIdAsync(int id) { return await context.Users.FindAsync(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(); } public async Task UpdateAsync(User entity) { context.Users.Update(entity); await context.SaveChangesAsync(); } public async Task DeleteAsync(int id) { var user = await GetByIdAsync(id); if (user != null) { context.Users.Remove(user); await context.SaveChangesAsync(); } } }

Builder Pattern

The builder pattern constructs complex objects step by step.

public class EmailMessage { public string To { get; set; } public string From { get; set; } public string Subject { get; set; } public string Body { get; set; } public List<string> Attachments { get; set; } = new List<string>(); } public class EmailMessageBuilder { private readonly EmailMessage message = new EmailMessage(); public EmailMessageBuilder To(string to) { message.To = to; return this; } public EmailMessageBuilder From(string from) { message.From = from; return this; } public EmailMessageBuilder WithSubject(string subject) { message.Subject = subject; return this; } public EmailMessageBuilder WithBody(string body) { message.Body = body; return this; } public EmailMessageBuilder AddAttachment(string path) { message.Attachments.Add(path); return this; } public EmailMessage Build() { return message; } } // Usage var email = new EmailMessageBuilder() .To("user@example.com") .From("admin@example.com") .WithSubject("Welcome") .WithBody("Hello and welcome!") .AddAttachment("/path/to/file.pdf") .Build();

Practical Example: Advanced Service Layer

using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; public record OrderDto(int Id, int CustomerId, decimal Amount, DateTime OrderDate); public interface IOrderService { Task<OrderDto> CreateOrderAsync(CreateOrderRequest request); Task<IEnumerable<OrderDto>> GetOrdersByCustomerAsync(int customerId); Task<OrderStatistics> GetOrderStatisticsAsync(); } public class OrderService : IOrderService { private readonly IRepository<Order> orderRepository; private readonly IEventPublisher eventPublisher; public OrderService(IRepository<Order> orderRepository, IEventPublisher eventPublisher) { this.orderRepository = orderRepository; this.eventPublisher = eventPublisher; } public async Task<OrderDto> CreateOrderAsync(CreateOrderRequest request) { var order = new Order { CustomerId = request.CustomerId, Amount = request.Amount, OrderDate = DateTime.UtcNow }; await orderRepository.AddAsync(order); await eventPublisher.PublishAsync(new OrderCreatedEvent(order.Id)); return order.ToDto(); } public async Task<IEnumerable<OrderDto>> GetOrdersByCustomerAsync(int customerId) { var orders = await orderRepository.GetAllAsync(); return orders .Where(o => o.CustomerId == customerId) .Select(o => o.ToDto()); } public async Task<OrderStatistics> GetOrderStatisticsAsync() { var orders = await orderRepository.GetAllAsync(); return new OrderStatistics { TotalOrders = orders.Count(), TotalRevenue = orders.Sum(o => o.Amount), AverageOrderValue = orders.Average(o => o.Amount), OrdersByMonth = orders .GroupBy(o => new { o.OrderDate.Year, o.OrderDate.Month }) .Select(g => new MonthlyStats { Year = g.Key.Year, Month = g.Key.Month, Count = g.Count(), Revenue = g.Sum(o => o.Amount) }) .ToList() }; } } public static class OrderExtensions { public static OrderDto ToDto(this Order order) { return new OrderDto(order.Id, order.CustomerId, order.Amount, order.OrderDate); } }

Key Takeaways