The Garbage Collector takes care of memory management in .NET. Well, it manages 'most' of the memory. There are scenarios when it cannot.
There are two main memory areas. Managed memory is the portion handled by the .NET Common Language Runtime. It includes objects created by .NET languages like C#, VB.NET, and F#. The CLR's Garbage Collector automatically manages the lifecycle of these objects, handling allocation and deallocation when objects are no longer referenced.
Unmanaged memory is everything outside the CLR's control. It includes memory allocated by native code, typically C or C++, which interacts directly with hardware and system resources without a higher-level runtime.
Whenever .NET apps interact with components outside the runtime, like the file system, a database, or a REST API, they move out of the managed environment. This is often confusing because we work with types in namespaces like System.Data and classes like SqlConnection, which look like ordinary .NET types.
But SqlConnection is a managed wrapper around lower-level native code that communicates with SQL Server. The GC can clean up the managed wrapper, but it cannot deallocate the underlying unmanaged resource. This is when developers need to call Dispose(), allowing the wrapper to free up unmanaged memory.
If you're working with legacy or large codebases, a good first step is enabling the static analysis rule CA2000. It's part of the default ruleset and will help uncover missing Dispose calls, though it's not foolproof.
One more thing: if your class holds IDisposable objects as fields, your class also needs to implement IDisposable and call Dispose on those fields in its own implementation.