Skip to main content

· 8 min read
Adnan Rafiq

Dispose Myth's - Two Questions

Read the below code to answer the questions that follow the code.

General Myth's - Two Questions

PrintFileContent();
Console.ReadKey();

static void PrintFileContent()
{
//Notice that I have not wrapped the DisposableStream object in a using block and neither I am calling Dispose method.
var disposableStream = new DisposableStream("file");//Assume that file exists in the current directory.
disposableStream.PrintAllLines();
Console.WriteLine("I am done with the stream.");
}

public class DisposableStream(string fileName) : IDisposable
{
private readonly FileStream _fileStream = new(fileName, FileMode.Open);

public void PrintAllLines()
{
using var streamReader = new StreamReader(_fileStream);
while (streamReader.ReadLine() is { } line)
{
Console.WriteLine(line);
}
}

public void Dispose()
{
_fileStream.Dispose();
Console.WriteLine("DisposableStream dispose was called");
}
}
  1. Will the Dispose method be called on the DisposableStream object when the PrintFileContent method completes, since we are not wrapping it in a using block and neither calling the Dispose method explicitly?
  2. If the Dispose method is not called, how will the FileStream object be collected by the Runtime?

Answer # 1

The .NET runtime will never call the Dispose method on the DisposableStream. Not for just this object, in fact, any object that implements the IDisposable interface will not have its Dispose method called by the .NET runtime. It is the responsibility of the consumer of the object to call the Dispose method when it is done with the object.

Answer # 2

The FileStream object will be collected by the .NET Garbage Collector when the DisposableStream object is collected. But the Dispose method will not be called on the FileStream object and that means that the resources held by it will not be properly released or disposed. As consumers, we do not know what kinds of resources were used by the FileStream object and how they should be released. If the FileStream object implements destructor or finalizer, it will be called by the Garbage collector on a special thread known as Finalizer thread to release the resources.

Does this mean that resources will be held until the finalizer thread runs? Yes, that's correct. But

  • We do not control the finalizer thread and when it will run.
  • It is not guaranteed that the finalizer thread will run immediately after the object is collected.
  • Finalizer thread maintains a queue of objects that need to be finalized. It is up to the runtime to decide when to run the finalizer thread. The key point is that it is two steps process.

In the below example,

  • I am forcing the Garbage Collector to run by calling GC.Collect method.
  • I added a destructor to the DisposableStream class to print a message when the finalizer is called.
Forcing Garbage Collector to run

PrintFileContent();
ForceGcSoFinalizerCanRun();
Console.ReadKey();

static void PrintFileContent()
{
//Notice that I have not wrapped the DisposableStream object in a using block and neither I am calling Dispose method.
var disposableStream = new DisposableStream("file");
disposableStream.PrintAllLines();
Console.WriteLine("I am done with the stream.");
}

void ForceGcSoFinalizerCanRun()
{
GC.WaitForPendingFinalizers();
GC.Collect(0, GCCollectionMode.Forced, true);
GC.WaitForPendingFinalizers();
GC.WaitForFullGCComplete();
}

public class DisposableStream(string fileName) : IDisposable
{
private readonly FileStream _fileStream = new(fileName, FileMode.Open);

public void PrintAllLines()
{
using var streamReader = new StreamReader(_fileStream);
while (streamReader.ReadLine() is { } line)
{
Console.WriteLine(line);
}
}

public void Dispose()
{
_fileStream.Dispose();
Console.WriteLine("DisposableStream dispose was called.");
}
~DisposableStream()
{
Console.WriteLine("DisposableStream finalizer was called.");
}
}

Two Steps Finalization Process

In the above snippet, you saw that we are forcing the GC by calling ForceGcSoFinalizerCanRun method. If we comment out the ForceGcSoFinalizerCanRun method call, you will see that the finalizer or destructor will not be called. In order for you to observe the two-step finalization process, you will have to take memory dump of the process and analyze it.

In the below code snippet, I have added comments to guide you on how to take memory dump and when to take it.

"Two

