Question

Save a Stream to a File in C#: Binary and Text Streams

csharp.netstream

Question

I have a StreamReader that was initialized from another object's input stream. I need to save the original stream to disk using a known filename. The data may be a GIF, JPG, or PDF file.

StreamReader reader = new StreamReader(myOtherObject.InputStream);

How should I save this data correctly? In the future, I may also store it in SQL Server. Do I need to keep the text encoding for that purpose?

Short Answer

You will learn the difference between byte streams and text readers in C#, how to copy a stream directly to a file, how to handle stream position and disposal, and why binary data such as images and PDFs should not be processed through StreamReader.

Concept

A Stream represents a sequence of bytes. Files such as JPG, GIF, PDF, ZIP, and most database attachments are binary data: their bytes must be preserved exactly.

StreamReader is different. It is a text reader that takes bytes, decodes them using an encoding such as UTF-8, and exposes characters through methods such as ReadToEnd() and ReadLine(). That conversion is useful for text files, but it can corrupt binary files.

For a file that might be an image or PDF, copy the original Stream directly to a FileStream:

inputStream.CopyTo(outputFileStream);

Encoding matters only when bytes represent text and you need to convert between bytes and characters. It does not describe the contents of a JPG, GIF, or PDF in a useful way.

For SQL Server, binary file contents are commonly stored in a varbinary(max) column. Store the raw bytes, not text produced by a StreamReader.

Mental Model

Think of a binary stream as a sealed package containing numbered pieces. A JPG or PDF requires every piece to remain unchanged and in the same order.

A StreamReader opens the package and tries to translate those pieces into letters and words. That is appropriate for a text document, but a photo is not a sentence. Some byte patterns do not map cleanly to text characters, so the original package cannot reliably be rebuilt.

Copying one Stream to another is like moving the sealed package without opening it. The bytes arrive unchanged.

Take Quiz

Syntax and Examples

Use File.Create to create a file and CopyTo to copy bytes from the source stream.

using System.IO;

Stream input = myOtherObject.InputStream;
string fileName = "document.pdf";

using FileStream output = File.Create(fileName);
input.CopyTo(output);

File.Create(fileName) returns a FileStream opened for writing. CopyTo reads bytes from input until it reaches the end, writing those same bytes to output.

For asynchronous code, use CopyToAsync:

using System.IO;

public static async Task SaveStreamAsync(Stream input, string fileName)
{
    await using FileStream output = File.Create(fileName);
    await input.CopyToAsync(output);
}

If you are handling actual text, use StreamReader and StreamWriter with an explicit encoding instead:

using System.Text;

using var reader = new StreamReader("notes.txt", Encoding.UTF8);
string text = reader.ReadToEnd();

File.WriteAllText("copy.txt", text, Encoding.UTF8);

This text approach should not be used for images, PDFs, or other arbitrary binary files.

Step by Step Execution

Consider this byte-for-byte copy:

using System.IO;

using Stream source = File.OpenRead("source.pdf");
using FileStream destination = File.Create("saved.pdf");

source.CopyTo(destination);

Execution flow:

  1. File.OpenRead("source.pdf") opens the source file and creates a stream positioned at byte 0.
  2. File.Create("saved.pdf") creates or overwrites saved.pdf and opens it for writing.
  3. CopyTo repeatedly reads a chunk of bytes from source.
  4. Each chunk is immediately written to destination without being decoded as text.
  5. At the end of the source stream, CopyTo stops.
  6. The using declarations dispose both streams, flushing and closing the output file.

A stream has a current position. If another operation already read the stream, copying starts at its current position rather than necessarily at the beginning. For a seekable stream, reset it first:

if (source.CanSeek)
{
    source.Position = 0;
}

source.CopyTo(destination);

Do not attempt to set Position on a non-seekable stream, such as many network request streams.

Real World Use Cases

  • File uploads: Copy an uploaded image, PDF, or spreadsheet stream to application storage.
  • HTTP downloads: Save an HTTP response stream directly to a local file.
  • Cloud storage: Copy an incoming stream to Azure Blob Storage, Amazon S3, or another storage provider.
  • Attachment handling: Store email attachments as raw bytes or files.
  • Data exports: Write a generated CSV as text with an encoding, or write a ZIP/PDF as binary bytes.
  • Database storage: Save file bytes in a varbinary(max) column when database-backed attachment storage is appropriate.
Take Quiz

Real Codebase Usage

In production code, developers usually wrap stream copying in a method that validates inputs, creates the destination directory, and uses asynchronous I/O for web applications.

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

public static class FileStorage
{
    public static async Task SaveAsync(
        Stream source,
        string path,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(source);
        ArgumentException.ThrowIfNullOrWhiteSpace(path);

        string? directory = Path.GetDirectoryName(path);
        if (!string.IsNullOrEmpty(directory))
        {
            Directory.CreateDirectory(directory);
        }

        await using FileStream destination = new FileStream(
            path,
            FileMode.Create,
            FileAccess.Write,
            FileShare.None,
            bufferSize: 81920,
            useAsync: true);

        await source.CopyToAsync(destination, cancellationToken);
    }
}

