Updated on
Writing a byte array to a file in C# is one call: File.WriteAllBytes() takes a path and the bytes, creates or truncates the file, writes, and closes it.
The other approaches exist for the cases that one call does not cover: appending instead of replacing, writing without blocking a thread, writing bytes that arrive in pieces, and writing at a chosen position in an existing file.
How Do We Write a Byte Array to a File in C#?
File.WriteAllBytes() takes a path and a byte array, and handles opening, writing and closing the file for us. It is the answer for the ordinary case.
We do not have to clear an existing file first. Microsoft Learn states the behaviour directly: the method “Creates a new file, writes the specified byte array to the file, and then closes the file. If the target file already exists, it is truncated and overwritten.”
Because of the truncation, the file ends up exactly as long as our array, with nothing left over from whatever was there before.
Nothing in the call decides the file type. Writing PDF bytes to report.pdf and image bytes to logo.png is the same call, because the array already holds that format’s own bytes and the extension only tells other programs what to expect.
Since .NET 9 a second overload takes a ReadOnlySpan<byte>, so a slice of a larger buffer reaches disk without a copy first.
Let’s write a new method that makes use of File.WriteAllBytes(), inside a static ByteArrayToFileConverter class that will hold every method in this article:
public static class ByteArrayToFileConverter
{
public static void SaveByteArrayToFileWithStaticMethod(byte[] data, string filePath)
=> File.WriteAllBytes(filePath, data);
}
The entire implementation is within the File class, hence, making it shorthand and straightforward.
If the bytes need to go on the end of an existing file rather than replace it, File.AppendAllBytes() is the call, added in .NET 9:
public static void AppendByteArrayToFile(byte[] data, string filePath)
=> File.AppendAllBytes(filePath, data);
When we need to read a file back into a byte array, File.ReadAllBytes() is the matching call.
How Do We Write a Byte Array to a File Asynchronously?
File.WriteAllBytesAsync() is the asynchronous form, and it is what belongs inside a web request or anywhere else that should not block a thread on disk.
The signature adds a CancellationToken that defaults to CancellationToken.None, so we can pass a request’s token and skip the write when the connection has already dropped.
Everything else matches the synchronous method. The file is created if it is missing, and truncated and overwritten if it is not.
We should always await the returned task. Microsoft Learn notes that the method “stores in the task it returns all non-usage exceptions that the method’s synchronous counterpart can throw”, so a call we forget to await can swallow a disk-full or permission error entirely.
The method has been available since .NET Core 2.0, and .NET 9 added a ReadOnlyMemory<byte> overload beside the array one. File.AppendAllBytesAsync() is the matching append.
Let’s add an asynchronous method to our class:
public static Task SaveByteArrayToFileAsync(byte[] data, string filePath, CancellationToken token = default)
=> File.WriteAllBytesAsync(filePath, data, token);
The method returns the task without awaiting it, which leaves the await to the caller.
How Do We Write a Byte Array With a FileStream?
A FileStream is the right tool when the bytes do not all exist at once, or when the file has to stay open past a single write.
File.Create() hands us one already open for reading and writing. Microsoft Learn describes what it does to a file that is already there: “if it does exist and it is not read-only, the contents are deleted and overwritten”, which is the same guarantee File.WriteAllBytes() gives.
From there, Write() takes the array and WriteAsync() takes it without blocking. The using declaration in the method below closes the handle and releases the operating system’s lock when the method returns.
A stream is also how we reach the options the one-call methods pick for us. The FileStream constructor accepts a FileOptions value, where FileOptions.Asynchronous opens the handle in overlapped mode. Leave it out and WriteAsync() still does not block our thread, but the input and output is performed synchronously. FileOptions.SequentialScan tells the operating system we will access the file from beginning to end.
Since we are simply working with byte arrays here, we can go ahead and implement our method using FileStream alone:
public static void SaveByteArrayToFileWithFileStream(byte[] data, string filePath)
{
using var stream = File.Create(filePath);
stream.Write(data, 0, data.Length);
}
Our method simply gets a FileStream from File.Create(), passing in the intended file path as the only argument. Then, it uses the instance method called Write() to write the byte array into the created file.
If our data starts out in a stream rather than an array, we can get the bytes out of a stream first, or skip the array and write the stream straight to a file.
When Should We Use BinaryWriter to Write a Byte Array?
BinaryWriter writes primitives and strings in a binary layout: an int as four bytes, a string with a length prefix. For a byte array it adds nothing, because it simply forwards the call to the stream underneath.
So it earns its place only when the byte array is one field among others in a format we are writing ourselves. If the array is all we have, File.WriteAllBytes() does the same job in one line.
Let’s write a method to save a byte array using the BinaryWriter class:
public static void SaveByteArrayToFileWithBinaryWriter(byte[] data, string filePath)
{
using var writer = new BinaryWriter(File.Create(filePath));
writer.Write(data);
}
We pass the FileStream that File.Create() returns into the BinaryWriter object and then call on the BinaryWriter to help us write the data into the stream.
Opening the stream with File.OpenWrite() instead would be a mistake. It opens with FileMode.OpenOrCreate, which does not truncate, so writing a short array over a longer existing file leaves the old tail behind.
The File.OpenWrite reference on Microsoft Learn shows the result with its own example: writing “Second run” over “This is a test of the OpenWrite method” leaves “Second runtest of the OpenWrite method” in the file. The sample’s regression test writes those two strings through our method and expects a 10-byte file, which fails with File.OpenWrite() and passes with File.Create(). We cover the other ways to overwrite an existing file in a separate article.
How Do We Write a Byte Array at a Specific Offset?
RandomAccess writes bytes at a byte offset without a stream position to move, and it has been in the framework since .NET 6.
It works on a SafeFileHandle rather than a stream. File.OpenHandle() opens one, taking the same FileMode, FileAccess, FileShare and FileOptions a FileStream takes, plus a preallocationSize that reserves space for the file up front.
RandomAccess.Write() then takes that handle, a ReadOnlySpan<byte> and the offset to write at. WriteAsync() is the same call with a ReadOnlyMemory<byte> and a cancellation token. A second Write() overload takes a list of ReadOnlyMemory<byte> buffers and writes them back to back from the same offset in one call.
Because the offset is an argument rather than state, several threads can write to different parts of one file through one handle. Microsoft Learn describes the class as providing “offset-based APIs for reading and writing files in a thread-safe manner”.
The class supports only regular disk files, so it cannot write to unseekable files such as pipes.
Let’s add a method that writes our array at a given offset:
public static void SaveByteArrayToFileAtOffset(byte[] data, string filePath, long offset)
{
using var handle = File.OpenHandle(filePath, FileMode.OpenOrCreate, FileAccess.Write);
RandomAccess.Write(handle, data, offset);
}
This time FileMode.OpenOrCreate is what we want, because the bytes around the offset have to stay where they are.
Which Way Should We Write a Byte Array to a File?
File.WriteAllBytes() is the default, and File.WriteAllBytesAsync() is the default anywhere an await is already in play. Between them they cover the common case, where the whole array is already in memory and only needs to reach a file. Appending goes through File.AppendAllBytes() or File.AppendAllBytesAsync(), which keep what the file already holds.
A FileStream earns its place when the bytes arrive in pieces, when the file stays open for more than one write, or when we need to pick the sharing mode or the file options ourselves.
BinaryWriter adds exactly one thing on top of a stream, a binary layout for primitives and strings. Given a byte array it forwards the call, so wrap a stream in it only when the rest of what we write is not already bytes.
RandomAccess writes at an offset without a stream position to move, and asks for a handle instead of a stream in exchange.
Here are the five approaches side by side:
| The call | Existing file | Asynchronous form | Reach for it when |
|---|---|---|---|
File.WriteAllBytes(path, bytes) | Truncated and overwritten | File.WriteAllBytesAsync() | the whole array is in memory (the default) |
File.AppendAllBytes(path, bytes) (.NET 9) | Kept, bytes added at the end | File.AppendAllBytesAsync() | adding to a file instead of replacing it |
using var s = File.Create(path); then s.Write(bytes) | Truncated and overwritten | s.WriteAsync() | the bytes arrive in pieces, or the file stays open |
using var w = new BinaryWriter(File.Create(path)); then w.Write(bytes) | Truncated and overwritten by File.Create() | none for writing (await using to dispose) | the array is one field among primitives and strings |
RandomAccess.Write(handle, bytes, offset) (.NET 6) | Kept, bytes replaced at the offset | RandomAccess.WriteAsync() | writing to one part of a file, possibly from several threads |
The Existing file column is the one to read before we point any of these at a file that already holds data. Only appending and the offset write keep what is there.
Conclusion
We have explored various ways to convert a byte array to a file. For most cases, File.WriteAllBytes() or File.WriteAllBytesAsync() is the best choice.
Nevertheless, you should always choose based on your needs. The first approach is great when you just need to save a file quickly and move on to something else. However, if you need to perform some further action or processing on the stream, you should consider a FileStream, wrapped in a BinaryWriter when the format mixes bytes with other values, and RandomAccess when the bytes belong at an offset.
Tested with .NET 10.0.10 (SDK 10.0.302) and MSTest 4.5.1.
