Be Careful with Using Declarations in C# 8

January 3rd 2020 C#

One of the less talked about new features in C# 8 are using declarations. That's not too surprising since it's essentially just syntactic sugar for using statements.

Before C# 8, one could create an IDisposable object with a using statement so that it would be automatically disposed at the end of the using block:

private IEnumerable<string> ReadLines(string path)
{
  using (var reader = new StreamReader(path))
  {
    var line = reader.ReadLine();
    while (line != null)
    {
      yield return line;
      line = reader.ReadLine();
    }
    // reader is disposed
  }
}

In C# 8, a using declaration can be used in such a scenario. Unlike the using statement, it doesn't introduce its own code block. Hence, the object is disposed at the end of the block it is contained in:

private IEnumerable<string> ReadLines(string path)
{
  using var reader = new StreamReader(path);
  var line = reader.ReadLine();
  while (line != null)
  {
    yield return line;
    line = reader.ReadLine();
  }
  // reader is disposed
}

The new syntax might make it less obvious what's happening until you get used to it. But it reduces the number of nested blocks, especially when you're dealing with multiple disposable objects:

private string Serialize(IDictionary<string, string> properties)
{
  using var stream = new MemoryStream();
  using var jsonWriter = new Utf8JsonWriter(stream);
  jsonWriter.WriteStartObject();
  foreach (var pair in properties)
  {
    jsonWriter.WriteString(pair.Key, pair.Value);
  }
  jsonWriter.WriteEndObject();

  stream.Position = 0;
  using var reader = new StreamReader(stream);
  return reader.ReadToEnd();
  // reader is disposed
  // jsonWriter is disposed
  // stream is disposed
}

There are three using declarations in the code above which would mean three nested using blocks if using statements were used instead:

private string Serialize(IDictionary<string, string> properties)
{
  using (var stream = new MemoryStream())
  {
    using (var jsonWriter = new Utf8JsonWriter(stream))
    {
      jsonWriter.WriteStartObject();
      foreach (var pair in properties)
      {
        jsonWriter.WriteString(pair.Key, pair.Value);
      }
      jsonWriter.WriteEndObject();
      // jsonWriter is disposed
    }

    stream.Position = 0;
    using (var reader = new StreamReader(stream))
    {
      return reader.ReadToEnd();
      // reader is disposed
    }
    // stream is disposed
  }
}

If you're familiar with how writing to streams works, you should be able to notice that while the method with using statements works as expected, the method with using declarations doesn't. The behavior of both methods is different because jsonWriter is disposed at different times:

  • In the method with using statements, jsonWriter is disposed before reader starts reading from the stream. At that point the Flush method is called implicitly to write everything to the underlying stream.
  • In the method with using declarations, jsonWriter is disposed after reader is done with reading for the stream. Because the Flush method was not called on jsonWriter before that, not everything was necessarily written to the stream already.

There are two ways to fix this issue:

  1. The Flush method can be called explicitly where it needs to be:

    private string Serialize(IDictionary<string, string> properties)
    {
      using var stream = new MemoryStream();
      using var jsonWriter = new Utf8JsonWriter(stream);
      jsonWriter.WriteStartObject();
      foreach (var pair in properties)
      {
        jsonWriter.WriteString(pair.Key, pair.Value);
      }
      jsonWriter.WriteEndObject();
      jsonWriter.Flush();
    
      stream.Position = 0;
      using var reader = new StreamReader(stream);
      return reader.ReadToEnd();
      // reader is disposed
      // jsonWriter is disposed
      // stream is disposed
    }
    
  2. Alternatively, a using statement can be used for jsonWriter to more strictly control when it's disposed. The using declaration can still be used for the other two disposable objects which will be disposed at the end of the method:

    private string Serialize(IDictionary<string, string> properties)
    {
      using var stream = new MemoryStream();
      using (var jsonWriter = new Utf8JsonWriter(stream))
      {
        jsonWriter.WriteStartObject();
        foreach (var pair in properties)
        {
          jsonWriter.WriteString(pair.Key, pair.Value);
        }
        jsonWriter.WriteEndObject();
        // jsonWriter is disposed
      }
    
      stream.Position = 0;
      using var reader = new StreamReader(stream);
      return reader.ReadToEnd();
      // reader is disposed
      // stream is disposed
    }
    

Interestingly enough, if you try running my original method with using declarations before applying one of the two suggested fixes, it will throw an ObjectDisposedException instead of simply returning the wrong result:

System.ObjectDisposedException : Cannot access a closed Stream.

The stack trace will help us determine what actually happened:

MemoryStream.Write(ReadOnlySpan`1 buffer)
Utf8JsonWriter.Flush()
Utf8JsonWriter.Dispose()
Tests.SerializeWithUsingDeclarations(IDictionary`2 properties)

When jsonWriter was disposed, the Flush method was called because not everything was yet written to the underlying stream. Since the stream was already closed at that time, the ObjectDisposedException was thrown.

But weren't the objects supposed to be disposed in the order reversed to the one they were created in? I.e. reader should be disposed first, jsonWriter second and stream last. That's true, but disposing a StreamReader has an unfortunate side effect: it also closes the underlying stream. Hence, stream was closed before jsonWriter was disposed and Flush was called. That's why the exception was thrown.

Use using declarations with care. The exact location in the code where an object is disposed might be different than with using statements. Since other methods might be called implicitly when an object is disposed, this might change the behavior of your code making it incorrect.

Big thanks to Daniel for posting his comment below and bringing up the inaccuracies in the first revision of this post.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License