Common project patterns include:

  • Guard clauses: Reject a null stream or missing path immediately.
  • Async copying: Avoid blocking server threads while large uploads are written.
  • Path safety: Build server-side paths with Path.Combine; do not trust a client-supplied filename as a complete path.
  • Size limits: Validate upload size before accepting large files.
  • Content validation: Check file type and content when accepting untrusted uploads; a filename extension alone is not reliable.
  • Ownership rules: Decide which method is responsible for disposing a stream. A method that receives a stream often should leave it open unless its contract says otherwise.

For SQL Server, use a parameterized command and a binary column such as varbinary(max). The important point is that the parameter contains bytes, not encoded text.

Common Mistakes

Using StreamReader for a binary file

This is unsafe for JPG, GIF, PDF, and similar files:

// Incorrect for binary content
using var reader = new StreamReader(myOtherObject.InputStream);
string content = reader.ReadToEnd();
File.WriteAllText("image.jpg", content);

StreamReader decodes bytes into characters, and WriteAllText encodes those characters back into bytes. The result may differ from the original file.

Use stream-to-stream copying instead:

using FileStream output = File.Create("image.jpg");
myOtherObject.InputStream.CopyTo(output);

Copying after the stream has already been read

A stream copy begins at the current position. If possible, reset a seekable stream before copying:

if (input.CanSeek)
{
    input.Position = 0;
}

Assuming every stream can be rewound

This can fail:

input.Position = 0; // Fails when CanSeek is false

Check CanSeek first. If a non-seekable stream must be read more than once, copy it to a temporary file or buffer it deliberately, while considering memory usage.

Forgetting to dispose the output stream

An undisposed output stream can keep a file locked or leave buffered data unwritten. Use using or await using.

Treating base64 as raw file bytes

Base64 is text that represents bytes, usually for JSON or text-only transport. Do not write the base64 string directly as a binary file. Decode it first:

byte[] bytes = Convert.FromBase64String(base64Text);
File.WriteAllBytes("file.pdf", bytes);

Confusing encoding with file type

UTF-8 and UTF-16 are text encodings. JPG, GIF, and PDF are file formats. Store file type separately when needed, for example as a MIME type (image/jpeg, application/pdf) or a validated extension.

Comparisons

Tool or approachBest forKey behavior
StreamAny bytes, including binary and textReads and writes raw bytes.
StreamReaderReading text from a byte streamDecodes bytes into char values using an encoding.
StreamWriterWriting text to a byte streamEncodes characters into bytes using an encoding.
FileStreamReading or writing a file as bytesA stream connected to a file.
File.ReadAllBytes / WriteAllBytesSmall binary filesConvenient, but loads the entire file into memory.
CopyTo / CopyToAsyncSmall or large streamsTransfers data in chunks rather than requiring all bytes in memory.
Base64Sending bytes through text-only formatsIncreases size and is not a replacement for binary storage.

For unknown content types, prefer Stream and CopyTo. Use readers and writers only when the contract clearly says the content is text.

Take Quiz

Cheat Sheet

// Copy any binary or text content without transforming it
using FileStream file = File.Create(path);
input.CopyTo(file);
// Asynchronous copy
await using FileStream file = File.Create(path);
await input.CopyToAsync(file, cancellationToken);
// Reset only when supported and needed
if (input.CanSeek)
{
    input.Position = 0;
}
// Text only: specify an encoding
string text = File.ReadAllText(path, Encoding.UTF8);
File.WriteAllText(path, text, Encoding.UTF8);
  • Use Stream for PDFs, images, archives, and arbitrary uploads.
  • Do not use StreamReader to copy binary content.
  • CopyTo starts from the stream's current position.
  • Dispose output streams with using or await using.
  • SQL Server binary content belongs in varbinary(max), not a text column.
  • Text encoding is relevant for text, not for preserving arbitrary binary files.

FAQ

Can I save a StreamReader directly to a file in C#?

Not safely for binary data. A StreamReader exposes decoded text, not the original byte sequence. Keep and copy the original Stream for images, PDFs, and other binary files.

Does CopyTo work for JPG, GIF, and PDF files?

Yes. CopyTo copies raw bytes and does not need to know the file format.

Do I need an encoding to store a file in SQL Server?

Not for binary files. Store the raw bytes in a binary column such as varbinary(max). Encoding is needed when you convert text between bytes and characters.

What SQL Server type should store uploaded file data?

varbinary(max) is the usual type for file bytes. Store metadata, such as original filename, MIME type, and upload date, in separate columns as needed.

Why is my saved file empty or incomplete?

The input stream may already be at its end, or it may have been disposed. If it is seekable and should be copied from the beginning, set Position to 0 before copying.

Should I use CopyTo or CopyToAsync?

Use CopyToAsync in asynchronous application code, especially web applications and services. CopyTo is fine for simple synchronous programs and scripts.

Does File.WriteAllBytes work instead?

Yes for small files when you already have a byte[]. For an existing stream or potentially large file, CopyTo is usually better because it avoids loading everything into memory.

Take Quiz

Mini Project

Description

Build a small utility that saves an incoming attachment stream to an uploads folder. It demonstrates safe byte-for-byte copying, directory creation, asynchronous file I/O, and a simple filename safety check.

Goal

Create a method that saves a PDF, image, or any other binary stream to a chosen uploads directory without corrupting its bytes.

Requirements

Requirement 1

Take Quiz

Keep learning

Related questions

View more