← Back to Blog

Performance Profiling and Optimization: Tools and Techniques

I've spent countless hours debugging performance issues that turned out to be simple oversights, and I've optimized code that was already running fast enough. Performance work is equal parts art, science, and experience. The key insight I've learned is that you can't optimize what you can't measure, and you shouldn't optimize what doesn't matter.

For C# developers, performance optimization isn't just about writing faster code-it's about understanding the .NET runtime, identifying bottlenecks, and applying the right tools at the right time. Whether you're building high-throughput APIs, real-time processing systems, or memory-constrained applications, the techniques in this guide will help you deliver consistently fast, efficient code.

In this comprehensive guide, we'll explore the complete performance optimization workflow: from identifying issues with profiling tools, to measuring improvements with benchmarks, to implementing optimizations that actually matter. You'll learn practical techniques that work in real-world applications.

By the end, you'll have a systematic approach to performance optimization that you can apply to any C# application.

Why Performance Matters: Setting Expectations

Before diving into tools and techniques, let's talk about why performance matters and when it doesn't. I've seen teams spend weeks optimizing code that users never noticed was slow, while ignoring obvious bottlenecks that frustrated customers daily.

Performance optimization should be driven by: - User experience: Response times that feel sluggish or cause frustration - Business requirements: SLAs, throughput requirements, or cost constraints - Scalability limits: Applications that can't handle expected load - Resource constraints: Memory limits, CPU usage, or infrastructure costs

The 80/20 rule applies heavily here: 80% of performance issues come from 20% of the code. Your job is to find that 20% and fix it. Everything else is usually not worth the complexity it adds.

Key Principle: Performance optimization is about making the right code fast, not making all code fast. Focus on bottlenecks that actually impact users.

Performance Profiling Fundamentals: Your First Line of Defense

Profiling is the systematic process of measuring your application's behavior to identify performance issues. Before you can optimize, you need to know what's slow and why.

// Basic performance monitoring with Stopwatch
public class PerformanceMonitor
{
    private readonly Stopwatch _stopwatch = new();
    private readonly ILogger _logger;

    public PerformanceMonitor(ILogger logger)
    {
        _logger = logger;
    }

    public async Task MeasureAsync(string operationName, Func> operation)
    {
        _stopwatch.Restart();

        try
        {
            var result = await operation();
            var elapsed = _stopwatch.ElapsedMilliseconds;

            _logger.LogInformation("Operation {OperationName} completed in {Elapsed}ms",
                operationName, elapsed);

            // Alert on performance thresholds
            if (elapsed > 1000) // 1 second threshold
            {
                _logger.LogWarning("Performance threshold exceeded: {OperationName} took {Elapsed}ms",
                    operationName, elapsed);
            }

            return result;
        }
        catch (Exception ex)
        {
            var elapsed = _stopwatch.ElapsedMilliseconds;
            _logger.LogError(ex, "Operation {OperationName} failed after {Elapsed}ms",
                operationName, elapsed);
            throw;
        }
    }

    public T Measure(string operationName, Func operation)
    {
        _stopwatch.Restart();

        try
        {
            var result = operation();
            var elapsed = _stopwatch.ElapsedMilliseconds;

            _logger.LogInformation("Operation {OperationName} completed in {Elapsed}ms",
                operationName, elapsed);

            return result;
        }
        catch (Exception ex)
        {
            var elapsed = _stopwatch.ElapsedMilliseconds;
            _logger.LogError(ex, "Operation {OperationName} failed after {Elapsed}ms",
                operationName, elapsed);
            throw;
        }
    }
}

// Usage in a service layer
public class OrderProcessingService
{
    private readonly PerformanceMonitor _monitor;
    private readonly IOrderRepository _repository;
    private readonly IPaymentService _paymentService;

    public OrderProcessingService(
        PerformanceMonitor monitor,
        IOrderRepository repository,
        IPaymentService paymentService)
    {
        _monitor = monitor;
        _repository = repository;
        _paymentService = paymentService;
    }

    public async Task ProcessOrderAsync(CreateOrderRequest request)
    {
        return await _monitor.MeasureAsync("ProcessOrder", async () =>
        {
            // Measure each step
            var validationResult = await _monitor.MeasureAsync("ValidateOrder",
                () => ValidateOrderAsync(request));

            if (!validationResult.IsValid)
                throw new ValidationException(validationResult.Errors);

            var order = await _monitor.MeasureAsync("CreateOrder",
                () => CreateOrderAsync(request, validationResult));

            var paymentResult = await _monitor.MeasureAsync("ProcessPayment",
                () => _paymentService.ProcessPaymentAsync(order.Id, order.Total));

            if (!paymentResult.Success)
            {
                await _monitor.MeasureAsync("CancelOrder",
                    () => CancelOrderAsync(order.Id));
                throw new PaymentException("Payment failed");
            }

            await _monitor.MeasureAsync("ConfirmOrder",
                () => ConfirmOrderAsync(order.Id));

            return order;
        });
    }
}