PrintFileContent();
//ForceGcSoFinalizerCanRun();
Console.WriteLine("Take memory dump using the command: dotnet-dump collect -p <process id> --output <output directory>");
Console.WriteLine("Once you have taken the memory dump, press any key to force the GC so we can take second dump.");
Console.ReadKey();
ForceGcSoFinalizerCanRun();
Console.WriteLine("You should have seen the finalizer being called line already.");
Console.WriteLine("Take memory dump using the command: dotnet-dump collect -p <process id> --output <output directory>");
Console.ReadKey();

static void PrintFileContent()
{
//Notice that I have not wrapped the DisposableStream object in a using block and neither I am calling Dispose method.
var disposableStream = new DisposableStream("file");
disposableStream.PrintAllLines();
Console.WriteLine("I am done with the stream.");
}

void ForceGcSoFinalizerCanRun()
{
GC.WaitForPendingFinalizers();
GC.Collect(0, GCCollectionMode.Forced, true);
GC.WaitForPendingFinalizers();
GC.WaitForFullGCComplete();
}

public class DisposableStream(string fileName) : IDisposable
{
private readonly FileStream _fileStream = new(fileName, FileMode.Open);

public void PrintAllLines()
{

using var streamReader = new StreamReader(_fileStream);
while (streamReader.ReadLine() is { } line)
{
Console.WriteLine(line);
}
}

public void Dispose()
{
_fileStream.Dispose();
Console.WriteLine("DisposableStream dispose was called.");
}
~DisposableStream()
{
Console.WriteLine("DisposableStream finalizer was called.");
}
}

You have two memory dumps now. You can analyze them using the dotnet-dump analyze command. Let's see what we can find in the finalizer queue, which is the queue of objects that need to be finalized.

You can run the below command to see the finalizer queue.

dotnet-dump analyze <path to the first memory dump> 
sos finalizequeue -allReady

Once you execute the above command, you should see the DisposableStream in the list of objects ready for finalization but the runtime has not called the finalizer or destructor yet.

When we took the second dump, we forced the GC, so it can run the finalizer for the objects which were ready to be finalized. So let's analyze the second dump.

dotnet-dump analyze <path to the second memory dump> 
sos finalizequeue -allReady

Now you will see that the DisposableStream is not in the list. It is because -allReady filter is only showing objects which are ready to be finalized.

But if you execute the command without filter sos finalizequeue, now you will see DisposableStream in the list because it already has been finalized.

The tools like dotMemory shows finalization queue visually but the feature isn't on Mac yet.

More questions than answers

You understand that

  1. how the finalization of an object works if you implement destructor.
  2. Dispose is your responsibility and GC never calls it.

There are more questions because you would like to avoid the two-step process of finalization and more importantly when you should implement destructor. Also, you have implemented dispose and destructor on an object, but consumer is properly wrapping it in using block, will the object go through the finalization process?

When should you implement destructor? Only when you are using unmanaged objects, and would like to clean up the resources if the consumer forgot to call dispose.

How to avoid the finalization if you are cleaning up in Dispose implementation? You can avoid the two-step finalization process by calling GC.SuppressFinalize(this); in the implementation of Dispose. If you do not suppress the finalizer, the object will go through the finalization process anyway.

In the last, I will you another question.

Should you access the managed objects or try to clean up managed resources inside the destructor since this method is triggered by a special runtime thread known as Finalizer thread?

The answer is No, because the finalizer thread has marked the object that it is ready for finalization, and the existence of managed object is not certain or 100%. That is why an IDE generates a disposed pattern like below code snippet.

Dispose Pattern

