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
- Generics enable type-safe reusable code with compile-time type checking and constraint validation
- Delegates and events provide type-safe callbacks for event-driven programming patterns
- Extension methods add functionality to existing types without modification
- LINQ advanced features include grouping, joins, projections, and aggregations for complex queries
- Nullable reference types help prevent null reference exceptions at compile time
- Pattern matching provides concise syntax for type checking, value extraction, and conditional logic
- Records offer immutable reference types with value-based equality and concise syntax
- Dependency injection promotes loose coupling, testability, and maintainable code architecture
- Repository pattern abstracts data access logic and separates concerns
- Builder pattern constructs complex objects step by step with a fluent interface
- Modern C# features significantly improve code readability and maintainability
- Understanding these patterns is essential for building professional, scalable applications