This basic monitoring gives you immediate visibility into where time is being spent. The key insight is that you need to measure before you can optimize. Without measurements, you're just guessing.

Let's create a more sophisticated monitoring system that tracks trends and provides alerts.

// Advanced performance tracking with metrics
public class PerformanceMetrics
{
    private readonly ConcurrentDictionary _stats = new();
    private readonly ILogger _logger;

    public PerformanceMetrics(ILogger logger)
    {
        _logger = logger;
    }

    public async Task TrackAsync(string operationName, Func> operation)
    {
        var startTime = Stopwatch.GetTimestamp();

        try
        {
            var result = await operation();
            var duration = Stopwatch.GetElapsedTime(startTime);

            UpdateStats(operationName, duration, true);
            return result;
        }
        catch (Exception ex)
        {
            var duration = Stopwatch.GetElapsedTime(startTime);
            UpdateStats(operationName, duration, false);

            _logger.LogError(ex, "Operation {OperationName} failed after {Duration}",
                operationName, duration);
            throw;
        }
    }

    private void UpdateStats(string operationName, TimeSpan duration, bool success)
    {
        var stats = _stats.GetOrAdd(operationName, _ => new PerformanceStats());

        lock (stats)
        {
            stats.TotalCalls++;
            stats.TotalDuration += duration;

            if (success)
                stats.SuccessfulCalls++;
            else
                stats.FailedCalls++;

            if (duration > stats.MaxDuration)
                stats.MaxDuration = duration;

            if (duration < stats.MinDuration || stats.MinDuration == TimeSpan.Zero)
                stats.MinDuration = duration;

            // Keep only last 1000 calls for rolling average
            stats.RecentDurations.Enqueue(duration);
            if (stats.RecentDurations.Count > 1000)
                stats.RecentDurations.Dequeue();
        }

        // Log performance issues
        if (duration > TimeSpan.FromSeconds(5))
        {
            _logger.LogWarning("Slow operation detected: {OperationName} took {Duration}",
                operationName, duration);
        }
    }

    public PerformanceStats GetStats(string operationName)
    {
        return _stats.TryGetValue(operationName, out var stats) ? stats : null;
    }

    public IEnumerable> GetAllStats()
    {
        return _stats.ToArray(); // Create snapshot
    }
}

public class PerformanceStats
{
    public long TotalCalls { get; set; }
    public long SuccessfulCalls { get; set; }
    public long FailedCalls { get; set; }
    public TimeSpan TotalDuration { get; set; }
    public TimeSpan MaxDuration { get; set; }
    public TimeSpan MinDuration { get; set; }
    public Queue RecentDurations { get; } = new();

    public TimeSpan AverageDuration =>
        TotalCalls > 0 ? TotalDuration / TotalCalls : TimeSpan.Zero;

    public double SuccessRate =>
        TotalCalls > 0 ? (double)SuccessfulCalls / TotalCalls : 0;

    public TimeSpan RecentAverageDuration =>
        RecentDurations.Any() ? TimeSpan.FromTicks((long)RecentDurations.Average(d => d.Ticks)) : TimeSpan.Zero;
}

This metrics system provides ongoing visibility into your application's performance. You can use it to identify trends, set up alerts, and make data-driven optimization decisions.

Memory Profiling: Finding and Fixing Memory Issues

Memory issues are often invisible until they cause problems. A memory leak might not crash your application immediately, but it will eventually lead to performance degradation and out-of-memory errors.

// Memory leak detection
public class MemoryLeakDetector
{
    private static readonly Dictionary _objectTracker = new();
    private static readonly object _trackerLock = new();

    public static void TrackObject(string name, object obj)
    {
        lock (_trackerLock)
        {
            _objectTracker[name] = new WeakReference(obj);
        }
    }

    public static void ReportLiveObjects()
    {
        lock (_trackerLock)
        {
            Console.WriteLine("Live objects:");
            foreach (var kvp in _objectTracker)
            {
                if (kvp.Value.IsAlive)
                {
                    Console.WriteLine($"  {kvp.Key}: {kvp.Value.Target?.GetType().Name ?? "null"}");
                }
            }
        }
    }

