When and by "whom" is the Dispose() method called after scenario

137 views Asked by At

I use SpecFlow.NUnit. Let's say I have 2 scenarios and I run them

Scenario: First
Given Definition1

Scenario: Second
Given Definition2

[Given (@"Definition1")]
public void Definition1{
  File.Open(@"C:\Users\1.txt", FileMode.Open);
}

[Given (@"Definition2")]
public void Definition2{
  File.Open(@"C:\Users\1.txt", FileMode.Open); // no error thrown
}

I am trying to undertand how is that that after the "First" scenario runs and opens a FileStream and method Definition1() does NOT close it, no error is thrown when the "Second" scenario opens a FileStream on the same file. This leads me into thinking that sdomewhere the Dispose(); is called, but where and by whom? maybe this is not a specflow thing but NUnit thing?

2

There are 2 answers

2
Eric Lippert On BEST ANSWER

I don't have a definitive answer but I have some informed speculation.

You are correct that something is closing the file between the first and second opens; the default sharing mode for File.Open is "no sharing even in the same process", and you should therefore get the (confusing and wrong) error message "The process cannot access the file because it is being used by another process." (The error message should say the truth: "The file cannot be opened because it is already opened for exclusive access".)

What then is closing the file? First, understand how finalization works in C#.

  • The correct thing to do is to call Dispose or otherwise close the file. A correct implementation of Dispose will close the file and mark the object as not requiring finalization. The finalizer thread then never sees the object.

  • The incorrect thing to do is to skip closing/disposing. Eventually the garbage collector will run and notice that an object which requires finalization is dead. That gets put onto a queue, and the finalizer thread eventually executes the finalizer, closing the file.

Since you are not disposing of the file, something must be closing it. I can think of four possibilities; there may be others.

(1) You are getting lucky (or unlucky, depending on your point of view) and the garbage collector is happening to run, finding the orphaned file, putting it on the finalizer queue, and the finalizer runs before the second attempt to open the file.

(2) Your test framework is calling GC.Collect and GC.WaitForPendingFinalizers between tests, forcing the resources to be cleaned up.

(3) Your test framework is putting each test into an AppDomain and unloading the AppDomain between tests. That should also force resources to be cleaned up.

(4) Your test framework is running each test in a separate process. Exclusive locks on files do not survive process termination.

Without knowing the details of how NUnit works, I couldn't tell you which of the above scenarios you're in, but you're likely in one of them.

3
Greg Burghardt On

While my answer won't explain why this happens, it should explain how to fix it within the realm of SpecFlow, rendering this question moot.

SpecFlow comes with its own dependency injection container, which will dispose of objects at the end of a scenario as long as they implement IDisposable. Step definition classes are automatically registered in the SpecFlow DI container.

In this case, you have some file handles to clean up. Assuming this file handle is only used in one step definition file, the solution is to assign the FileStream object returned by File.Open to a field, which is then disposed of. The step definition class needs to implement IDisposable in order to do this:

[Binding]
public class YourSteps : IDisposable
{ //                   ^^^^^^^^^^^^^
    private FileStream oneDotTxtFile;

    private FileStream OneDotTxtFile
    {
        if (oneDotTxtFile == null)
        {
            oneDotTxtFile = File.Open(@"C:\Users\1.txt", FileMode.Open);
        }

        return oneDotTxtFile;
    }

    [Given(@"Definition1")]
    public void Definition1()
    {
        OneDotTxtFile.WhateverYouNeedToDoWithIt();
    }

    [Given(@"Definition2")]
    public void Definition2()
    {
        OneDotTxtFile.WhateverYouNeedToDoWithIt();
    }

    #region Implement Dispose Pattern found at https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose

    private void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                if (oneDotTxtFile != null)
                {
                    oneDotTxtFile.Dispose();
                    oneDotTxtFile = null;
                }
            }

            disposedValue = true;
        }
    }

    public void Dispose()
    {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    #endregion
}

Note that adding : IDisposable to your step definition class declaration is necessary. Visual Studio gives you a Quick Action to implement the Dispose pattern if you right-click on the IDisposable interface name in your code. This stubs out the boilerplate code to implement this interface according to the Microsoft documentation.

Now you can simply use the FileStream without thinking about it. The OneDotTxtFile property will lazily open the file the first time you call the property. The Dispose() method in the step definition class is invoked by the SpecFlow DI container at the end of each scenario, which will dispose of the FileStream object if the field is not null.