class DisposePattern : IDisposable
{
private void ReleaseUnmanagedResources()
{
// TODO release unmanaged resources here
}

protected virtual void Dispose(bool disposing)
{
ReleaseUnmanagedResources();
if (disposing)
{
// TODO release managed resources here
}
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

~DisposePattern()
{
Dispose(false);
}
}

Similarly, you can generate the pattern of IAsyncDisposable.

IAsyncDisposable Pattern

class DisposePattern : IDisposable, IAsyncDisposable
{
private void ReleaseUnmanagedResources()
{
// TODO release unmanaged resources here
}

protected virtual void Dispose(bool disposing)
{
ReleaseUnmanagedResources();
if (disposing)
{
// TODO release managed resources here
}
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

~DisposePattern()
{
Dispose(false);
}

protected virtual async ValueTask DisposeAsyncCore()
{
// TODO release managed resources here
ReleaseUnmanagedResources();
}

public async ValueTask DisposeAsync()
{
await DisposeAsyncCore();
GC.SuppressFinalize(this);
}
}

· 6 min read
Adnan Rafiq

Refactorings

Any code written in the past is legacy code as soon as adding a new feature or fixing a bug makes it difficult to change it. There is only one cure to this problem: Refactor it, but only one refactoring at a time. Otherwise, you will be frustrated to the least.

Refactoring is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior. Its heart is a series of small behavior preserving transformations. Each transformation (called a 'refactoring') does little, but a sequence of transformations can produce a significant restructuring. Since each refactoring is small, it's less likely to go wrong. The system is kept fully working after each small refactoring, reducing the chances that a system can get seriously broken during the restructuring. Martin Fowler

There is no better refactoring than making the code more readable. After all, to change code, you first have to understand it: by reading it.

· 3 min read
Adnan Rafiq
Timeout in ASP.NET Core 8

No built-in support for Request Timeout before .NET 8

There is no default way to control the request timeout in ASP.NET Core 6.0 or 7.0. But the ASP.NET Core 8.0 introduces a RequestTimeout Middleware. It supports policy convention/pattern to apply the timeout options on a specific endpoint or globally. You will find it familiar if you have configured CORS using the CORS middleware or any other middleware. But if you love extension methods, you are in luck, as it supports extension methods on the Builder.

· 3 min read
Adnan Rafiq
How to use Serilog with Application Builder

Logging

Logs are facts emitted by your application especially when it is running in production.

Structured logging is a way to give facts a shape so that you can query logs better.

Serilog is a structured logging library for .NET and it is one of the most popular logging libraries in .NET.

But how to use Serilog with the new .NET 8 Worker template?

· 5 min read
Adnan Rafiq

Middleware is a function in then ASP.NET 8, when many functions are invoked one after the other like a chain; it becomes a middleware pipeline. The middleware pipeline is responsible for handling the incoming HTTP request and outgoing HTTP response.

For in depth understanding of the middleware you can read my blog post here.

This is a series of blog posts in which I will cover all the builtin middlewares in ASP.NET 8.

· 5 min read
Adnan Rafiq
Validate Options on Startup

Validate Options<T> on Startup in .NET 8

The .NET 8 Host Builder allows you to bind configuration with C# objects by using AddOptions<T> and binding to the configuration.

It provides you an opportunity to validate the configuration values when the host (WebApplication or Hosted Server) is starting by using ValidateOnStart.

But there are two interesting aspects of it, which I will explain in this post.

· 16 min read
Adnan Rafiq
Diagnostic Middlewares

Middleware is a function in then ASP.NET 8, when many functions are invoked one after the other like a chain; it becomes a middleware pipeline. The middleware pipeline is responsible for handling the incoming HTTP request and outgoing HTTP response.

For in depth understanding of the middleware you can read my blog post here.

This is a series of blog posts in which I will cover all the builtin middlewares in ASP.NET 8.

· 8 min read
Adnan Rafiq
Validate Options on Startup

Validate Options<T> on Startup

The .NET 8 Host Builder allows you to bind configuration with C# objects by using AddOptions<T> and binding to the configuration.

It provides you an opportunity to validate the configuration values when the host (WebApplication or Hosted Server) is starting by using ValidateOnStart.

But there are two interesting aspects of it, which I will explain in this post.

· 15 min read
Adnan Rafiq
Title Image of the blog

Middleware is a function in then ASP.NET 8, when many functions are invoked one after the other like a chain; it becomes a middleware pipeline. The middleware pipeline is responsible for handling the incoming HTTP request and outgoing HTTP response.

For in depth understanding of the middleware you can read my blog post here.

This is a series of blog posts in which I will cover all the builtin middlewares in ASP.NET 8.

· 6 min read
Adnan Rafiq
Title Image of the blog

Middleware is a function in then ASP.NET 8, when many functions are invoked one after the other like a chain; it becomes a middleware pipeline. The middleware pipeline is responsible for handling the incoming HTTP request and outgoing HTTP response.

For in depth understanding of the middleware you can read my blog post here.

This is a series of blog posts in which I will cover all the builtin middlewares in ASP.NET 8.