    public static void AnalyzeMemoryUsage()
    {
        var process = Process.GetCurrentProcess();

        Console.WriteLine("Memory Analysis:");
        Console.WriteLine($"  Working Set: {process.WorkingSet64 / 1024 / 1024} MB");
        Console.WriteLine($"  Private Memory: {process.PrivateMemorySize64 / 1024 / 1024} MB");
        Console.WriteLine($"  Virtual Memory: {process.VirtualMemorySize64 / 1024 / 1024} MB");
        Console.WriteLine($"  GC Total Memory: {GC.GetTotalMemory(false) / 1024 / 1024} MB");

        // Force GC for analysis
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        Console.WriteLine($"  After GC - Working Set: {process.WorkingSet64 / 1024 / 1024} MB");
        Console.WriteLine($"  After GC - Total Memory: {GC.GetTotalMemory(true) / 1024 / 1024} MB");
    }
}

// Common memory issues and solutions
public class MemoryOptimizationExamples
{
    // PROBLEM: String concatenation in loops
    public string BadStringBuilding(IEnumerable items)
    {
        string result = string.Empty;
        foreach (var item in items)
        {
            result += item; // Creates new string each iteration
        }
        return result;
    }

    // SOLUTION: Use StringBuilder
    public string GoodStringBuilding(IEnumerable items)
    {
        var builder = new StringBuilder();
        foreach (var item in items)
        {
            builder.Append(item);
        }
        return builder.ToString();
    }

    // PROBLEM: Boxing value types unnecessarily
    public void BadBoxingExample()
    {
        int value = 42;
        object boxed = value; // Boxing occurs
        Console.WriteLine($"Value: {boxed}"); // Unboxing occurs
    }

    // SOLUTION: Use generics to avoid boxing
    public void GoodGenericExample(T value) where T : struct
    {
        Console.WriteLine($"Value: {value}"); // No boxing/unboxing
    }

    // PROBLEM: Large object heap (LOH) fragmentation
    private static List _largeObjects = new();

    public void BadLargeObjectUsage()
    {
        for (int i = 0; i < 100; i++)
        {
            _largeObjects.Add(new byte[100000]); // >85KB goes to LOH
        }
    }

    // SOLUTION: Object pooling for large objects
    private static ObjectPool _bufferPool =
        ObjectPool.Create(() => new byte[100000]);

    public void GoodLargeObjectUsage()
    {
        var buffer = _bufferPool.Get();
        try
        {
            // Use buffer...
            Array.Fill(buffer, (byte)0);
        }
        finally
        {
            _bufferPool.Return(buffer);
        }
    }

    // PROBLEM: Closures capturing large objects
    public IEnumerable BadClosureCapture(List largeList)
    {
        foreach (var item in largeList)
        {
            yield return item;
        }
    }

    // SOLUTION: Avoid capturing large objects in closures
    public IEnumerable GoodClosureUsage(IEnumerable items)
    {
        foreach (var item in items)
        {
            yield return item;
        }
    }
}

Memory optimization is about understanding how the .NET garbage collector works and minimizing allocations. The key insights are: - Small objects (<85KB) go on the regular heap and are collected frequently - Large objects go on the Large Object Heap (LOH) and are collected less frequently - Boxing/unboxing creates unnecessary allocations - String operations can be allocation-heavy if not done carefully

Let's look at a more sophisticated memory analysis tool.

// Advanced memory analysis
public class MemoryAnalyzer
{
    private readonly Dictionary _baselineMemory = new();
    private readonly ILogger _logger;

    public MemoryAnalyzer(ILogger logger)
    {
        _logger = logger;
    }

    public IDisposable StartAnalysis(string operationName)
    {
        return new MemoryAnalysisScope(operationName, this);
    }

    public void SetBaseline(string name)
    {
        _baselineMemory[name] = GC.GetTotalMemory(true);
        _logger.LogInformation("Memory baseline set for {Name}: {Memory} bytes", name, _baselineMemory[name]);
    }

    public void ReportMemoryUsage(string operationName, long startMemory)
    {
        var endMemory = GC.GetTotalMemory(false);
        var memoryDelta = endMemory - startMemory;

        _logger.LogInformation("Memory analysis for {Operation}:", operationName);
        _logger.LogInformation("  Start: {Start} bytes", startMemory);
        _logger.LogInformation("  End: {End} bytes", endMemory);
        _logger.LogInformation("  Delta: {Delta} bytes", memoryDelta);

        if (memoryDelta > 1024 * 1024) // More than 1MB
        {
            _logger.LogWarning("High memory allocation detected in {Operation}: {Delta} bytes",
                operationName, memoryDelta);
        }
    }

    private class MemoryAnalysisScope : IDisposable
    {
        private readonly string _operationName;
        private readonly MemoryAnalyzer _analyzer;
        private readonly long _startMemory;

        public MemoryAnalysisScope(string operationName, MemoryAnalyzer analyzer)
        {
            _operationName = operationName;
            _analyzer = analyzer;
            _startMemory = GC.GetTotalMemory(true);
        }

        public void Dispose()
        {
            _analyzer.ReportMemoryUsage(_operationName, _startMemory);
        }
    }
}

