The mock FileStream class does not handle shared file contents correctly.
The source code below should write the values 0-9 to the console. Instead it writes the value 0 to the console ten times.
The code behaves as expected when using the real file system.
using System.IO.Abstractions;
using System.IO.Abstractions.TestingHelpers;
//var fileSystem = new FileSystem();
var fileSystem = new MockFileSystem();
var filename = fileSystem.Path.GetTempFileName();
try
{
using var file1 = fileSystem.FileStream.New(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
using var file2 = fileSystem.FileStream.New(filename, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
var buffer = new byte[4];
for (int ix = 0; ix < 10; ix++)
{
file1.Position = 0;
file1.Write(BitConverter.GetBytes(ix));
file1.Flush();
file2.Position = 0;
file2.Flush();
file2.Read(buffer);
Console.WriteLine(BitConverter.ToInt32(buffer));
}
}
finally
{
fileSystem.File.Delete(filename);
}
Thanks for reporting this issue. The root cause, seems to be, that the underlying MemoryStream is not updated when accessed from a parallel file handle.
Hi, It looks like this issue has been fixed in PR #625, which has already been merged. Should this issue be closed or is there something else still pending? @vbreuss edit: I noticed it’s from Testably/Testably.Abstractions, not this repo. What’s the relationship between these two projects? edit2: ok, I see what is the relation in issue #471
Is this issue fixed or not? I am happy to pick this up.
@oni-shiro : It's not yet fixed here. I assigned it to you :-)
I am bit confused with this issue, at base it is using the FileStreamSystem to write into file. Then should this change be part of this repo or not?
FileStremSystem is coming from Testably.Abstractions.FileSystem.Interface/FileSystemStream.cs I guess.
Yes, you are right @oni-shiro, the FileSystemStream class is located here and would need to be fixed there.
Cool, I will look into it this week or weekend.
The MockFileStream class does not handle shared file contents correctly when multiple streams are opened on the same file with FileShare.ReadWrite. Changes written through one stream were not visible to other streams, unlike real filesystem behavior.
Expected Behavior: When file1 writes data and flushes, file2 should be able to read the updated content.
Actual Behavior: file2 continued reading stale content (zeros) regardless of what file1 wrote.
Root Cause Analysis
- Architecture Issue
Each MockFileStream instance creates its own independent MemoryStream (line 46 in MockFileStream.cs). While there is a shared MockFileData.Contents that represents the "file on disk," each stream operates on its own memory buffer.
- Content Synchronization Problem
The synchronization between individual stream buffers and shared content is incomplete:
- Write → Shared: InternalFlush() correctly updated MockFileData.Contents when a stream flushed
- Shared → Read: Missing mechanism to refresh stream content from shared data before reads
- Race Condition in Flush
The InternalFlush() method is called by both streams during Flush(), causing the second stream (which hadn't written anything) to overwrite the shared content with empty data:
file1.Write(data) → file1.Flush() → Updates shared content to [1,0,0,0] file2.Flush() → Overwrites shared content to [] (empty) file2.Read() → Reads empty data
This problem is harder to solve than it may first seem, because each MockFileStream needs to maintain its own independent state while simultaneously staying synchronized with shared content from other streams.
The challenge lies in preserving unflushed local changes during synchronization - when Stream A has written data but hasn't flushed yet, and Stream B flushes new content to the shared file, Stream A must refresh to see Stream B's changes without losing its own pending writes.
Additionally, the solution must handle complex scenarios like concurrent writes to different file positions, streams with different access permissions (read-only vs write-only), and operations that change file size (SetLength, writes beyond EOF) while maintaining correct stream positions and avoiding infinite recursion between refresh operations and property access.
While there is a shared MockFileData.Contents that represents the "file on disk," each stream operates on its own memory buffer.
FileStreams or Streams in general, operate with their own internal buffer, so MockFileStream having its own stream isnt inherently bad. I suspect the real problem is that updating the position in the stream (or seeking) doesn't force the buffer to invalidate its internal state and force a read from the underlying data.