Skip to main content

Testably.Abstractions.MemoryMappedFiles

Wraps System.IO.MemoryMappedFiles.MemoryMappedFile as a MemoryMappedFile extension property on IFileSystem, so memory-mapped-file code can be tested against the in-memory MockFileSystem.

Installation​

dotnet add package Testably.Abstractions.MemoryMappedFiles

Latest version: 7.2.0

The extensions live in the Testably.Abstractions namespace.

Why this package exists​

System.IO.MemoryMappedFiles.MemoryMappedFile.CreateFromFile needs a real path (or a real FileStream) on disk; it cannot map a file that only exists inside a MockFileSystem. Without the companion package, any production code that touches MemoryMappedFile is effectively untestable against the mock: your test either falls back to writing real temp files or the memory-mapped path stays uncovered.

IFileSystem fileSystem = //...

using IMemoryMappedFile mappedFile = fileSystem.MemoryMappedFile
.CreateFromFile("red-square.png");

using IMemoryMappedViewAccessor accessor = mappedFile.CreateViewAccessor();
accessor.Write(0, 42);
int value = accessor.ReadInt32(0);

The same code runs unchanged against RealFileSystem and MockFileSystem.

Production uses the real implementation

On RealFileSystem, every call forwards to the base class library (BCL) MemoryMappedFile, so production keeps the native memory-mapping semantics. Only on MockFileSystem are the views built over the in-memory file bytes.

Creating a memory-mapped file​

fileSystem.MemoryMappedFile exposes the CreateFromFile overloads of System.IO.MemoryMappedFiles.MemoryMappedFile:

using IMemoryMappedFile fromPath = fileSystem.MemoryMappedFile
.CreateFromFile("red-square.png", FileMode.Open, mapName: null, capacity: 0,
MemoryMappedFileAccess.ReadWrite);

The FileStream-based overload is exposed with a FileSystemStream instead of a FileStream, so it works against both the real and the mocked file system:

using FileSystemStream stream =
fileSystem.FileStream.New("red-square.png", FileMode.Open, FileAccess.ReadWrite);
using IMemoryMappedFile mappedFile = fileSystem.MemoryMappedFile.CreateFromFile(
stream, mapName: null, capacity: 0, MemoryMappedFileAccess.ReadWrite,
HandleInheritability.None, leaveOpen: true);

Views​

An IMemoryMappedFile creates two kinds of views over its bytes, mirroring the BCL:

// Random-access, typed reads and writes:
using IMemoryMappedViewAccessor accessor = mappedFile.CreateViewAccessor(0, 100);
accessor.Write(0, 1234567);
int roundtrip = accessor.ReadInt32(0);

// A Stream over a window of the mapped bytes:
using MemoryMappedFileSystemViewStream stream = mappedFile.CreateViewStream(0, 50);
stream.Write([1, 2, 3, 4], 0, 4);

IMemoryMappedViewAccessor mirrors MemoryMappedViewAccessor: the primitive Read*/Write overloads, the generic Read<T>/Write<T> for structs, ReadArray<T>/WriteArray<T>, plus Capacity, CanRead, CanWrite, PointerOffset and Flush.

MemoryMappedFileSystemViewStream derives from UnmanagedMemoryStream, like the real MemoryMappedViewStream, and adds its Capacity and PointerOffset members.

Behavior on the mock​

On MockFileSystem the reads and writes, the capacity handling and the thrown exceptions mirror the real MemoryMappedViewAccessor / MemoryMappedViewStream, including:

  • Default view access is ReadWrite: creating a writable view over a read-only mapping throws UnauthorizedAccessException, exactly as the BCL does.
  • The file and its views have independent lifetimes: a view stays usable after the IMemoryMappedFile is disposed.
  • CopyOnWrite views privatize pages on write: their writes are neither persisted nor visible to other views, while pages they have not written keep reflecting changes made through other views.

Not supported​

These parts of the BCL operate on operating-system shared memory rather than a file. They forward normally on RealFileSystem, but throw NotSupportedException on MockFileSystem:

  • CreateNew
  • CreateOrOpen (Windows-only)
  • OpenExisting (Windows-only)
  • A non-null mapName in CreateFromFile (named mappings are shared-memory objects)

The SafeMemoryMappedFileHandle / SafeMemoryMappedViewHandle handle and pointer APIs, and the SafeFileHandle-based CreateFromFile overload, are not exposed at all. The unsafe PositionPointer of a view stream throws ObjectDisposedException, because no unmanaged memory backs it.

Differences from the real file system​

Details tied to operating-system memory mapping that the mock simplifies
  • View size: a view created without an explicit size has a Capacity of exactly the remaining bytes (the real file system rounds up to the page size), and PointerOffset is always 0.
  • Visibility of writes: writes are immediately visible to all views of the same mapping, but reach the underlying file (and other streams of it) only through the backing stream, at the latest when a view is flushed or disposed. The real operating system makes them visible to other readers immediately.
  • Disposed backing stream: when the stream passed to CreateFromFile with leaveOpen: true is disposed while views are open, operations on those views throw ObjectDisposedException. Real views keep working.
  • Truncation: truncating the backing stream while views are open succeeds, and reads of the truncated range return zeros. The real operating system rejects the truncation.
  • Size: the mock keeps the whole file in memory, so a memory-mapped file is limited to 2 GB. Growing it beyond that throws NotSupportedException.
  • Exceptions: exception types and messages follow the Windows behavior of the BCL, also on Linux and macOS and for every simulated operating system.
  • Struct size on .NET Framework: Read<T>/Write<T>/ReadArray<T>/WriteArray<T> size structs by their managed layout, like .NET (Core). The .NET Framework uses the marshalled size, so the two disagree for structs such as one containing a bool.