// Usage in performance-critical code
public class OptimizedDataProcessor
{
    private readonly MemoryAnalyzer _memoryAnalyzer;

    public OptimizedDataProcessor(MemoryAnalyzer memoryAnalyzer)
    {
        _memoryAnalyzer = memoryAnalyzer;
    }

    public async Task> ProcessLargeDatasetAsync(IEnumerable data)
    {
        using (_memoryAnalyzer.StartAnalysis("ProcessLargeDataset"))
        {
            var results = new List();

            // Process in chunks to control memory usage
            foreach (var chunk in data.Chunk(1000))
            {
                using (_memoryAnalyzer.StartAnalysis("ProcessChunk"))
                {
                    var processedChunk = await ProcessChunkAsync(chunk);
                    results.AddRange(processedChunk);
                }

                // Allow GC between chunks
                await Task.Yield();
            }

            return results;
        }
    }

    private async Task> ProcessChunkAsync(IEnumerable chunk)
    {
        // Process chunk without creating large intermediate collections
        var results = new List();

        foreach (var item in chunk)
        {
            var processed = await ProcessItemAsync(item);
            if (processed != null)
            {
                results.Add(processed);
            }
        }

        return results;
    }

    private async Task ProcessItemAsync(RawData item)
    {
        // Simulate processing
        await Task.Delay(1);
        return new ProcessedData { Id = item.Id, Value = item.Value * 2 };
    }
}

This memory analyzer helps you understand allocation patterns and identify memory-intensive operations. The key is to measure memory usage systematically and optimize the operations that allocate the most.

CPU Profiling: Finding Computational Bottlenecks

CPU profiling helps you identify where your application spends its time. This is crucial for optimizing algorithms and eliminating unnecessary computations.

// CPU profiling utilities
public class CpuProfiler
{
    private readonly ThreadLocal _stopwatch = new(() => new Stopwatch());
    private readonly ILogger _logger;

    public CpuProfiler(ILogger logger)
    {
        _logger = logger;
    }

    public IDisposable Profile(string operationName)
    {
        return new CpuProfileScope(operationName, _stopwatch.Value, _logger);
    }

    private class CpuProfileScope : IDisposable
    {
        private readonly string _operationName;
        private readonly Stopwatch _stopwatch;
        private readonly ILogger _logger;

        public CpuProfileScope(string operationName, Stopwatch stopwatch, ILogger logger)
        {
            _operationName = operationName;
            _stopwatch = stopwatch;
            _logger = logger;
            _stopwatch.Restart();
        }

        public void Dispose()
        {
            _stopwatch.Stop();
            var elapsed = _stopwatch.Elapsed;

            _logger.LogInformation("CPU Profile - {OperationName}: {Elapsed}ms",
                _operationName, elapsed.TotalMilliseconds);

            // Alert on CPU-intensive operations
            if (elapsed > TimeSpan.FromSeconds(1))
            {
                _logger.LogWarning("CPU-intensive operation detected: {OperationName} took {Elapsed}ms",
                    _operationName, elapsed);
            }
        }
    }
}

// Algorithm optimization examples
public class AlgorithmOptimization
{
    // PROBLEM: O(n²) algorithm for finding duplicates
    public List BadFindDuplicates(List numbers)
    {
        var duplicates = new List();

        for (int i = 0; i < numbers.Count; i++)
        {
            for (int j = i + 1; j < numbers.Count; j++)
            {
                if (numbers[i] == numbers[j] && !duplicates.Contains(numbers[i]))
                {
                    duplicates.Add(numbers[i]);
                    break;
                }
            }
        }

        return duplicates;
    }

    // SOLUTION: O(n) algorithm using HashSet
    public List GoodFindDuplicates(List numbers)
    {
        var seen = new HashSet();
        var duplicates = new HashSet();

        foreach (var number in numbers)
        {
            if (!seen.Add(number))
            {
                duplicates.Add(number);
            }
        }

        return duplicates.ToList();
    }

    // PROBLEM: Inefficient LINQ queries
    public IEnumerable BadProcessOrders(IEnumerable orders)
    {
        return orders
            .Where(o => o.Status == OrderStatus.Pending)
            .Select(o => new OrderSummary
            {
                Id = o.Id,
                Total = o.Items.Sum(item => item.Quantity * item.UnitPrice), // Calculated multiple times
                ItemCount = o.Items.Count()
            })
            .OrderBy(o => o.Total);
    }

    // SOLUTION: Optimize LINQ with single pass
    public IEnumerable GoodProcessOrders(IEnumerable orders)
    {
        return orders
            .Where(o => o.Status == OrderStatus.Pending)
            .Select(o =>
            {
                var items = o.Items.ToList(); // Materialize once
                return new OrderSummary
                {
                    Id = o.Id,
                    Total = items.Sum(item => item.Quantity * item.UnitPrice),
                    ItemCount = items.Count
                };
            })
            .OrderBy(o => o.Total);
    }

