Introduction
There are times when, while writing a C# program, we find ourselves needing to execute code between two operations that must be called together in a specific order. For example, we might open a file, perform some operations, and then close the file.
These operations must both be executed—either because we need to release resources (as in the file open/close scenario) or because our program’s correct behavior depends on executing both statements in a specific order. This requirement creates what is known as temporal coupling. I discuss various ways to address this issue in my How To Avoid Temporal Coupling in C# post.
In this post, we’ll explore a different approach to managing scoped operations. While the previous solutions can be effective, they may not handle scenarios where exceptions are thrown during execution. Here, we’ll look at a solution that ensures our program remains in a valid state, even if an exception occurs between the two operations. In other words, even if the code of the user of our library throws an exception, our implementation will guarantee that the second operation still executes.
An Example With Code
For the purposes of this post, let’s consider the following scenario: we have an Enemy
class with StartPatrol
and EndPatrol
methods. These methods change the internal state of the Enemy instance. The user must call StartPatrol
to initiate patrol mode, perform any number of operations, and then call EndPatrol
to return the instance to a non-patrolling state and perform any necessary cleanup.
This code is not what we would consider critical in the sense that it doesn’t involve unreleased resources if EndPatrol
is not called. The only potential issue is a logical error in our program’s behavior. Still, this example is simple and easy to understand. Instead of starting and stopping an enemy patrol, the two operations could represent any number of tasks, such as starting and finishing a data save.
Let’s first look at a “normal” implementation of our Enemy
class:
public class Enemy
{
public bool IsPatrolling { get; private set; }
public void StartPatrol()
{
IsPatrolling = true;
Console.WriteLine("Enemy starting Patrol mode");
// Here rest of patrol mode initialization code
}
public void EndPatrol()
{
IsPatrolling = false;
Console.WriteLine($"Enemy patrol mode ended, IsPatrolling is {IsPatrolling}");
// Here rest of patrol mode clean up code
}
}
In this implementation, the user of our Enemy
class, has to remember and guarantee, that after each call to StartPatrol
, there will be a call to EndPatrol
, like this:
public class Test
{
public void TestEnemy(Enemy enemy)
{
enemy.StartPatrol();
Console.WriteLine("This is code that executes while enemy is in patrol mode");
enemy.EndPatrol();
}
}
Enemy enemy = new();
Test test = new();
test.TestEnemy(enemy);
Console.WriteLine($"At the end enemy patrol mode is: {enemy.IsPatrolling}");
This guarantees that enemy.IsPatrolling
is set to false. Rather than relying on the user to handle this manually, we can explore one of the approaches outlined in How To Avoid Temporal Coupling in C#. However, none of those solutions address scenarios where the user’s code throws an exception:
public class Test
{
public void TestEnemy(Enemy enemy)
{
enemy.StartPatrol();
Console.WriteLine("This is code that executes while enemy is in patrol mode");
throw new InvalidOperationException();
enemy.EndPatrol(); // This will never get executed !!!
}
}
A Better Implementation Of The Enemy Class
We can eliminate the need for the user to manually call EndPatrol
by leveraging the using
keyword and the IDisposable
interface.
By implementing IDisposable
in a nested struct, we can place the logic of the EndPatrol
method inside the struct’s Dispose
method. Then, we can have the StartPatrol
method return an instance of this struct. In fact, since there is no longer a separate EndPatrol
method, it makes sense to rename StartPatrol
, as the returned EnemyScoped
instance simply represents a patrolling state:
public class EnemyScoped
{
public bool IsPatrolling { get; private set; }
public EndPatrol Patrolling()
{
IsPatrolling = true;
Console.WriteLine("EnemyScoped starting Patrol mode");
// Here rest of patrol mode initialization code
return new EndPatrol(this);
}
public readonly struct EndPatrol : IDisposable
{
private readonly EnemyScoped _enemy;
internal EndPatrol(EnemyScoped enemy) => _enemy = enemy;
public void Dispose() => Dispose(true);
private void Dispose(bool disposing)
{
if (_enemy == null)
return;
_enemy.IsPatrolling = false;
Console.WriteLine($"Patrol mode ended, IsPatrolling is {_enemy.IsPatrolling}");
// Here rest of patrol mode clean up code
}
}
}
In this implementation, the Patrolling
method contains the same code as before but now returns an EndPatrol
struct. This struct holds a reference to the EnemyScoped
class, allowing it to access its private members, and it implements the IDisposable
pattern. Since there are no unmanaged resources to release, the Dispose
method simply contains the logic that was previously in EndPatrol
.
Now it can be used like this:
public class Test
{
public void TestEnemyScoped(EnemyScoped enemyScoped)
{
using (enemyScoped.Patrolling())
{
Console.WriteLine("This is code that executes while enemyScoped is in patrol mode");
}
}
}
The using
block will ensure that the code will run.
Advantages and Notes About the Disposing Implementation
Typically, we want the code in the Dispose
method to be short and performant, but this is not a strict requirement in this case. This concern only applies when calling Dispose(false)
from the type’s finalizer. The reason for this is that finalizers can cause significant performance issues. They are executed sequentially from a queue by the garbage collector, which can keep the object and any other instances it references alive, since it’s considered a root object. However, the finalizer is only necessary when unmanaged resources need to be released, if they haven’t already been released during the instance’s finalization. If there are no unmanaged resources, we can treat the Dispose
method like any other method, considering performance, as long as any classes that inherit from it also don’t manage unmanaged resources and therefore don’t require finalizers.
A key advantage of this approach is that any code that throws an exception will not leave our program in an invalid state. Consider the following example with the Enemy
class:
public class Test
{
public void TestEnemy(Enemy enemy)
{
enemy.StartPatrol();
Console.WriteLine("This is code that executes while enemy is in patrol mode");
throw new InvalidOperationException();
enemy.EndPatrol();
}
}
If we run this:
try
{
test.TestEnemy(enemy);
}
catch{ }
Console.WriteLine($"At the end enemy patrol mode is: {enemy.IsPatrolling}");
The output will be:
Enemy starting Patrol mode
This is code that executes while enemy is in patrol mode
At the end enemy patrol mode is: True
Because the enemy.EndPatrol()
never run. But in the case of the using
block, because it actually is a try/finally
block, the code will always run, so the following:
public class Test
{
public void TestEnemyScoped(EnemyScoped enemyScoped)
{
using (enemyScoped.Patrolling())
{
Console.WriteLine("This is code that executes while enemyScoped is in patrol mode");
throw new InvalidOperationException();
}
}
}
try
{
test.TestEnemyScoped(enemyScoped);
}
catch { }
Console.WriteLine($"At the end enemyScoped patrol mode is: {enemyScoped.IsPatrolling}");
will have an output of:
EnemyScoped starting Patrol mode
This is code that executes while enemyScoped is in patrol mode
Patrol mode ended, IsPatrolling is False
At the end enemyScoped patrol mode is: False
Our finalization code, will always run.
NOTE: Never use empty catch
blocks. The ones shown here are empty for the sake of the example. You should always handle the exceptions that are caught in some way.
Conclusion
When dealing with operations that are coupled and used to change the state of an object, allowing code to run between them (e.g., open/close, start/end, initialize/finalize), the using
keyword and the Dispose
method are the most reliable way to ensure that finalization code will run, regardless of what happens.
In contrast to other approaches that attempt to encapsulate temporal coupling (such as the template method pattern), this solution ensures that temporal coupling can be avoided not only at compile time but also at runtime, especially when user code fails to execute due to a thrown exception.
This is just one more way to avoid temporal coupling while ensuring that our program remains in a valid state if the code executed between operations throws an exception. This approach is particularly beneficial when building tools or libraries for others to use, as we can never be certain of the code they will attempt to execute. In our own code, unless an exception is likely, we can choose this or any other method that avoids temporal coupling and is better suited to the situation.
Thank you for reading, and if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.