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.
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:
File.OpenRead("source.pdf")opens the source file and creates a stream positioned at byte0.File.Create("saved.pdf")creates or overwritessaved.pdfand opens it for writing.CopyTorepeatedly reads a chunk of bytes fromsource.- Each chunk is immediately written to
destinationwithout being decoded as text. - At the end of the source stream,
CopyTostops. - The
usingdeclarations 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.
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 approach | Best for | Key behavior |
|---|---|---|
Stream | Any bytes, including binary and text | Reads and writes raw bytes. |
StreamReader | Reading text from a byte stream | Decodes bytes into char values using an encoding. |
StreamWriter | Writing text to a byte stream | Encodes characters into bytes using an encoding. |
FileStream | Reading or writing a file as bytes | A stream connected to a file. |
File.ReadAllBytes / WriteAllBytes | Small binary files | Convenient, but loads the entire file into memory. |
CopyTo / CopyToAsync | Small or large streams | Transfers data in chunks rather than requiring all bytes in memory. |
| Base64 | Sending bytes through text-only formats | Increases 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.
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
Streamfor PDFs, images, archives, and arbitrary uploads. - Do not use
StreamReaderto copy binary content. CopyTostarts from the stream's current position.- Dispose output streams with
usingorawait 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.
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
Keep learning
Related questions
AddTransient vs AddScoped vs AddSingleton in ASP.NET Core Dependency Injection
Learn the differences between AddTransient, AddScoped, and AddSingleton in ASP.NET Core DI with examples and practical usage.
Best Way to Repeat a Character in C#: Building Repeated Strings Efficiently
Learn the best way to repeat a character in C#, compare StringBuilder, string concatenation, and simpler built-in options.
C# Access Modifiers and static: public, private, protected, and Defaults
Learn how C# public, private, protected, and default access control visibility, and how static differs from instance members.