    // PROBLEM: Recursive algorithms that can cause stack overflow
    public long BadFibonacci(int n)
    {
        if (n <= 1) return n;
        return BadFibonacci(n - 1) + BadFibonacci(n - 2); // Exponential time
    }

    // SOLUTION: Iterative approach
    public long GoodFibonacci(int n)
    {
        if (n <= 1) return n;

        long a = 0, b = 1;
        for (int i = 2; i <= n; i++)
        {
            long temp = a + b;
            a = b;
            b = temp;
        }
        return b;
    }
}

// Async performance optimization
public class AsyncOptimization
{
    // PROBLEM: Blocking async code
    public async Task BadAsyncOperation()
    {
        // This blocks a thread unnecessarily
        Thread.Sleep(1000);
        await Task.Delay(1000); // This is properly async
    }

    // SOLUTION: Fully async implementation
    public async Task GoodAsyncOperation()
    {
        await Task.Delay(2000); // Single async operation
    }

    // PROBLEM: Sequential async operations in loop
    public async Task> BadAsyncProcessing(IEnumerable ids)
    {
        var results = new List();

        foreach (var id in ids)
        {
            var result = await GetResultAsync(id); // Sequential
            results.Add(result);
        }

        return results;
    }

    // SOLUTION: Parallel async processing
    public async Task> GoodAsyncProcessing(IEnumerable ids)
    {
        var tasks = ids.Select(id => GetResultAsync(id));
        var results = await Task.WhenAll(tasks);
        return results.ToList();
    }

    // PROBLEM: Async void (fire and forget without error handling)
    public void BadFireAndForget(Task task)
    {
        // Exceptions will be lost
        Task.Run(async () => await task);
    }

    // SOLUTION: Proper async error handling
    public async Task GoodFireAndForget(Func operation)
    {
        try
        {
            await Task.Run(operation);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Fire-and-forget operation failed");
            // Handle error appropriately
        }
    }
}

CPU optimization is about choosing the right algorithms and data structures. The examples above show how small changes in approach can lead to dramatic performance improvements: - O(n²) to O(n) algorithms - Reducing unnecessary computations - Proper async/await usage - Parallel processing when appropriate

Benchmarking: Measuring Performance Scientifically

Benchmarking provides objective, reproducible performance measurements. Unlike profiling (which tells you where time is spent), benchmarking tells you how fast something is and whether your optimizations actually help.

// BenchmarkDotNet setup and usage
[MemoryDiagnoser]
[RankColumn]
public class CollectionBenchmarks
{
    [Params(100, 1000, 10000)]
    public int Size { get; set; }

    private List _list;
    private int[] _array;

    [GlobalSetup]
    public void Setup()
    {
        var data = Enumerable.Range(0, Size).ToArray();
        _list = data.ToList();
        _array = data;
    }

    [Benchmark(Baseline = true)]
    public int List_ForEach()
    {
        int sum = 0;
        foreach (var item in _list) sum += item;
        return sum;
    }

    [Benchmark]
    public int Array_ForEach()
    {
        int sum = 0;
        foreach (var item in _array) sum += item;
        return sum;
    }

    [Benchmark]
    public int LINQ_Sum_List() => _list.Sum();

    [Benchmark]
    public int LINQ_Sum_Array() => _array.Sum();
}

// JSON serialization benchmarks
[MemoryDiagnoser]
public class SerializationBenchmarks
{
    private TestData _data;
    private string _json;

    [GlobalSetup]
    public void Setup()
    {
        _data = new TestData { Id = Guid.NewGuid(), Name = "Test", Values = Enumerable.Range(0, 100).ToArray() };
        _json = JsonSerializer.Serialize(_data);
    }

    [Benchmark]
    public string SystemTextJson_Serialize() => JsonSerializer.Serialize(_data);

    [Benchmark]
    public TestData SystemTextJson_Deserialize() => JsonSerializer.Deserialize(_json);
}

BenchmarkDotNet provides statistically significant performance measurements. The key features are: - Automatic warmup and multiple iterations - Memory allocation tracking - Statistical analysis of results - Platform-specific optimizations - Easy comparison between implementations

The benchmarks show interesting results: - Arrays are often faster than Lists for iteration - LINQ has overhead but provides readability - Parallel LINQ helps with large datasets but has coordination overhead - Manual loops can be fastest but are harder to maintain

Database Performance Optimization

Database operations are often the biggest performance bottleneck in applications. The N+1 query problem alone can kill application performance.

// Entity Framework optimization patterns
public class OptimizedRepository
{
    private readonly ApplicationDbContext _context;

