Performance bottlenecks can disable an application’s efficiency, scalability, and user experience. Many .NET developers unknowingly fall into anti-patterns that degrade performance over time. In this article, we’ll explore the top 10 .NET performance anti-patterns, explain why they are problematic, and show how to fix them with optimized solutions.
1. Excessive Object Allocation and Garbage Collection Pressure
The Problem:
Creating too many short-lived objects leads to frequent garbage collection (GC) cycles, impacting application performance.
Fix:
- Use object pooling for reusable objects.
- Leverage structs over classes for small, immutable objects.
- Reduce allocations by using Span and Memory.
- Enable GC tuning using
GCSettings.LargeObjectHeapCompactionMode
when necessary.
Example:
// Instead of this:
var data = new byte[1024];// Use MemoryPool to reuse allocated memory:
var pool = MemoryPool<byte>.Shared;
using (var owner = pool.Rent(1024))
{
var memory = owner.Memory;
// Process memory here
}
2. Blocking Asynchronous Code (Sync Over Async)
The Problem:
Calling .Result
or .GetAwaiter().GetResult()
on asynchronous methods blocks threads and can lead to deadlocks.
Fix:
- Always use async/await.
- Avoid mixing synchronous and asynchronous code.
Example:
// Anti-pattern
public string GetData()
{
return GetDataAsync().Result; // Blocks thread
}
// Fix
public async Task<string> GetDataAsync()
{
return await FetchDataFromServiceAsync();
}
3. Inefficient Database Queries
The Problem:
- N+1 query problem in ORMs like Entity Framework.
- Not using proper indexing.
- Querying too much data.
Fix:
- Avoid lazy loading and Use eager loading by including all necessary related data upfront.
- Optimize queries with pagination and indexing.
- Profile queries using EF Core logging.
Example:
// Anti-pattern
var orders = context.Orders.ToList();
foreach (var order in orders)
{
var customer = context.Customers.Find(order.CustomerId); // N+1 issue
}
// Fix
var ordersWithCustomers = context.Orders.Include(o => o.Customer).ToList();
4. Overusing Reflection
The Problem:
Reflection incurs a significant performance cost due to metadata inspection.
Fix:
- Use compiled expressions or source generators instead.
- Cache reflection results instead of calling it repeatedly.
Example:
// Anti-pattern
var type = typeof(MyClass);
var property = type.GetProperty("MyProperty");
var value = property.GetValue(instance);
// Fix
var propertyDelegate = (Func<MyClass, object>)Delegate.CreateDelegate(
typeof(Func<MyClass, object>), null, type.GetProperty("MyProperty").GetMethod);
var valueOptimized = propertyDelegate(instance);
5. Using String Concatenation in Loops
The Problem:
Strings are immutable; repeated concatenation creates multiple new string objects in memory.
Fix:
- Use StringBuilder for repeated string operations.
Example:
// Anti-pattern
string result = "";
for (int i = 0; i < 1000; i++)
{
result += i.ToString();
}
// Fix
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append(i);
}
string optimizedResult = sb.ToString();
6. Ignoring Caching for Expensive Computations
The Problem:
Performing the same expensive computation multiple times wastes resources.
Fix:
- Use MemoryCache, Redis, or Lazy for caching.
- Implement output caching for repeated responses.
Example:
private static readonly MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
public string GetExpensiveData(string key)
{
if (!_cache.TryGetValue(key, out string cachedData))
{
cachedData = ComputeExpensiveData();
_cache.Set(key, cachedData, TimeSpan.FromMinutes(10));
}
return cachedData;
}
7. Not Using async
Database Calls
The Problem:
Synchronous database queries block threads and reduce scalability.
Fix:
- Use
async
versions of EF Core methods likeToListAsync()
.
Example:
// Anti-pattern
var users = context.Users.ToList(); // Blocks thread
// Fix
var users = await context.Users.ToListAsync();
8. Excessive Logging in Performance-Critical Paths
The Problem:
Logging too much in hot paths slows down execution.
Fix:
- Use conditional logging.
- Reduce logging verbosity in critical sections.
Example:
// Anti-pattern
_logger.LogInformation("Processing item: {Id}", item.Id);
// Fix
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Processing item: {Id}", item.Id);
}
9. Using LINQ Inefficiently
The Problem:
Certain LINQ operations, like ToList()
before filtering, result in unnecessary memory usage.
Fix:
- Use deferred execution by avoiding
.ToList()
too early.
Example:
// Anti-pattern
var filteredUsers = context.Users.ToList().Where(u => u.IsActive);
// Fix
var filteredUsers = context.Users.Where(u => u.IsActive).ToList();
10. Ignoring Async Streams for Large Data Processing
The Problem:
Loading large datasets into memory at once causes high memory consumption.
Fix:
- Use IAsyncEnumerable to stream data.
Example:
public async IAsyncEnumerable<User> GetUsersAsync()
{
await foreach (var user in context.Users.AsAsyncEnumerable())
{
yield return user;
}
}
Key Takeaways:
- Avoid excessive memory allocations to reduce GC overhead.
- Embrace async programming to improve responsiveness.
- Optimize database queries to prevent unnecessary data retrieval.
- Use caching to minimize redundant computations.
- Reduce logging in performance-sensitive paths.
Fixing these anti-patterns will significantly enhance the performance of your .NET applications, leading to better scalability and efficiency.
What’s Next?
Start by profiling your code using dotnet-trace or BenchmarkDotNet to detect performance bottlenecks, then apply these fixes incrementally.
🚀 Happy coding!
Link: https://medium.com/@kohzadi90/top-10-net-performance-anti-patterns-you-should-fix-today-d58f4a682340