Skip to main content

RandomAccess

IFileSystem.RandomAccess abstracts the static methods on System.IO.RandomAccess. It reads and writes a file through a SafeFileHandle at an explicit offset, without a stream in between.

Every method is positionless: the offset is a parameter, not state carried on the handle. Nothing advances, so two reads at the same offset return the same bytes and concurrent writes at different offsets do not interfere.

MockFileSystem fileSystem = new();
fileSystem.File.WriteAllBytes("red-square.png", [1, 2, 3, 4]);

using SafeFileHandle handle = fileSystem.File.OpenHandle("red-square.png",
FileMode.Open, FileAccess.ReadWrite);

byte[] buffer = new byte[2];
fileSystem.RandomAccess.Read(handle, buffer, fileOffset: 1); // buffer is [2, 3]

fileSystem.RandomAccess.Write(handle, new byte[] { 9 }, fileOffset: 0);
await That(fileSystem.File.ReadAllBytes("red-square.png"))
.IsEqualTo(new byte[] { 9, 2, 3, 4 });

Get the handle from IFile.OpenHandle, so the file system it came from can resolve it.

What is available​

MemberAvailable from
GetLength.NET 6
Read, ReadAsync.NET 6
Write, WriteAsync.NET 6
SetLength.NET 8
FlushToDisk.NET 8

Read and Write each have a scatter/gather overload taking an IReadOnlyList<Memory<byte>>, which fills or consumes the buffers in order as though they were one.

Behavior worth knowing​

  • A read whose offset is at or past the end of the file returns 0; a read that runs off the end returns a short count rather than throwing.
  • A write past the end grows the file and zero-fills the gap.
  • SetLength truncates, or grows with zeroes.
  • GetLength is metadata and works on a write-only handle. Reading a write-only handle, or writing a read-only one, throws UnauthorizedAccessException.
  • On a handle opened with FileMode.Append the offset is still honored: RandomAccess does not append for you.
  • Operations on a disposed handle throw ObjectDisposedException.

FlushToDisk against the mock​

FlushToDisk asks the operating system to make prior writes durable. The MockFileSystem keeps its files in memory and has no write-back cache, so there is nothing to flush, but the call is still resolved against the handle and recorded, so a test can assert that a durability barrier was requested:

MockFileSystem fileSystem = new();
fileSystem.File.WriteAllText("guard-rounds.log", null);

using (SafeFileHandle handle = fileSystem.File.OpenHandle("guard-rounds.log",
FileMode.Open, FileAccess.Write))
{
fileSystem.RandomAccess.Write(handle, "round"u8.ToArray(), 0);
fileSystem.RandomAccess.FlushToDisk(handle);
}

await That(fileSystem.Statistics.RandomAccess.Methods)
.Contains(m => m.Name == nameof(IRandomAccess.FlushToDisk));

That is something the real file system cannot tell you, and it is the reason to reach for the abstraction rather than the static class: code that needs a durability barrier can be tested for asking.