    public OptimizedRepository(ApplicationDbContext context)
    {
        _context = context;
    }

    // PROBLEM: N+1 query problem
    public async Task> BadGetOrdersWithItems()
    {
        var orders = await _context.Orders.ToListAsync();

        // This executes N additional queries (one per order)
        foreach (var order in orders)
        {
            order.Items = await _context.OrderItems
                .Where(item => item.OrderId == order.Id)
                .ToListAsync();
        }

        return orders.Select(MapToDto);
    }

    // SOLUTION: Eager loading with Include
    public async Task> GoodGetOrdersWithItems()
    {
        var orders = await _context.Orders
            .Include(o => o.Items) // Single query with join
            .ToListAsync();

        return orders.Select(MapToDto);
    }

    // PROBLEM: Loading unnecessary data
    public async Task BadGetOrderSummary(int id)
    {
        var order = await _context.Orders
            .Include(o => o.Items)
            .ThenInclude(i => i.Product) // Loads entire product objects
            .FirstOrDefaultAsync(o => o.Id == id);

        return new OrderSummary
        {
            Id = order.Id,
            Total = order.Items.Sum(i => i.Quantity * i.UnitPrice)
        };
    }

    // SOLUTION: Select only needed data
    public async Task GoodGetOrderSummary(int id)
    {
        return await _context.Orders
            .Where(o => o.Id == id)
            .Select(o => new OrderSummary
            {
                Id = o.Id,
                Total = o.Items.Sum(i => i.Quantity * i.UnitPrice)
            })
            .FirstOrDefaultAsync();
    }

    // PROBLEM: Inefficient paging
    public async Task> BadGetPagedOrders(int page, int pageSize)
    {
        // Loads ALL data, then skips/takes
        var orders = await _context.Orders.ToListAsync();
        return orders.Skip((page - 1) * pageSize).Take(pageSize);
    }

    // SOLUTION: Database-level paging
    public async Task> GoodGetPagedOrders(int page, int pageSize)
    {
        return await _context.Orders
            .OrderBy(o => o.CreatedAt)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();
    }

    // PROBLEM: Missing indexes
    public async Task> BadSearchOrders(string customerId, DateTime? startDate)
    {
        return await _context.Orders
            .Where(o => o.CustomerId.Contains(customerId) ||
                       (startDate.HasValue && o.CreatedAt >= startDate))
            .ToListAsync();
    }

    // SOLUTION: Optimized query with proper indexing strategy
    public async Task> GoodSearchOrders(string customerId, DateTime? startDate)
    {
        var query = _context.Orders.AsQueryable();

        if (!string.IsNullOrEmpty(customerId))
        {
            query = query.Where(o => o.CustomerId == customerId); // Exact match for index usage
        }

        if (startDate.HasValue)
        {
            query = query.Where(o => o.CreatedAt >= startDate);
        }

        return await query
            .OrderBy(o => o.CreatedAt)
            .Take(1000) // Limit results
            .ToListAsync();
    }

    private OrderDto MapToDto(Order order) => new OrderDto
    {
        Id = order.Id,
        CustomerId = order.CustomerId,
        Items = order.Items,
        Total = order.Total,
        Status = order.Status
    };
}

// Connection pooling and optimization
public class DatabaseOptimization
{
    public static void ConfigureDbContext(IServiceCollection services, string connectionString)
    {
        services.AddDbContextPool(options =>
        {
            options.UseSqlServer(connectionString, sqlOptions =>
            {
                sqlOptions.EnableRetryOnFailure(
                    maxRetryCount: 3,
                    maxRetryDelay: TimeSpan.FromSeconds(5),
                    errorNumbersToAdd: null);

                sqlOptions.CommandTimeout(30);
                sqlOptions.MinBatchSize(1);
                sqlOptions.MaxBatchSize(100);
            });

            // Enable query logging in development
            if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development")
            {
                options.EnableSensitiveDataLogging();
                options.EnableDetailedErrors();
                options.LogTo(Console.WriteLine, LogLevel.Information);
            }
        });
    }
}

Database optimization focuses on: - Reducing query count (N+1 problem) - Selecting only needed data - Using proper indexes - Implementing efficient paging - Connection pooling - Query optimization

Caching Strategies: Reducing Database Load

Caching is one of the most effective performance optimizations. A well-implemented cache can reduce database load by orders of magnitude.

// Multi-level caching implementation
public class CacheManager
{
    private readonly IDistributedCache _distributedCache;
    private readonly IMemoryCache _memoryCache;
    private readonly ILogger _logger;

    public CacheManager(
        IDistributedCache distributedCache,
        IMemoryCache memoryCache,
        ILogger logger)
    {
        _distributedCache = distributedCache;
        _memoryCache = memoryCache;
        _logger = logger;
    }

