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.
dotnet add package Testably.Abstractions.MemoryMappedFiles
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("data.bin");
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 base class library (BCL) implementation
The abstraction isn't just a uniform façade. When IFileSystem is a RealFileSystem, every call forwards to the underlying BCL MemoryMappedFile, so production keeps the native memory-mapping semantics unchanged. Only when IFileSystem is a MockFileSystem are the views built directly over the in-memory file bytes:
// Inside MemoryMappedFileFactory.CreateFromFile:
if (extensibility.TryGetWrappedInstance(out FileStream? realStream))
{
// real → BCL MemoryMappedFile over the real FileStream
return new MemoryMappedFileWrapper(FileSystem,
MemoryMappedFile.CreateFromFile(realStream, ...), backingStream);
}
// mock → view over the in-memory file bytes
return new MemoryMappedFileMock(FileSystem, stream, capacity, access, ownsStream);
Creating a memory-mapped file
fileSystem.MemoryMappedFile exposes the CreateFromFile overloads of System.IO.MemoryMappedFiles.MemoryMappedFile:
using IMemoryMappedFile fromPath = fileSystem.MemoryMappedFile
.CreateFromFile("data.bin", 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("data.bin", 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 - the same base class as the real MemoryMappedViewStream - and re-declares the Capacity member on top of the stream members (the base is not backed by unmanaged memory), plus the PointerOffset member specific to MemoryMappedViewStream.
Not supported on the mock
Some parts of the BCL surface operate on operating-system shared memory rather than a file, so they have no meaningful in-memory equivalent. They forward normally on RealFileSystem, but throw NotSupportedException on MockFileSystem:
CreateNewCreateOrOpen(Windows-only)OpenExisting(Windows-only)- A non-null
mapNameinCreateFromFile(named mappings are operating-system shared-memory objects)
The SafeMemoryMappedFileHandle / SafeMemoryMappedViewHandle handle and pointer APIs, as well as the SafeFileHandle-based CreateFromFile overload, are not exposed at all - mirroring how the SafeFileHandle-based FileStream construction is excluded elsewhere. Since no unmanaged memory backs a MemoryMappedFileSystemViewStream, the members that only exist on its UnmanagedMemoryStream base throw ObjectDisposedException: the unsafe PositionPointer, and Capacity when accessed through a reference typed as UnmanagedMemoryStream instead of MemoryMappedFileSystemViewStream.
Behaviour 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 throwsUnauthorizedAccessException, exactly as the BCL does. - The file and its views have independent lifetimes - a view stays usable after the
IMemoryMappedFileis disposed. CopyOnWriteviews privatize pages on write - writes are neither persisted to the underlying file nor visible to other views, while pages the view has not written keep reflecting later changes made through other views, matching the lazy page privatization (4096-byte granularity) of real copy-on-write views.
Some details tied to operating-system memory mapping are intentionally simplified:
- A view created without an explicit size has a
Capacityof exactly the remaining bytes (the real file system rounds up to the system page size), andPointerOffsetis always0. - The views of the mock read and write through the backing stream instead of mapped memory. Writes are immediately visible to all views of the same mapping, but reach the underlying file (and other open streams of it) only when a view is flushed or disposed, or earlier when the backing stream persists its pending writes on a reposition - the real operating system makes them visible to other readers immediately. For the same reason, when the stream passed to
CreateFromFilewithleaveOpen: trueis disposed while views are still open, operations on those views throwObjectDisposedException(disposing them stays safe), whereas real views keep operating on the mapped pages without the file handle. - The mock does not track the open mappings of a file, so truncating the backing stream (for example via
SetLengthon a stream passed withleaveOpen: true) while views are open succeeds, and reads of the truncated range afterwards return zeros. The real operating system rejects such a truncation (on Windows with anIOExceptionabout an open user-mapped section). - The mocked file system stores the complete file content in memory, which limits the size of a memory-mapped file to 2 GB. Growing a file beyond that throws a
NotSupportedException, whereas the real file system creates sparse multi-gigabyte mappings. - Exception types and messages of the mock follow the Windows behaviour of the BCL, regardless of the operating system the tests run on and of the simulated operating system of the
MockFileSystem. On Linux and macOS the real file system can throw different exception types for the same invalid call (for example when creating a view with execute access or growing a read-only mapping). - Structs in
Read<T>/Write<T>/ReadArray<T>/WriteArray<T>are sized by their managed layout (Unsafe.SizeOf<T>), matching the BCL on .NET (Core). The realMemoryMappedViewAccessorof the .NET Framework uses the marshalled size instead, so on .NET Framework the mock and the real accessor disagree for structs whose marshalled size differs from the managed one (for example a struct containing abool: 1 byte managed, 4 bytes marshalled).