Skip to main content

SafeFileHandle

SafeFileHandle is a wrapper around an OS handle to a real file, and overloads like File.GetLastAccessTime(SafeFileHandle) route through it.

Handles from the abstraction​

IFile.OpenHandle returns a handle that the file system it came from understands, so nothing has to be registered:

MockFileSystem fileSystem = new();
fileSystem.File.WriteAllText("red-square.txt", "Oil on board");

using SafeFileHandle handle = fileSystem.File.OpenHandle("red-square.txt");

await That(fileSystem.File.GetLastAccessTime(handle))
.IsEqualTo(fileSystem.File.GetLastAccessTime("red-square.txt"));
await That(fileSystem.RandomAccess.GetLength(handle)).IsEqualTo(12);

Like a real handle, it refers to the file it was opened on rather than to its name, so it keeps working after the file is renamed. It also honors the FileShare and FileOptions (including DeleteOnClose) it was opened with, following the rules of the simulated operating system.

Only for the abstraction

A handle the MockFileSystem created is not a real OS handle, so passing it to System.IO.RandomAccess or to another process fails. Use IFileSystem.RandomAccess and the IFile overloads instead.

note

The mock cannot observe the moment a handle is disposed, so a released share lock or DeleteOnClose takes effect the next time the file system checks whether the file exists or may be opened.

Handles from elsewhere​

When a handle comes from somewhere the mock knows nothing about (platform invocation, or a library that hands you one), you still have to tell it which file that handle stands for.

MockFileSystem.WithSafeFileHandleStrategy(ISafeFileHandleStrategy) registers the translation. The strategy maps a SafeFileHandle to a SafeFileHandleMock that points at a location inside the mock, and is consulted for any handle the mock did not create itself.

The default strategy (NullSafeFileHandleStrategy) is registered automatically and throws NotSupportedException for any handle. Install a custom strategy as soon as your code under test reaches for a handle from outside.

Example: mapping handles of the real file system​

The strategy below keeps an explicit map from SafeFileHandle to (real path, mock path). On every lookup it copies the real file's attributes and timestamps into the mock, so the test sees fresh data without having to write a fixture by hand.

CustomSafeFileHandleStrategy
public class CustomSafeFileHandleStrategy : ISafeFileHandleStrategy
{
private readonly RealFileSystem _realFileSystem;
private readonly MockFileSystem _mockFileSystem;
private readonly Dictionary<SafeFileHandle, (string RealPath, SafeFileHandleMock Mock)> _mapping = new();

public CustomSafeFileHandleStrategy(
RealFileSystem realFileSystem, MockFileSystem mockFileSystem)
{
_realFileSystem = realFileSystem;
_mockFileSystem = mockFileSystem;
}

public CustomSafeFileHandleStrategy AddMapping(
SafeFileHandle handle, string realPath, SafeFileHandleMock mock)
{
_mapping[handle] = (realPath, mock);
return this;
}

public SafeFileHandleMock MapSafeFileHandle(SafeFileHandle handle)
{
if (!_mapping.TryGetValue(handle, out (string RealPath, SafeFileHandleMock Mock) entry))
throw new KeyNotFoundException("The provided fileHandle is not mapped!");

SyncMetadata(entry.RealPath, entry.Mock.Path);
return entry.Mock;
}

private void SyncMetadata(string realPath, string mockPath)
{
_mockFileSystem.File.SetAttributes(mockPath,
_realFileSystem.File.GetAttributes(realPath));
_mockFileSystem.File.SetCreationTime(mockPath,
_realFileSystem.File.GetCreationTime(realPath));
_mockFileSystem.File.SetLastAccessTime(mockPath,
_realFileSystem.File.GetLastAccessTime(realPath));
_mockFileSystem.File.SetLastWriteTime(mockPath,
_realFileSystem.File.GetLastWriteTime(realPath));
}
}

Wiring it into a test​

RealFileSystem realFileSystem = new();
MockFileSystem mockFileSystem = new();

CustomSafeFileHandleStrategy strategy = new(realFileSystem, mockFileSystem);
mockFileSystem.WithSafeFileHandleStrategy(strategy);

using IDirectoryCleaner _ = realFileSystem.SetCurrentDirectoryToEmptyTemporaryDirectory();

// Create the same file on both sides
mockFileSystem.File.WriteAllText("mock", "Oil on board");
realFileSystem.File.WriteAllText("real", "Oil on board");

// Mutate the real file's last-access time
DateTime expected = new DateTime(2026, 1, 1);
realFileSystem.File.SetLastAccessTime("real", expected);

// Open a SafeFileHandle on the real file
SafeFileHandle handle = realFileSystem.File.OpenHandle("real");

// Register the mapping
strategy.AddMapping(handle, "real", new SafeFileHandleMock("mock"));

// Reading via the SafeFileHandle goes through the strategy and synchronizes
// the mock metadata from the real file system on the way in
await That(mockFileSystem.File.GetLastAccessTime(handle)).IsEqualTo(expected);

// As a side effect the mock path now also reflects the synced value
await That(mockFileSystem.File.GetLastAccessTime("mock")).IsEqualTo(expected);

Here the handle belongs to the real file system, so the mock cannot resolve it on its own and the strategy supplies the mapping. A handle from mockFileSystem.File.OpenHandle needs none of this.