    public async Task GetOrSetAsync(
        string key,
        Func> factory,
        TimeSpan? expiry = null,
        bool useSlidingExpiration = false)
    {
        // Try L1 cache first (memory)
        if (_memoryCache.TryGetValue(key, out T value))
        {
            _logger.LogDebug("Cache hit (L1): {Key}", key);
            return value;
        }

        // Try L2 cache (distributed)
        var serialized = await _distributedCache.GetStringAsync(key);
        if (serialized != null)
        {
            value = JsonSerializer.Deserialize(serialized);

            // Populate L1 cache
            var options = new MemoryCacheEntryOptions
            {
                SlidingExpiration = TimeSpan.FromMinutes(5)
            };
            _memoryCache.Set(key, value, options);

            _logger.LogDebug("Cache hit (L2): {Key}", key);
            return value;
        }

        // Cache miss - compute value
        _logger.LogDebug("Cache miss: {Key}", key);
        value = await factory();

        // Cache in both levels
        var distCacheOptions = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = expiry ?? TimeSpan.FromHours(1)
        };

        if (useSlidingExpiration)
        {
            distCacheOptions.SlidingExpiration = expiry ?? TimeSpan.FromMinutes(30);
        }

        await _distributedCache.SetStringAsync(
            key,
            JsonSerializer.Serialize(value),
            distCacheOptions);

        // L1 cache with shorter expiry
        _memoryCache.Set(key, value, TimeSpan.FromMinutes(5));

        return value;
    }

    public async Task RemoveAsync(string key)
    {
        await _distributedCache.RemoveAsync(key);
        _memoryCache.Remove(key);
        _logger.LogDebug("Cache invalidated: {Key}", key);
    }

    public async Task RemoveByPatternAsync(string pattern)
    {
        // Note: This is a simplified implementation
        // In production, you'd need a more sophisticated approach
        _logger.LogWarning("RemoveByPattern not fully implemented for distributed cache");
    }
}

// Cache-aside pattern implementation
public class ProductService
{
    private readonly IProductRepository _repository;
    private readonly CacheManager _cache;
    private readonly ILogger _logger;

    public ProductService(
        IProductRepository repository,
        CacheManager cache,
        ILogger logger)
    {
        _repository = repository;
        _cache = cache;
        _logger = logger;
    }

    public async Task GetProductAsync(int id)
    {
        var cacheKey = $"product:{id}";

        return await _cache.GetOrSetAsync(
            cacheKey,
            () => _repository.GetByIdAsync(id),
            TimeSpan.FromMinutes(30));
    }

    public async Task> GetProductsByCategoryAsync(string category)
    {
        var cacheKey = $"products:category:{category}";

        return await _cache.GetOrSetAsync(
            cacheKey,
            () => _repository.GetByCategoryAsync(category),
            TimeSpan.FromMinutes(15),
            useSlidingExpiration: true);
    }

    public async Task UpdateProductAsync(Product product)
    {
        await _repository.UpdateAsync(product);

        // Invalidate related caches
        await _cache.RemoveAsync($"product:{product.Id}");

        // Invalidate category cache (simplified - in production you'd track dependencies)
        await _cache.RemoveAsync($"products:category:{product.Category}");

        _logger.LogInformation("Product updated and caches invalidated: {ProductId}", product.Id);
    }
}

// Cache warming for frequently accessed data
public class CacheWarmer
{
    private readonly IServiceProvider _services;
    private readonly ILogger _logger;

    public CacheWarmer(IServiceProvider services, ILogger logger)
    {
        _services = services;
        _logger = logger;
    }

    public async Task WarmCacheAsync()
    {
        using var scope = _services.CreateScope();
        var cache = scope.ServiceProvider.GetRequiredService();
        var repository = scope.ServiceProvider.GetRequiredService();

        _logger.LogInformation("Starting cache warming");

        // Warm up popular products
        var popularProductIds = await repository.GetPopularProductIdsAsync(100);

        var tasks = popularProductIds.Select(async id =>
        {
            try
            {
                await cache.GetOrSetAsync(
                    $"product:{id}",
                    () => repository.GetByIdAsync(id),
                    TimeSpan.FromHours(1));
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to warm cache for product {ProductId}", id);
            }
        });

        await Task.WhenAll(tasks);
        _logger.LogInformation("Cache warming completed for {Count} products", popularProductIds.Count);
    }
}

Effective caching strategies include: - Multi-level caching (memory + distributed) - Cache-aside pattern - Proper cache invalidation - Cache warming for frequently accessed data - Appropriate expiry times

Production Monitoring and Alerting

Performance optimization doesn't end with development. You need continuous monitoring to catch issues in production and maintain performance over time.

// Application Insights integration
public class ApplicationInsightsMonitor
{
    private readonly TelemetryClient _telemetry;
    private readonly ILogger _logger;

