Your .NET application’s memory usage grows infinitely? You’re probably not disposing resources properly. Here’s the complete guide.
The Memory Leak – Classic Example:
// WRONG: Memory leak
public async Task ProcessOrdersAsync()
{
var dbContext = new ApplicationDbContext();
var orders = await dbContext.Orders.ToListAsync();
// Process orders...
// dbContext never disposed = connection stays open = memory leak
}
// After 1000 requests: 1000 open DB connections
// Result: "Connection pool exhausted" errors
Solution 1 – Using Block (Recommended):
public async Task ProcessOrdersAsync()
{
using (var dbContext = new ApplicationDbContext())
{
var orders = await dbContext.Orders.ToListAsync();
// Process orders...
} // dbContext.Dispose() called automatically here
}
// Modern C# 8+ syntax (even cleaner)
public async Task ProcessOrdersAsync()
{
using var dbContext = new ApplicationDbContext();
var orders = await dbContext.Orders.ToListAsync();
// Process orders...
} // Disposed at end of method
Why This Works:
‘using’ statement ensures Dispose() is called even if exceptions occur. It compiles to:
ApplicationDbContext dbContext = null;
try
{
dbContext = new ApplicationDbContext();
// Your code
}
finally
{
dbContext?.Dispose(); // Always runs
}
Common Leak Sources in .NET Core:
// LEAK 1: HttpClient without disposal
public async Task GetDataAsync(string url)
{
var client = new HttpClient(); // WRONG
return await client.GetStringAsync(url);
}
// FIX: Use IHttpClientFactory
public class MyService
{
private readonly IHttpClientFactory _httpClientFactory;
public MyService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task GetDataAsync(string url)
{
using var client = _httpClientFactory.CreateClient();
return await client.GetStringAsync(url);
}
}
// Register in Program.cs
builder.Services.AddHttpClient();
LEAK 2: Event Handlers Not Unsubscribed:
// WRONG: Memory leak
public class OrderService
{
public OrderService(IEventPublisher publisher)
{
publisher.OrderCreated += OnOrderCreated;
// OrderService can never be garbage collected
// even after it's "done" because publisher keeps reference
}
private void OnOrderCreated(object sender, OrderEventArgs e)
{
// Handle event
}
}
// FIX: Unsubscribe in Dispose
public class OrderService : IDisposable
{
private readonly IEventPublisher _publisher;
public OrderService(IEventPublisher publisher)
{
_publisher = publisher;
_publisher.OrderCreated += OnOrderCreated;
}
private void OnOrderCreated(object sender, OrderEventArgs e)
{
// Handle event
}
public void Dispose()
{
_publisher.OrderCreated -= OnOrderCreated;
}
}
LEAK 3: Static Event Handlers:
// WRONG: Permanent memory leak
public class MyClass
{
public MyClass()
{
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
// Static event keeps MyClass alive FOREVER
}
}
// FIX: Use WeakEventManager or unsubscribe
public class MyClass : IDisposable
{
public MyClass()
{
AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
}
public void Dispose()
{
AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
}
private void OnProcessExit(object sender, EventArgs e)
{
// Cleanup
}
}
Implementing IDisposable Correctly:
public class ResourceManager : IDisposable
{
private FileStream _fileStream;
private HttpClient _httpClient;
private bool _disposed = false;
public ResourceManager()
{
_fileStream = new FileStream("data.txt", FileMode.Open);
_httpClient = new HttpClient();
}
// Public dispose method
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // Prevent finalizer from running
}
// Protected dispose method
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// Dispose managed resources
_fileStream?.Dispose();
_httpClient?.Dispose();
}
// Free unmanaged resources here (if any)
// Example: CloseHandle(ptr)
_disposed = true;
}
// Finalizer (only if you have unmanaged resources)
~ResourceManager()
{
Dispose(false);
}
}
Detecting Memory Leaks – Tools:
// Visual Studio Memory Profiler
// 1. Debug → Performance Profiler
// 2. Select ".NET Object Allocation"
// 3. Run your app
// 4. Look for objects that never get collected
// dotnet-counters (CLI tool)
dotnet-counters monitor --process-id --counters System.Runtime
// Watch these metrics:
// - GC Heap Size (should not grow infinitely)
// - Gen 2 Collections (frequent = potential leak)
// - Exception Count (exceptions prevent disposal)
ASP.NET Core Scoped Services Pattern:
// Register disposable services properly
builder.Services.AddScoped<IOrderService, OrderService>();
// Scoped = one instance per HTTP request
// Automatically disposed at end of request
// Inside controller
public class OrderController : ControllerBase
{
private readonly IOrderService _orderService;
public OrderController(IOrderService orderService)
{
_orderService = orderService;
// Don't dispose! DI container handles it
}
}