    public ApplicationInsightsMonitor(
        TelemetryClient telemetry,
        ILogger logger)
    {
        _telemetry = telemetry;
        _logger = logger;
    }

    public async Task MonitorOperationAsync(
        string operationName,
        Func> operation,
        Dictionary properties = null)
    {
        var operationTelemetry = _telemetry.StartOperation(operationName);

        try
        {
            // Add custom properties
            if (properties != null)
            {
                foreach (var prop in properties)
                {
                    operationTelemetry.Properties[prop.Key] = prop.Value;
                }
            }

            var result = await operation();
            operationTelemetry.Success = true;

            return result;
        }
        catch (Exception ex)
        {
            operationTelemetry.Success = false;
            _telemetry.TrackException(ex);

            // Add exception details to operation
            operationTelemetry.Properties["ExceptionType"] = ex.GetType().Name;
            operationTelemetry.Properties["ExceptionMessage"] = ex.Message;

            throw;
        }
        finally
        {
            _telemetry.StopOperation(operationTelemetry);
        }
    }

    public void TrackCustomMetric(
        string metricName,
        double value,
        Dictionary properties = null)
    {
        var metric = new MetricTelemetry(metricName, value);

        if (properties != null)
        {
            foreach (var prop in properties)
            {
                metric.Properties[prop.Key] = prop.Value;
            }
        }

        _telemetry.TrackMetric(metric);
    }

    public void TrackDependency(
        string dependencyName,
        string commandName,
        DateTimeOffset startTime,
        TimeSpan duration,
        bool success)
    {
        var dependency = new DependencyTelemetry
        {
            Name = dependencyName,
            CommandName = commandName,
            Timestamp = startTime,
            Duration = duration,
            Success = success
        };

        _telemetry.TrackDependency(dependency);
    }
}

// Performance health check
public class PerformanceHealthCheck : IHealthCheck
{
    private readonly PerformanceMetrics _metrics;
    private readonly ILogger _logger;

    public PerformanceHealthCheck(
        PerformanceMetrics metrics,
        ILogger logger)
    {
        _metrics = metrics;
        _logger = logger;
    }

    public Task CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var stats = _metrics.GetAllStats().ToList();

        // Check for slow operations
        var slowOperations = stats.Where(s =>
            s.Value.AverageDuration > TimeSpan.FromSeconds(2)).ToList();

        if (slowOperations.Any())
        {
            var details = string.Join(", ",
                slowOperations.Select(s => $"{s.Key}: {s.Value.AverageDuration.TotalSeconds:F1}s"));

            return Task.FromResult(HealthCheckResult.Degraded(
                $"Slow operations detected: {details}"));
        }

        // Check error rates
        var highErrorRateOperations = stats.Where(s =>
            s.Value.TotalCalls > 10 && s.Value.SuccessRate < 0.95).ToList();

        if (highErrorRateOperations.Any())
        {
            var details = string.Join(", ",
                highErrorRateOperations.Select(s => $"{s.Key}: {(1 - s.Value.SuccessRate):P1} errors"));

            return Task.FromResult(HealthCheckResult.Degraded(
                $"High error rates detected: {details}"));
        }

        return Task.FromResult(HealthCheckResult.Healthy(
            $"Performance healthy. Monitored {stats.Count} operations."));
    }
}

// Middleware for automatic performance monitoring
public class PerformanceMonitoringMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ApplicationInsightsMonitor _monitor;
    private readonly ILogger _logger;

    public PerformanceMonitoringMiddleware(
        RequestDelegate next,
        ApplicationInsightsMonitor monitor,
        ILogger logger)
    {
        _next = next;
        _monitor = monitor;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        var path = context.Request.Path.Value;
        var method = context.Request.Method;

        var properties = new Dictionary
        {
            ["Path"] = path,
            ["Method"] = method,
            ["UserAgent"] = context.Request.Headers.UserAgent.ToString()
        };

        await _monitor.MonitorOperationAsync(
            $"HTTP {method} {path}",
            () => _next(context),
            properties);
    }
}

Production monitoring provides: - Automatic performance tracking - Custom metrics and alerts - Dependency monitoring - Health checks for load balancers - Request tracing and correlation

Summary

Performance profiling and optimization is a systematic process that combines measurement, analysis, and optimization. The key insights are to measure first using profiling tools to identify bottlenecks, focus on impact following the 80/20 rule, benchmark everything to verify optimizations, address memory issues early, optimize database queries and implement effective caching, and monitor continuously in production.

The most important lesson is that performance optimization is about making the right code fast, not making all code fast. By building performance awareness into your development process-from initial design through production monitoring-you create applications that scale effectively and provide excellent user experience. Remember: premature optimization is the root of all evil, but systematic performance analysis is the foundation of all success.