Files, Directories and Streams

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Almost every program reads or writes a file sooner or later: a catalogue exported to CSV, a configuration file, a log, a cache. This page covers the .NET types that do it (System.IO), the Visual Basic additions on top of them (Microsoft.VisualBasic.FileIO.FileSystem, My.Computer.FileSystem and TextFieldParser), the numbered-file statements inherited from BASIC, and the habits that keep file code correct: disposing streams, choosing an encoding, replacing a file safely, and checking where a path lands. It was written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and checked on 2026-10-19. Every program was built with Option Strict On and TreatWarningsAsErrors and run in a Linux container, so path separators are /, paths are case-sensitive and the temporary directory is /tmp; the programs create a uniquely named folder under the temporary directory, print only relative names, and delete it afterwards. The project root namespace is ShelfDocs, which appears in some exception messages. System.IO is not one of the namespaces the SDK imports for Visual Basic projects, so each program that uses File, Path or Directory starts with Imports System.IO.

Serialization of objects into JSON and XML files is on Serialization: JSON and XML. The Using statement and disposal in general are on Object Lifetime and Disposal, the Microsoft.VisualBasic module functions that this page touches are listed on The Visual Basic Runtime Library, and My is on The My Namespace. Text encoding of strings is on Strings and Text, and the C# view of the same APIs is on Memory Management and Disposal in the C# reference.

The System.IO map

The types fall into four groups. The static helpers do a whole job in one call and never leave a file open; the stream types give control and are the only way to handle data that does not fit in memory; the *Info classes cache metadata about one file or folder; and the Visual Basic additions package common jobs with a different API.

Type Purpose Use it when

File, Directory, Path

Static helpers: read or write a whole file, copy, move, delete, list, and manipulate path strings

The data fits in memory and you want one call

FileInfo, DirectoryInfo

An object per file or folder with Length, Name, Exists, Parent and the same operations

You query the same entry several times, or filter a listing by size or date

FileStream

Bytes with position, seeking, sharing and a chosen access mode

Random access, binary formats, locking, large files

StreamReader, StreamWriter

Text over any stream, with an encoding

Line-by-line processing and large text files

BinaryReader, BinaryWriter

Primitive values over any stream, in a fixed byte order

Your own binary formats

MemoryStream, StringReader, StringWriter

The same APIs over memory

Tests, building a payload before sending it

GZipStream and friends

A stream that compresses or decompresses another stream

Archives, network payloads

FileSystemWatcher

Events when files in a folder change

Reacting to drops into an inbox folder

FileSystem in Microsoft.VisualBasic.FileIO

Recursive copy and delete, wildcards, text helpers, optional Windows UI

Migrating code that used My.Computer.FileSystem

TextFieldParser in Microsoft.VisualBasic.FileIO

Delimited and fixed-width text with quote handling

Reading CSV without a third-party package

FileOpen, PrintLine, Input, FilePut and the other numbered-file functions

The BASIC file model

Reading code written before 2002; never in new code

The File class: whole-file operations

File.ReadAllText, ReadAllLines, WriteAllText, WriteAllLines, AppendAllText and the byte versions open the file, do the work and close it, so they cannot leak a handle. File.ReadLines is the lazy cousin of ReadAllLines: it returns an IEnumerable(Of String) that reads as it is enumerated, so a loop over a large file keeps only one line in memory, but the file stays open until the enumeration finishes or is disposed. The text methods use UTF-8 without a byte order mark unless you pass an encoding (see the encodings section below).

Option Strict On
Imports System.IO
Imports System.Text

Module Ex01
    Sub Main()
        Dim root As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N"))
        Directory.CreateDirectory(root)
        Try
            Dim shelfFile As String = Path.Combine(root, "shelf.txt")

            ' Write, append, read: the one-call File methods.
            File.WriteAllText(shelfFile, "Dune" & vbLf & "Emma" & vbLf)
            File.AppendAllText(shelfFile, "Ulysses" & Environment.NewLine)
            File.AppendAllLines(shelfFile, {"Hamlet", "Beloved"})

            Dim lines As String() = File.ReadAllLines(shelfFile)
            Console.WriteLine($"ReadAllLines: {lines.Length} lines, first={lines(0)}, last={lines(lines.Length - 1)}")
            Console.WriteLine($"ReadAllText length: {File.ReadAllText(shelfFile).Length}")

            ' ReadLines is lazy: it streams, and the file stays open until the enumeration ends.
            Dim longTitles As Integer = 0
            For Each title As String In File.ReadLines(shelfFile)
                If title.Length > 5 Then longTitles += 1
            Next
            Console.WriteLine($"Titles longer than 5 characters: {longTitles}")

            ' Existence, copy, move, delete.
            Dim backup As String = Path.Combine(root, "shelf.bak")
            File.Copy(shelfFile, backup)
            Console.WriteLine($"Exists: {File.Exists(shelfFile)} {File.Exists(backup)} {File.Exists(Path.Combine(root, "nope.txt"))}")
            Try
                File.Copy(shelfFile, backup)
            Catch ex As IOException
                Console.WriteLine($"Copy over existing file: {ex.GetType().Name}")
            End Try
            File.Copy(shelfFile, backup, overwrite:=True)
            Dim moved As String = Path.Combine(root, "archive.txt")
            File.Move(backup, moved)
            Console.WriteLine($"After move: {File.Exists(backup)} {File.Exists(moved)}")
            File.Delete(moved)
            File.Delete(moved) ' Deleting a file that does not exist is not an error.
            Console.WriteLine($"After delete: {File.Exists(moved)}")

            ' Metadata without opening the file.
            Console.WriteLine($"Length of shelf.txt in bytes: {New FileInfo(shelfFile).Length}")
            Console.WriteLine($"Attributes: {File.GetAttributes(shelfFile)}")
            File.SetLastWriteTimeUtc(shelfFile, New Date(2026, 1, 15, 8, 30, 0, DateTimeKind.Utc))
            Console.WriteLine($"Last write (UTC): {File.GetLastWriteTimeUtc(shelfFile):yyyy-MM-dd HH:mm}")

            ' Bytes.
            File.WriteAllBytes(Path.Combine(root, "raw.bin"), {&H44, &H75, &H6E, &H65})
            Console.WriteLine($"Bytes as text: {Encoding.UTF8.GetString(File.ReadAllBytes(Path.Combine(root, "raw.bin")))}")

            ' The errors you meet.
            Try
                File.ReadAllText(Path.Combine(root, "missing.txt"))
            Catch ex As FileNotFoundException
                Console.WriteLine($"FileNotFoundException, FileName ends with: {Path.GetFileName(ex.FileName)}")
            End Try
            Try
                File.ReadAllText(Path.Combine(root, "no-such-dir", "x.txt"))
            Catch ex As DirectoryNotFoundException
                Console.WriteLine("DirectoryNotFoundException for a missing folder")
            End Try
            Try
                File.ReadAllText(root)
            Catch ex As UnauthorizedAccessException
                Console.WriteLine("UnauthorizedAccessException when the path is a directory")
            End Try
        Finally
            Directory.Delete(root, recursive:=True)
        End Try
    End Sub
End Module
ReadAllLines: 5 lines, first=Dune, last=Beloved
ReadAllText length: 33
Titles longer than 5 characters: 3
Exists: True True False
Copy over existing file: IOException
After move: False True
After delete: False
Length of shelf.txt in bytes: 33
Attributes: Normal
Last write (UTC): 2026-01-15 08:30
Bytes as text: Dune
FileNotFoundException, FileName ends with: missing.txt
DirectoryNotFoundException for a missing folder
UnauthorizedAccessException when the path is a directory

The exceptions are part of the contract, and which one you get tells you what to do.

Exception Typical cause and response

FileNotFoundException

The file does not exist. ex.FileName holds the path. File.Exists before opening is a race (the file can vanish in between); catch the exception where a missing file is a normal case.

DirectoryNotFoundException

A folder in the path does not exist. Directory.CreateDirectory creates all missing levels and does nothing if the folder exists.

UnauthorizedAccessException

No permission, a read-only file, or (as in the example) a path that names a directory where a file was expected.

IOException

The file is in use, the target already exists (File.Copy and FileMode.CreateNew without overwrite), the directory is not empty, the disk is full, or the device is unavailable. FileNotFoundException, DirectoryNotFoundException and PathTooLongException derive from IOException, so catch them before it.

PathTooLongException

A path longer than the platform allows. Modern Windows with long path support enabled, and Linux, accept much longer paths than the classic 260 characters.

ArgumentException, NotSupportedException

The path is empty, contains an invalid character, or has an invalid format (a colon in the wrong place on Windows).

IOException with a sharing violation

Another process holds the file open (the message says the file is being used by another process). Retrying after a short delay is the usual response.

Visual Basic version notes: File.Move(source, target, overwrite) exists since .NET Core 3.0; ReadLinesAsync and ReadExactly since .NET 7; none of these existed on .NET Framework 4.8, where File.Move always failed if the target existed.

Paths and directories

A path is only a string until it is used, and Path manipulates strings without touching the disk. That makes it safe to call with paths that do not exist, and also means it cannot tell you whether a path is valid on disk. Three rules save most path bugs: never build a path with & and a literal separator (use Path.Combine or Path.Join); never trust a relative path (it resolves against the process’s current directory, which is not the executable’s directory); and never combine a fixed root with untrusted input without checking where the result lands (see the safety section below).

Option Strict On
Imports System.IO

Module Ex02
    Sub Main()
        ' Path works on strings only: it never touches the disk.
        Dim full As String = Path.Combine("data", "shelf", "books.csv")
        Console.WriteLine($"Combine: {full.Replace(Path.DirectorySeparatorChar, "/"c)}")
        Console.WriteLine($"Separator: {Path.DirectorySeparatorChar}  Alt: {Path.AltDirectorySeparatorChar}  PathSeparator: {Path.PathSeparator}")
        Console.WriteLine($"GetFileName: {Path.GetFileName(full)}")
        Console.WriteLine($"GetFileNameWithoutExtension: {Path.GetFileNameWithoutExtension(full)}")
        Console.WriteLine($"GetExtension: {Path.GetExtension(full)}")
        Console.WriteLine($"GetDirectoryName: {Path.GetDirectoryName(full)?.Replace(Path.DirectorySeparatorChar, "/"c)}")
        Console.WriteLine($"ChangeExtension: {Path.ChangeExtension(full, ".json").Replace(Path.DirectorySeparatorChar, "/"c)}")
        Console.WriteLine($"HasExtension: {Path.HasExtension("README")} {Path.HasExtension("a.md")}")
        Console.WriteLine($"IsPathRooted: {Path.IsPathRooted(full)} {Path.IsPathRooted("/etc")}")

        ' Combine discards everything before a rooted argument; Join does not.
        Console.WriteLine($"Combine with rooted part: {Path.Combine("data", "/etc/passwd")}")
        Console.WriteLine($"Join with rooted part:    {Path.Join("data", "/etc/passwd")}")

        ' Relative paths.
        Dim a As String = Path.GetFullPath("/srv/shelf/data/books.csv")
        Dim b As String = "/srv/shelf"
        Console.WriteLine($"GetRelativePath: {Path.GetRelativePath(b, a)}")

        ' Random and temporary names.
        Console.WriteLine($"GetRandomFileName length: {Path.GetRandomFileName().Length}")
        Dim tempName As String = Path.GetTempFileName()
        Console.WriteLine($"GetTempFileName created a file: {File.Exists(tempName)}")
        File.Delete(tempName)

        ' Invalid characters depend on the operating system.
        Console.WriteLine($"Invalid file-name characters on this OS: {Path.GetInvalidFileNameChars().Length}")

        ' Directory and DirectoryInfo.
        Dim root As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N"))
        Try
            Directory.CreateDirectory(Path.Combine(root, "catalog", "2026"))
            Directory.CreateDirectory(Path.Combine(root, "catalog", "2025"))
            File.WriteAllText(Path.Combine(root, "catalog", "books.csv"), "x")
            File.WriteAllText(Path.Combine(root, "catalog", "2026", "new.csv"), "x")
            File.WriteAllText(Path.Combine(root, "catalog", "2026", "notes.txt"), "x")
            File.WriteAllText(Path.Combine(root, "catalog", "2025", "old.csv"), "x")

            Dim catalog As String = Path.Combine(root, "catalog")
            Console.WriteLine("Top-level entries:")
            For Each entry As String In Directory.EnumerateFileSystemEntries(catalog).OrderBy(Function(s) s, StringComparer.Ordinal)
                Console.WriteLine("  " & Path.GetRelativePath(root, entry).Replace(Path.DirectorySeparatorChar, "/"c))
            Next
            Console.WriteLine("All CSV files, recursively:")
            For Each csvFile As String In Directory.EnumerateFiles(catalog, "*.csv", SearchOption.AllDirectories).OrderBy(Function(s) s, StringComparer.Ordinal)
                Console.WriteLine("  " & Path.GetRelativePath(root, csvFile).Replace(Path.DirectorySeparatorChar, "/"c))
            Next
            Console.WriteLine($"Directories: {String.Join(", ", Directory.GetDirectories(catalog).Select(Function(d) Path.GetFileName(d)).OrderBy(Function(s) s, StringComparer.Ordinal))}")

            Dim info As New DirectoryInfo(catalog)
            Console.WriteLine($"DirectoryInfo: Name={info.Name}, Parent={info.Parent?.Name?.StartsWith("shelf-")}, Exists={info.Exists}")
            Dim total As Long = info.EnumerateFiles("*", SearchOption.AllDirectories).Sum(Function(f) f.Length)
            Console.WriteLine($"Total size: {total} bytes in {info.GetFiles("*", SearchOption.AllDirectories).Length} files")

            ' Deleting a non-empty directory needs recursive:=True.
            Try
                Directory.Delete(catalog)
            Catch ex As IOException
                Console.WriteLine($"Delete non-empty directory: {ex.GetType().Name}")
            End Try
            Directory.Delete(catalog, recursive:=True)
            Console.WriteLine($"Catalog exists after recursive delete: {Directory.Exists(catalog)}")
        Finally
            Directory.Delete(root, recursive:=True)
        End Try
    End Sub
End Module
Combine: data/shelf/books.csv
Separator: /  Alt: /  PathSeparator: :
GetFileName: books.csv
GetFileNameWithoutExtension: books
GetExtension: .csv
GetDirectoryName: data/shelf
ChangeExtension: data/shelf/books.json
HasExtension: False True
IsPathRooted: False True
Combine with rooted part: /etc/passwd
Join with rooted part:    data/etc/passwd
GetRelativePath: data/books.csv
GetRandomFileName length: 12
GetTempFileName created a file: True
Invalid file-name characters on this OS: 2
Top-level entries:
  catalog/2025
  catalog/2026
  catalog/books.csv
All CSV files, recursively:
  catalog/2025/old.csv
  catalog/2026/new.csv
  catalog/books.csv
Directories: 2025, 2026
DirectoryInfo: Name=catalog, Parent=True, Exists=True
Total size: 4 bytes in 4 files
Delete non-empty directory: IOException
Catalog exists after recursive delete: False
Member Behaviour worth knowing

Path.Combine(a, b)

Joins with the platform separator, but if b is rooted the result is b alone, discarding a. This is the bug behind several directory traversal vulnerabilities.

Path.Join(a, b)

Joins and never discards: a rooted b is appended. (.NET Core 2.1.)

Path.GetFullPath(p)

Resolves . and .. segments and relative paths against the current directory. It does not check that the path exists.

Path.GetRelativePath(base, p)

The reverse: how to reach p starting at base. (.NET Core 2.0.)

Path.GetExtension, GetFileName, GetFileNameWithoutExtension, GetDirectoryName, ChangeExtension

String operations. The extension includes the leading dot; GetDirectoryName returns Nothing for a root.

Path.GetTempPath(), GetTempFileName(), GetRandomFileName()

The temporary folder, a created empty file with a unique name, and a random name without creating anything. GetTempFileName creates a file, so it must be deleted.

Path.GetInvalidFileNameChars()

Depends on the operating system: two characters on Linux (the slash and NUL), many more on Windows. Code that validates file names with this list on Linux accepts names that Windows rejects.

Path.DirectorySeparatorChar, AltDirectorySeparatorChar, PathSeparator

\ and / on Windows (both separators are accepted there), / on Linux and macOS, and ; or : for the separator in a list of paths such as PATH.

Directory and DirectoryInfo list and manage folders. The Get* methods return an array after reading everything; the Enumerate* methods stream results, which matters for folders with many entries. Neither guarantees an order, so sort when the output must be stable (the example sorts with StringComparer.Ordinal). A wildcard such as *.csv is applied to the file name; a three-letter extension filter on Windows also matches longer extensions that begin with the same three letters, a legacy short-name quirk that Linux does not share.

Reading and writing text: StreamReader, StreamWriter and FileStream

When the data is large or arrives gradually, open a stream. StreamWriter and StreamReader add an encoding and line handling on top of a byte stream; FileStream is the byte stream itself with position and seeking. Always wrap them in Using, so that End Using calls Dispose, which flushes buffered data and releases the file even if an exception is thrown.

Option Strict On
Imports System.IO
Imports System.Text

Module Ex03
    Sub Main()
        Dim root As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N"))
        Directory.CreateDirectory(root)
        Try
            Dim report As String = Path.Combine(root, "report.txt")

            ' StreamWriter / StreamReader: text, line by line. Using closes (and flushes) the file.
            Using writer As New StreamWriter(report, append:=False, encoding:=New UTF8Encoding(encoderShouldEmitUTF8Identifier:=False))
                writer.WriteLine("Shelf report")
                writer.WriteLine($"{"Dune",-10}{3,5}")
                writer.Write("Total: ")
                writer.WriteLine(3)
            End Using

            Using reader As New StreamReader(report)
                Dim lineNumber As Integer = 0
                Dim line As String = reader.ReadLine()
                Do While line IsNot Nothing
                    lineNumber += 1
                    Console.WriteLine($"{lineNumber}: {line}")
                    line = reader.ReadLine()
                Loop
                Console.WriteLine($"EndOfStream: {reader.EndOfStream}")
            End Using

            ' FileStream gives full control: mode, access, sharing, position.
            Dim dataFile As String = Path.Combine(root, "data.bin")
            Using stream As New FileStream(dataFile, FileMode.Create, FileAccess.ReadWrite, FileShare.None)
                stream.Write(Encoding.ASCII.GetBytes("ABCDEFGHIJ"))
                Console.WriteLine($"Position after write: {stream.Position}, Length: {stream.Length}")
                stream.Seek(2, SeekOrigin.Begin)
                Dim buffer(2) As Byte
                Dim read As Integer = stream.Read(buffer, 0, buffer.Length)
                Console.WriteLine($"Read {read} bytes: {Encoding.ASCII.GetString(buffer)}")
                stream.Seek(-1, SeekOrigin.End)
                stream.WriteByte(CByte(Asc("Z")))
                stream.SetLength(8)
            End Using
            Console.WriteLine($"File content: {File.ReadAllText(dataFile)}")

            ' FileMode behaviours.
            Try
                Using s As New FileStream(dataFile, FileMode.CreateNew)
                End Using
            Catch ex As IOException
                Console.WriteLine($"FileMode.CreateNew on existing file: {ex.GetType().Name}")
            End Try
            Try
                Using s As New FileStream(Path.Combine(root, "absent.bin"), FileMode.Open)
                End Using
            Catch ex As FileNotFoundException
                Console.WriteLine("FileMode.Open on missing file: FileNotFoundException")
            End Try
            Using s As New FileStream(dataFile, FileMode.Append, FileAccess.Write)
                s.Write(Encoding.ASCII.GetBytes("++"))
            End Using
            Console.WriteLine($"After Append: {File.ReadAllText(dataFile)}")
            Using s As New FileStream(dataFile, FileMode.Truncate, FileAccess.Write)
            End Using
            Console.WriteLine($"After Truncate: length {New FileInfo(dataFile).Length}")

            ' Sharing: a second writer is refused while the first is open.
            Using first As New FileStream(dataFile, FileMode.Open, FileAccess.ReadWrite, FileShare.None)
                Try
                    Using second As New FileStream(dataFile, FileMode.Open, FileAccess.Read, FileShare.Read)
                    End Using
                Catch ex As IOException
                    Console.WriteLine($"Second open while locked: {ex.GetType().Name}")
                End Try
            End Using
        Finally
            Directory.Delete(root, recursive:=True)
        End Try
    End Sub
End Module
1: Shelf report
2: Dune          3
3: Total: 3
EndOfStream: True
Position after write: 10, Length: 10
Read 3 bytes: CDE
File content: ABCDEFGH
FileMode.CreateNew on existing file: IOException
FileMode.Open on missing file: FileNotFoundException
After Append: ABCDEFGH++
After Truncate: length 0
Second open while locked: IOException

The mode, access and sharing parameters of the FileStream constructor decide what the open does.

FileMode Behaviour

CreateNew

Creates a new file; throws IOException if it already exists. Use it to avoid overwriting.

Create

Creates the file, or truncates it to zero length if it exists.

Open

Opens an existing file; FileNotFoundException if it does not exist.

OpenOrCreate

Opens the file or creates an empty one.

Truncate

Opens an existing file and empties it.

Append

Opens or creates the file and positions at the end; allowed only with FileAccess.Write, and the position cannot be moved before the original end.

FileAccess (Read, Write, ReadWrite) states what this stream may do, and FileShare (None, Read, Write, ReadWrite, Delete) states what other streams may do while it is open. The shared default for File.OpenRead is FileShare.Read; for File.Create and File.OpenWrite it is FileShare.None. On Windows the operating system enforces sharing; on Linux and macOS .NET emulates it with advisory locks, which protects against other .NET processes but not against programs that ignore the locks. A StreamWriter buffers: data reaches the file only on Flush, Close or Dispose, so a program that crashes before disposing loses the last buffered text.

Reading a text file line by line is the same loop in every program: ReadLine returns Nothing at the end, so the idiomatic loop is Do While line IsNot Nothing, reading the next line at the bottom, or Do Until reader.EndOfStream. The page on Control Flow shows the sentinel-loop pattern in general.

Disposing streams: the File.Create leak

A common beginner program writes File.Create(path) and discards the result, or keeps the FileStream in a variable and forgets it. File.Create does not only create the file: it returns the open FileStream, and until that object is disposed the file is locked. The garbage collector eventually finalizes the stream, but not at a time the program controls, so the failure is intermittent and often appears only on a faster machine or in a release build.

Option Strict On
Imports System.IO

Module Ex04
    ' The classic leak: File.Create returns an open FileStream that nobody disposes.
    Sub Main()
        Dim root As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N"))
        Directory.CreateDirectory(root)
        Try
            Dim target As String = Path.Combine(root, "leak.txt")

            ' Wrong: the returned FileStream stays open until the garbage collector finalizes it.
            Dim leaked As FileStream = File.Create(target)
            Try
                File.WriteAllText(target, "second writer")
            Catch ex As IOException
                Console.WriteLine($"Leak: writing the file again fails with {ex.GetType().Name}")
            End Try
            leaked.Dispose()
            File.WriteAllText(target, "after dispose")
            Console.WriteLine($"After Dispose: {File.ReadAllText(target)}")

            ' Right: Using disposes the stream at End Using, even when an exception is thrown.
            Using created As FileStream = File.Create(target)
                created.WriteByte(65)
            End Using
            File.WriteAllText(target, "ok")
            Console.WriteLine($"With Using: {File.ReadAllText(target)}")

            ' Even simpler: one-call helpers never leave a stream open.
            File.WriteAllText(target, "helpers close the file")
            Console.WriteLine(File.ReadAllText(target))

            ' Dispose order: the writer owns the stream, so disposing the writer is enough.
            Dim fs As New FileStream(target, FileMode.Create)
            Using writer As New StreamWriter(fs)
                writer.Write("owned")
            End Using
            Console.WriteLine($"Stream CanWrite after the writer was disposed: {fs.CanWrite}")
        Finally
            Directory.Delete(root, recursive:=True)
        End Try
    End Sub
End Module
Leak: writing the file again fails with IOException
After Dispose: after dispose
With Using: ok
helpers close the file
Stream CanWrite after the writer was disposed: False

The rules are the ones on Object Lifetime and Disposal: dispose what you create; the object that wraps a stream (StreamWriter, StreamReader, BinaryWriter, GZipStream) owns it by default and disposes it too, so disposing the outer object is enough; and the one-call File helpers never leave anything open. The analyzer rule CA2000 flags a disposable that is not disposed on every path, and CA2022 flags a Stream.Read whose result is ignored (see the binary section).

Encodings

Bytes become characters only through an encoding, and files do not say which one they use, except by an optional byte order mark (BOM) at the start. .NET Core and .NET 5 and later use UTF-8 as the default for the File text helpers and StreamReader; Encoding.Default is UTF-8 on every platform, where it was the legacy ANSI code page of the machine on .NET Framework. The rest of this section is what to do when the file is not UTF-8.

Option Strict On
Imports System.IO
Imports System.Text

Module Ex05
    Sub Main()
        Dim root As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N"))
        Directory.CreateDirectory(root)
        Try
            Dim text As String = "Cien años de soledad"
            Dim names As String() = {"utf8", "utf8bom", "utf16", "utf16be", "latin1", "ascii"}
            Dim encodings As Encoding() = {
                New UTF8Encoding(False), New UTF8Encoding(True), Encoding.Unicode,
                Encoding.BigEndianUnicode, Encoding.Latin1, Encoding.ASCII}

            For i As Integer = 0 To names.Length - 1
                Dim path1 As String = Path.Combine(root, names(i) & ".txt")
                File.WriteAllText(path1, text, encodings(i))
                Dim bytes As Byte() = File.ReadAllBytes(path1)
                Console.WriteLine($"{names(i),-8} {bytes.Length,3} bytes, starts {Convert.ToHexString(bytes, 0, 4)}, default read: {File.ReadAllText(path1)}")
            Next

            ' Reading with the wrong encoding gives mojibake, not an exception.
            Dim latinPath As String = Path.Combine(root, "latin1.txt")
            Console.WriteLine($"Latin-1 file read as UTF-8: {File.ReadAllText(latinPath, New UTF8Encoding(False, throwOnInvalidBytes:=False))}")
            Try
                File.ReadAllText(latinPath, New UTF8Encoding(False, throwOnInvalidBytes:=True))
            Catch ex As DecoderFallbackException
                Console.WriteLine("Strict UTF-8 refuses the Latin-1 bytes: DecoderFallbackException")
            End Try

            ' StreamReader detects a byte order mark by default and tells you what it found.
            Using reader As New StreamReader(Path.Combine(root, "utf16.txt"))
                reader.Peek()
                Console.WriteLine($"Detected from BOM: {reader.CurrentEncoding.EncodingName}")
            End Using
            Using reader As New StreamReader(Path.Combine(root, "utf16.txt"), Encoding.Latin1, detectEncodingFromByteOrderMarks:=False)
                Console.WriteLine($"UTF-16 file read as Latin-1 (no detection), first line length: {reader.ReadLine().Length}")
            End Using

            ' Characters that the encoding cannot represent are replaced.
            File.WriteAllText(Path.Combine(root, "ascii2.txt"), "Ñandú", Encoding.ASCII)
            Console.WriteLine($"Written as ASCII: {File.ReadAllText(Path.Combine(root, "ascii2.txt"))}")

            ' Code pages beyond the built-in ones need the System.Text.Encoding.CodePages provider.
            Try
                Console.WriteLine(Encoding.GetEncoding(1252).EncodingName)
            Catch ex As NotSupportedException
                Console.WriteLine($"GetEncoding(1252) without the provider: {ex.GetType().Name}")
            End Try
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
            Console.WriteLine($"After RegisterProvider: {Encoding.GetEncoding(1252).EncodingName}")
        Finally
            Directory.Delete(root, recursive:=True)
        End Try
    End Sub
End Module
utf8      21 bytes, starts 4369656E, default read: Cien años de soledad
utf8bom   24 bytes, starts EFBBBF43, default read: Cien años de soledad
utf16     42 bytes, starts FFFE4300, default read: Cien años de soledad
utf16be   42 bytes, starts FEFF0043, default read: Cien años de soledad
latin1    20 bytes, starts 4369656E, default read: Cien a�os de soledad
ascii     20 bytes, starts 4369656E, default read: Cien a?os de soledad
Latin-1 file read as UTF-8: Cien a�os de soledad
Strict UTF-8 refuses the Latin-1 bytes: DecoderFallbackException
Detected from BOM: Unicode
UTF-16 file read as Latin-1 (no detection), first line length: 42
Written as ASCII: ?and?
GetEncoding(1252) without the provider: NotSupportedException
After RegisterProvider: Western European (Windows)
Encoding Notes Bytes for the text above

New UTF8Encoding(False)

UTF-8 without a BOM; the default of File.WriteAllText. The right choice for new files, JSON, source code and most tools on Linux.

21

New UTF8Encoding(True) or Encoding.UTF8

UTF-8 that writes the three-byte BOM EF BB BF. Excel on Windows usually needs it to open a UTF-8 CSV with accented characters correctly. Passing Encoding.UTF8 to a StreamWriter or WriteAllText writes the BOM.

24

Encoding.Unicode

UTF-16 little-endian with the BOM FF FE; the native format of .NET strings and of many Windows files.

42

Encoding.BigEndianUnicode

UTF-16 big-endian with the BOM FE FF.

42

Encoding.Latin1

ISO-8859-1, one byte per character for the first 256 code points. Available since .NET 5; Encoding.GetEncoding("iso-8859-1") gives the same.

20

Encoding.ASCII

Seven bits; every character above 127 is replaced with ? when writing and when decoding.

20

Windows code pages (Encoding.GetEncoding(1252))

Not built in on .NET Core: register CodePagesEncodingProvider.Instance first, otherwise NotSupportedException.

20

A StreamReader detects a UTF-8, UTF-16 or UTF-32 BOM by default (detectEncodingFromByteOrderMarks:=True) and prefers it over the encoding you pass. A file without a BOM is decoded with the encoding you pass or with UTF-8; that is why a Latin-1 file read as UTF-8 shows replacement characters instead of failing. To make the failure loud, create the decoder with throwOnInvalidBytes:=True, as the example does.

Asynchronous file I/O

File reads and writes are I/O, so they benefit from Async and Await in programs that must stay responsive (a user-interface thread) or that serve many requests (a web service). The File class has async twins for the whole-file methods (ReadAllTextAsync, ReadAllLinesAsync, WriteAllTextAsync, WriteAllLinesAsync, AppendAllTextAsync), StreamReader and StreamWriter have ReadLineAsync, ReadToEndAsync and WriteLineAsync, and every stream has ReadAsync and WriteAsync. A FileStream opened with useAsync:=True (or FileOptions.Asynchronous) uses overlapped I/O on Windows; without it, async calls run the blocking call on a thread-pool thread.

Option Strict On
Imports System.IO
Imports System.Threading

Module Ex06
    Sub Main()
        ' Visual Basic has no Async Main: bridge from Sub Main.
        RunAsync().GetAwaiter().GetResult()
    End Sub

    Private Async Function RunAsync() As Task
        Dim root As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N"))
        Directory.CreateDirectory(root)
        Try
            Dim target As String = Path.Combine(root, "loans.txt")
            Dim lines As String() = {"978-0-441-17271-9,Dune,2026-01-05", "978-0-14-143951-8,Emma,2026-01-09", "978-0-14-243724-7,Beloved,2026-01-12"}

            Await File.WriteAllLinesAsync(target, lines)
            Dim readBack As String() = Await File.ReadAllLinesAsync(target)
            Console.WriteLine($"Round trip: {readBack.Length} lines")

            ' Stream with useAsync:=True (FileOptions.Asynchronous) for real overlapped I/O.
            Using stream As New FileStream(target, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync:=True)
                Using reader As New StreamReader(stream)
                    Dim count As Integer = 0
                    Dim line As String = Await reader.ReadLineAsync()
                    Do While line IsNot Nothing
                        count += 1
                        line = Await reader.ReadLineAsync()
                    Loop
                    Console.WriteLine($"Counted {count} lines asynchronously")
                End Using
            End Using

            ' Several files in parallel with Task.WhenAll.
            Dim names As String() = {"a.txt", "b.txt", "c.txt"}
            Dim writes As New List(Of Task)()
            For Each name As String In names
                writes.Add(File.WriteAllTextAsync(Path.Combine(root, name), "file " & name))
            Next
            Await Task.WhenAll(writes)
            Dim reads As New List(Of Task(Of String))()
            For Each name As String In names
                reads.Add(File.ReadAllTextAsync(Path.Combine(root, name)))
            Next
            Dim all As String() = Await Task.WhenAll(reads)
            Console.WriteLine(String.Join(" | ", all))

            ' Streaming with an enumerator (VB has no Await For Each).
            Dim enumerator As IAsyncEnumerator(Of String) = File.ReadLinesAsync(target).GetAsyncEnumerator()
            Dim titles As New List(Of String)()
            Do While Await enumerator.MoveNextAsync()
                titles.Add(enumerator.Current.Split(","c)(1))
            Loop
            Await enumerator.DisposeAsync() ' Await is not allowed in Finally, so there is no Try/Finally here
            Console.WriteLine("ReadLinesAsync titles: " & String.Join(", ", titles))

            ' Cancellation.
            Using cts As New CancellationTokenSource()
                cts.Cancel()
                Try
                    Await File.ReadAllTextAsync(target, cts.Token)
                Catch ex As OperationCanceledException
                    Console.WriteLine($"Cancelled before reading: {ex.GetType().Name}")
                End Try
            End Using
        Finally
            Directory.Delete(root, recursive:=True)
        End Try
    End Function
End Module
Round trip: 3 lines
Counted 3 lines asynchronously
file a.txt | file b.txt | file c.txt
ReadLinesAsync titles: Dune, Emma, Beloved
Cancelled before reading: TaskCanceledException

Three Visual Basic limits show in this program. There is no Async Sub Main, so Sub Main calls RunAsync().GetAwaiter().GetResult(). There is no Await For Each, so an IAsyncEnumerable(Of T) is consumed by calling MoveNextAsync in a Do While loop and disposing the enumerator afterwards. And Await is not allowed in a Catch or Finally block (BC36943), which is why the enumerator is disposed after the loop instead of in a Try/Finally: if the loop throws, the enumerator is not disposed, which is acceptable for a file enumerator but not for one that holds a lock. The page on Async and Await has the rest of the rules. Cancellation surfaces as TaskCanceledException, a subclass of OperationCanceledException, so catch the base class.

Watching a folder

FileSystemWatcher raises Created, Changed, Deleted and Renamed events for a folder and, optionally, its subfolders. It sits on operating-system notification facilities (inotify on Linux, ReadDirectoryChangesW on Windows, FSEvents or kqueue on macOS) and inherits their limits. The events arrive on thread-pool threads, one save can raise several Changed events, a large burst can overflow the internal buffer (the Error event, InternalBufferOverflowException), and the event is raised as soon as the file appears, which may be before the writing program has finished, so reading it immediately can fail with a sharing violation. Production code treats events as hints: debounce them, retry the read, and rescan the folder after an Error.

Option Strict On
Imports System.IO
Imports System.Threading

Module Ex07
    Sub Main()
        Dim root As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N"))
        Directory.CreateDirectory(root)
        Try
            Dim seen As New System.Collections.Concurrent.ConcurrentQueue(Of String)()
            Dim renamed As New ManualResetEventSlim(False)

            Using watcher As New FileSystemWatcher(root, "*.csv")
                watcher.NotifyFilter = NotifyFilters.FileName Or NotifyFilters.LastWrite
                watcher.IncludeSubdirectories = False

                ' Handlers run on a thread-pool thread, not on the thread that called Main.
                AddHandler watcher.Created, Sub(sender As Object, e As FileSystemEventArgs) seen.Enqueue("Created " & e.Name)
                AddHandler watcher.Deleted, Sub(sender As Object, e As FileSystemEventArgs) seen.Enqueue("Deleted " & e.Name)
                AddHandler watcher.Renamed, Sub(sender As Object, e As RenamedEventArgs)
                                                seen.Enqueue("Renamed " & e.OldName & " -> " & e.Name)
                                                renamed.Set()
                                            End Sub
                AddHandler watcher.Error, Sub(sender As Object, e As ErrorEventArgs) seen.Enqueue("Error " & e.GetException().GetType().Name)
                watcher.EnableRaisingEvents = True

                File.WriteAllText(Path.Combine(root, "books.csv"), "x")
                File.WriteAllText(Path.Combine(root, "notes.txt"), "ignored: the filter is *.csv")
                Thread.Sleep(300)
                File.Move(Path.Combine(root, "books.csv"), Path.Combine(root, "catalog.csv"))
                renamed.Wait(TimeSpan.FromSeconds(5))
                File.Delete(Path.Combine(root, "catalog.csv"))
                Thread.Sleep(500)
            End Using

            For Each entry As String In seen
                Console.WriteLine(entry)
            Next
        Finally
            Directory.Delete(root, recursive:=True)
        End Try
    End Sub
End Module
Created books.csv
Renamed books.csv -> catalog.csv
Deleted catalog.csv

The handlers here are lambdas passed to AddHandler, because the watcher is a local variable and Handles needs a WithEvents field. Events from different files can arrive in any order, and the example waits for the rename with a ManualResetEventSlim instead of a fixed sleep. NotifyFilter and the filter pattern reduce the number of events; the text file written in the example does not raise one because the filter is *.csv.

Microsoft.VisualBasic.FileIO.FileSystem and My.Computer.FileSystem

Visual Basic 2005 added My.Computer.FileSystem, an object that wraps the common jobs behind friendlier names. The implementation is the class Microsoft.VisualBasic.FileIO.FileSystem, whose members are Shared and which therefore works without My, in any project type and on any platform. Its distinctive members are CopyDirectory and MoveDirectory (recursive in one call, with optional overwrite), DeleteDirectory with DeleteDirectoryOption.DeleteAllContents, GetFiles and GetDirectories with several wildcards and FileIO.SearchOption, FindInFiles, RenameFile and RenameDirectory, ReadAllText and WriteAllText with an append flag, OpenTextFieldParser, and the UIOption and RecycleOption parameters.

Option Strict On
Imports System.IO
Imports Microsoft.VisualBasic.FileIO

Module Ex08
    Sub Main()
        Dim root As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N"))
        Directory.CreateDirectory(root)
        Try
            Dim source As String = Path.Combine(root, "source")
            Dim copyTo As String = Path.Combine(root, "copy")
            ' FileSystem here is Microsoft.VisualBasic.FileIO.FileSystem: all Shared methods, usable from any project.
            FileSystem.CreateDirectory(Path.Combine(source, "sub"))
            FileSystem.WriteAllText(Path.Combine(source, "a.txt"), "alpha", append:=False)
            FileSystem.WriteAllText(Path.Combine(source, "a.txt"), " beta", append:=True)
            FileSystem.WriteAllText(Path.Combine(source, "sub", "b.log"), "log", append:=False)
            Console.WriteLine($"ReadAllText: {FileSystem.ReadAllText(Path.Combine(source, "a.txt"))}")

            ' Recursive copy in one call. The overloads that take a UIOption show Windows shell dialogs.
            Try
                FileSystem.CopyDirectory(source, copyTo, UIOption.OnlyErrorDialogs)
            Catch ex As PlatformNotSupportedException
                Console.WriteLine($"CopyDirectory with UIOption: {ex.Message}")
            End Try
            FileSystem.CopyDirectory(source, copyTo)
            Console.WriteLine($"Copied tree: {String.Join(", ", FileSystem.GetFiles(copyTo, FileIO.SearchOption.SearchAllSubDirectories).Select(Function(f) Path.GetRelativePath(copyTo, f).Replace(Path.DirectorySeparatorChar, "/"c)).OrderBy(Function(s) s, StringComparer.Ordinal))}")

            ' Wildcards are a filter list, not a single pattern.
            Dim logs As IEnumerable(Of String) = FileSystem.GetFiles(source, FileIO.SearchOption.SearchAllSubDirectories, "*.log")
            Console.WriteLine($"GetFiles *.log: {logs.Count()}")
            Dim hits As IEnumerable(Of String) = FileSystem.FindInFiles(source, "alpha", ignoreCase:=True, searchType:=FileIO.SearchOption.SearchAllSubDirectories)
            Console.WriteLine($"FindInFiles 'alpha': {hits.Count()} file(s)")

            ' Rename changes only the name; MoveFile changes the folder.
            FileSystem.RenameFile(Path.Combine(copyTo, "a.txt"), "renamed.txt")
            FileSystem.MoveFile(Path.Combine(copyTo, "renamed.txt"), Path.Combine(copyTo, "sub", "moved.txt"), overwrite:=True)
            Console.WriteLine($"Moved: {FileSystem.FileExists(Path.Combine(copyTo, "sub", "moved.txt"))}")

            ' The destination must not be inside the source.
            Try
                FileSystem.CopyDirectory(source, Path.Combine(source, "sub"))
            Catch ex As InvalidOperationException
                Console.WriteLine($"CopyDirectory into itself: {ex.GetType().Name}")
            End Try

            ' Metadata helpers.
            Dim info As FileInfo = FileSystem.GetFileInfo(Path.Combine(source, "a.txt"))
            Console.WriteLine($"GetFileInfo: {info.Name}, {info.Length} bytes")
            Console.WriteLine($"GetName/GetParentPath: {FileSystem.GetName("/x/y/z.txt")} {FileSystem.GetParentPath("/x/y/z.txt")}")
            Console.WriteLine($"CombinePath: {FileSystem.CombinePath("/data", "b.txt").Replace(Path.DirectorySeparatorChar, "/"c)}")

            ' Deleting: the recycle bin is a Windows concept.
            Try
                FileSystem.DeleteFile(Path.Combine(source, "a.txt"), UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin)
                Console.WriteLine($"Delete with SendToRecycleBin: exists afterwards = {File.Exists(Path.Combine(source, "a.txt"))}")
            Catch ex As PlatformNotSupportedException
                Console.WriteLine($"SendToRecycleBin: {ex.GetType().Name}: {ex.Message}")
            End Try
            FileSystem.DeleteFile(Path.Combine(source, "a.txt"))
            FileSystem.DeleteDirectory(source, DeleteDirectoryOption.DeleteAllContents)
            Console.WriteLine($"Deleted tree: {Not Directory.Exists(source)}")
        Finally
            Directory.Delete(root, recursive:=True)
        End Try
    End Sub
End Module
ReadAllText: alpha beta
CopyDirectory with UIOption: UI not available for copy or move
Copied tree: a.txt, sub/b.log
GetFiles *.log: 1
FindInFiles 'alpha': 1 file(s)
Moved: True
CopyDirectory into itself: InvalidOperationException
GetFileInfo: a.txt, 13 bytes
GetName/GetParentPath: z.txt /x/y
CombinePath: /data/b.txt
SendToRecycleBin: PlatformNotSupportedException: UI not available for copy or move
Deleted tree: True

Three observations come out of the run. First, the overloads that take UIOption.AllDialogs or UIOption.OnlyErrorDialogs use the Windows shell to show a progress and confirmation dialog; on Linux the call throws PlatformNotSupportedException, so cross-platform code uses the overloads without a UIOption. Second, RecycleOption.SendToRecycleBin has the same restriction: it requires the Windows shell, and a portable program should delete permanently or implement its own trash folder. Third, Imports Microsoft.VisualBasic.FileIO together with Imports System.IO makes SearchOption ambiguous, because both namespaces define an enum of that name with different members (AllDirectories against SearchAllSubDirectories); qualify it, or import only one of the namespaces.

Option Strict On
Imports System.IO
Imports Microsoft.VisualBasic.FileIO

Module Ex20
    Sub Main()
        Dim found As IEnumerable(Of String) = FileSystem.GetFiles(".", SearchOption.AllDirectories, "*.vb")
    End Sub
End Module
Ex20.vb(7,72): error BC30561: 'SearchOption' is ambiguous, imported from the namespaces or types 'System.IO, Microsoft.VisualBasic.FileIO'.

My.Computer.FileSystem is the same object reached through My. On .NET it needs a project that defines My.Computer, which today means a Windows Forms project (UseWindowsForms with a -windows target framework); the console MyType value of .NET Framework no longer works in the SDK-style projects of .NET 10 (the build reports BC30002 for Microsoft.VisualBasic.ApplicationServices.ConsoleApplicationBase), and with the SDK default (MyType empty) the My namespace has no Computer. The next two programs show both sides: the first was built for net10.0-windows with -p:EnableWindowsTargeting=true and was built, not run on this Linux machine; the second shows the error in a console project.

Option Strict On
Imports System.IO

Module Ex15
    Sub Main()
        Dim root As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N"))
        Try
            ' My.Computer.FileSystem is the same API as Microsoft.VisualBasic.FileIO.FileSystem, reached through My.
            My.Computer.FileSystem.CreateDirectory(root)
            Dim target As String = My.Computer.FileSystem.CombinePath(root, "note.txt")
            My.Computer.FileSystem.WriteAllText(target, "from My.Computer", append:=False)
            Console.WriteLine(My.Computer.FileSystem.ReadAllText(target))
            Console.WriteLine($"Drives reported: {My.Computer.FileSystem.Drives.Count > 0}")
            Console.WriteLine($"Special directory Temp is set: {My.Computer.FileSystem.SpecialDirectories.Temp.Length > 0}")
        Finally
            If My.Computer.FileSystem.DirectoryExists(root) Then
                My.Computer.FileSystem.DeleteDirectory(root, FileIO.DeleteDirectoryOption.DeleteAllContents)
            End If
        End Try
    End Sub
End Module
Option Strict On
Imports System.IO

Module Ex16
    Sub Main()
        ' With the default console MyType, My.Computer is not available.
        My.Computer.FileSystem.WriteAllText("x.txt", "x", False)
    End Sub
End Module
Ex16.vb(7,9): error BC30456: 'Computer' is not a member of 'ShelfDocs.My'.

A program that must run in a console or web project and still use these members writes Microsoft.VisualBasic.FileIO.FileSystem.ReadAllText(...) instead of My.Computer.FileSystem.ReadAllText(...); the two are the same calls. The page on The My Namespace lists which My objects are available per project type.

CSV with TextFieldParser

The .NET libraries have no CSV reader. Visual Basic ships one, TextFieldParser, in the Microsoft.VisualBasic.FileIO namespace since Visual Basic 2005, and it is also usable from C# by referencing Microsoft.VisualBasic.dll, which is part of every .NET project (a C# project needs using Microsoft.VisualBasic.FileIO;). It reads delimited or fixed-width text and handles quoted fields; it does not write anything.

CSV is not one format. RFC 4180 describes the common one and the parser follows it closely:

RFC 4180 rule Meaning for a reader

One record per line, lines end with CRLF

TextFieldParser also accepts a lone LF as the line end.

The last record may or may not end with a line break

Both work; EndOfData becomes True after the last record either way.

An optional header record

The parser does not treat the first record specially: read it with ReadFields and keep or discard it.

Fields are separated by commas

Any delimiter works with SetDelimiters (semicolons are common in locales that use a comma as decimal separator; tabs for TSV).

A field that contains a comma, a double quote or a line break must be enclosed in double quotes

HasFieldsEnclosedInQuotes (default True) turns this on.

A double quote inside a quoted field is written as two double quotes

Handled; the field comes back with one quote.

Every record has the same number of fields

Not enforced: you check fields.Length.

A field with an embedded line break spans several physical lines, so parser.LineNumber counts physical lines while a record count must be kept separately. A defaults probe shows the state of a fresh parser.

Option Strict On
Imports System.IO
Imports Microsoft.VisualBasic.FileIO

Module Ex21
    Sub Main()
        Using parser As New TextFieldParser(New StringReader("  a , b  ,""c""" & vbLf & "next,line"))
            Console.WriteLine($"TextFieldType={parser.TextFieldType}, HasFieldsEnclosedInQuotes={parser.HasFieldsEnclosedInQuotes}, TrimWhiteSpace={parser.TrimWhiteSpace}")
            Console.WriteLine($"Delimiters is Nothing={parser.Delimiters Is Nothing}, CommentTokens count={parser.CommentTokens.Length}, FieldWidths is Nothing={parser.FieldWidths Is Nothing}")
            Console.WriteLine($"LineNumber before reading={parser.LineNumber}, ErrorLineNumber={parser.ErrorLineNumber}, ErrorLine=[{parser.ErrorLine}]")
            parser.SetDelimiters(",")
            Console.WriteLine("[" & String.Join("][", parser.ReadFields()) & "]")
            Console.WriteLine($"After one record: LineNumber={parser.LineNumber}")
            Console.WriteLine($"ReadToEnd: [{parser.ReadToEnd()}]")
            Console.WriteLine($"EndOfData={parser.EndOfData}")
        End Using
        ' Without delimiters, ReadFields cannot split the line.
        Using parser As New TextFieldParser(New StringReader("a,b"))
            Try
                Console.WriteLine("[" & String.Join("][", parser.ReadFields()) & "]")
            Catch ex As Exception
                Console.WriteLine($"No delimiters: {ex.GetType().Name}: {ex.Message}")
            End Try
        End Using
    End Sub
End Module
TextFieldType=Delimited, HasFieldsEnclosedInQuotes=True, TrimWhiteSpace=True
Delimiters is Nothing=True, CommentTokens count=0, FieldWidths is Nothing=True
LineNumber before reading=1, ErrorLineNumber=-1, ErrorLine=[]
[a][b][c]
After one record: LineNumber=2
ReadToEnd: [next,line]
EndOfData=True
No delimiters: ArgumentException: Unable to read delimited fields because Delimiters is Nothing or empty. (Parameter 'Delimiters')

The main loop of a delimited reader is therefore: create the parser over a path, a TextReader or a Stream, set TextFieldType, SetDelimiters, HasFieldsEnclosedInQuotes and TrimWhiteSpace, then Do While Not parser.EndOfData, calling ReadFields once per record. The next program feeds it a catalogue with every kind of difficult field.

Option Strict On
Imports System.IO
Imports Microsoft.VisualBasic.FileIO

Module Ex09
    Sub Main()
        ' The book list as RFC 4180 CSV: a header, a quoted field with a comma, an escaped quote and an embedded line break.
        Dim csv As String =
            "Isbn,Title,Author,Year" & vbCrLf &
            "978-0-441-17271-9,Dune,Frank Herbert,1965" & vbCrLf &
            "978-0-14-143951-8,""Emma, a Novel"",Jane Austen,1815" & vbCrLf &
            "978-0-14-243724-7,""The """"Beloved"""" Edition"",Toni Morrison,1987" & vbCrLf &
            "978-0-00-000000-0,""Two" & vbCrLf & "Lines"",Anonymous,2000" & vbCrLf

        Using reader As New StringReader(csv)
            Using parser As New TextFieldParser(reader)
                parser.TextFieldType = FieldType.Delimited
                parser.SetDelimiters(",")
                parser.HasFieldsEnclosedInQuotes = True
                parser.TrimWhiteSpace = True

                Dim header As String() = parser.ReadFields()
                Console.WriteLine($"Header: {String.Join("|", header)}")
                Do While Not parser.EndOfData
                    Dim lineBefore As Long = parser.LineNumber
                    Dim fields As String() = parser.ReadFields()
                    Dim title As String = fields(1).Replace(vbCrLf, "\n")
                    Console.WriteLine($"Line {lineBefore}: {fields.Length} fields, title=[{title}], year={CInt(fields(3))}")
                Loop
                Console.WriteLine($"After the last record: EndOfData={parser.EndOfData}, LineNumber={parser.LineNumber}")
            End Using
        End Using

        ' Other delimiters, comment lines and untrimmed whitespace.
        Dim semicolons As String =
            "# exported from the shelf" & vbLf &
            "Dune ; Frank Herbert ; 1965" & vbLf &
            "Emma;Jane Austen;1815" & vbLf
        Using parser As New TextFieldParser(New StringReader(semicolons))
            parser.SetDelimiters(";", vbTab)
            parser.CommentTokens = New String() {"#"}
            parser.TrimWhiteSpace = False
            Do While Not parser.EndOfData
                Dim fields As String() = parser.ReadFields()
                Console.WriteLine("[" & String.Join("][", fields) & "]")
            Loop
        End Using

        ' ReadLine returns the raw line; PeekChars looks ahead without consuming.
        Using parser As New TextFieldParser(New StringReader("alpha,beta" & vbLf & "gamma,delta"))
            parser.SetDelimiters(",")
            Console.WriteLine($"PeekChars(3): {parser.PeekChars(3)}")
            Console.WriteLine($"ReadLine: {parser.ReadLine()}")
            Console.WriteLine($"ReadFields: {String.Join("|", parser.ReadFields())}")
        End Using

        ' The same parser reads a file: the constructor with a path detects the encoding.
        Dim path1 As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N") & ".csv")
        Try
            File.WriteAllText(path1, csv)
            Using parser As New TextFieldParser(path1)
                parser.SetDelimiters(",")
                parser.ReadFields()
                Dim count As Integer = 0
                Do While Not parser.EndOfData
                    parser.ReadFields()
                    count += 1
                Loop
                Console.WriteLine($"Records in the file: {count}")
            End Using
        Finally
            File.Delete(path1)
        End Try
    End Sub
End Module
Header: Isbn|Title|Author|Year
Line 2: 4 fields, title=[Dune], year=1965
Line 3: 4 fields, title=[Emma, a Novel], year=1815
Line 4: 4 fields, title=[The "Beloved" Edition], year=1987
Line 5: 4 fields, title=[Two\nLines], year=2000
After the last record: EndOfData=True, LineNumber=-1
[Dune ][ Frank Herbert ][ 1965]
[Emma][Jane Austen][1815]
PeekChars(3): alp
ReadLine: alpha,beta
ReadFields: gamma|delta
Records in the file: 4

Reading the output: the quoted comma and the doubled quotes come back as ordinary text; the embedded line break stays in the field (the program escapes it for display); EndOfData is True after the last record and LineNumber is then -1; TrimWhiteSpace removes the spaces around fields when True, and the semicolon example sets it to False to show them; CommentTokens skips whole lines that start with a token, which is how # comments in exports are ignored; ReadLine returns the next raw line and PeekChars previews characters without consuming them. When the constructor receives a path, it detects the encoding from the byte order mark.

Member Notes

TextFieldType

FieldType.Delimited (default) or FieldType.FixedWidth.

SetDelimiters(...), Delimiters

One or more delimiter strings. With none set, ReadFields throws ArgumentException.

SetFieldWidths(...), FieldWidths

Widths in characters for fixed-width text; a width of zero or less on the last column means "the rest of the line".

HasFieldsEnclosedInQuotes

True by default. When True, a quote at the start of a field begins a quoted field; a quote in the middle of an unquoted field is accepted as it is.

TrimWhiteSpace

True by default; trims unquoted fields.

CommentTokens

Lines that start with one of the tokens (after leading white space) are skipped entirely.

ReadFields()

The next record as String(), or Nothing at the end of the data.

ReadLine(), ReadToEnd(), PeekChars(n)

Raw access that bypasses field splitting.

EndOfData

True when no more records exist; skips blank and comment lines when it decides.

LineNumber

The number of the next line to be read (1-based); -1 after the end.

ErrorLine, ErrorLineNumber

The text and number of the last line that could not be parsed.

Close()/Dispose()

Releases the reader; wrap the parser in Using.

Malformed lines and fixed-width text

ReadFields throws MalformedLineException when a line cannot be parsed: for delimited text that means an unterminated quote or a closing quote that is not followed by a delimiter; for fixed-width text, a line shorter than the declared widths. The exception’s LineNumber and the parser’s ErrorLine identify the problem. After the exception the parser has already advanced, so the loop continues with the next line, which makes a "log and skip" strategy possible. A quote in the middle of an unquoted field, such as Em"ma, is not an error when HasFieldsEnclosedInQuotes is True.

Option Strict On
Imports System.IO
Imports Microsoft.VisualBasic.FileIO

Module Ex10
    Sub Main()
        ' Malformed input: a quote in the middle of an unquoted field, and an unterminated quoted field.
        Dim bad As String =
            "Dune,Frank Herbert,1965" & vbLf &
            "Em""ma,Jane Austen,1815" & vbLf &
            "Ulysses,James Joyce,1922" & vbLf &
            """Hamlet,William Shakespeare,1603" & vbLf

        Using parser As New TextFieldParser(New StringReader(bad))
            parser.SetDelimiters(",")
            parser.HasFieldsEnclosedInQuotes = True
            Do While Not parser.EndOfData
                Try
                    Dim fields As String() = parser.ReadFields()
                    Console.WriteLine($"OK  : {String.Join("|", fields)}")
                Catch ex As MalformedLineException
                    Console.WriteLine($"Skip: line {ex.LineNumber} is malformed -> {parser.ErrorLine.Trim()} (ErrorLineNumber={parser.ErrorLineNumber})")
                End Try
            Loop
        End Using

        ' Without quote handling the same quotes are ordinary characters.
        Using parser As New TextFieldParser(New StringReader("Em""ma,Jane Austen,1815"))
            parser.SetDelimiters(",")
            parser.HasFieldsEnclosedInQuotes = False
            Console.WriteLine("No quote handling: " & String.Join("|", parser.ReadFields()))
        End Using

        ' Fixed width: column widths; a negative last width means "to the end of the line".
        Dim fixedWidth As String =
            "Dune      Herbert   1965" & vbLf &
            "Emma      Austen    1815" & vbLf &
            "Beloved   Morrison  1987 extra" & vbLf
        Using parser As New TextFieldParser(New StringReader(fixedWidth))
            parser.TextFieldType = FieldType.FixedWidth
            parser.SetFieldWidths(10, 10, -1)
            parser.TrimWhiteSpace = True
            Do While Not parser.EndOfData
                Console.WriteLine(String.Join("|", parser.ReadFields()))
            Loop
        End Using

        ' Setting delimiters on a fixed-width parser, or widths on a delimited one, is an error.
        Using parser As New TextFieldParser(New StringReader("a,b"))
            parser.TextFieldType = FieldType.FixedWidth
            Try
                parser.SetDelimiters(",")
                Console.WriteLine("SetDelimiters accepted")
                parser.ReadFields()
            Catch ex As Exception
                Console.WriteLine($"Wrong configuration: {ex.GetType().Name}")
            End Try
        End Using

        ' A fixed-width line that is too short.
        Using parser As New TextFieldParser(New StringReader("Dune"))
            parser.TextFieldType = FieldType.FixedWidth
            parser.SetFieldWidths(10, 10)
            Try
                Console.WriteLine("[" & String.Join("][", parser.ReadFields()) & "]")
            Catch ex As MalformedLineException
                Console.WriteLine($"Short line: {ex.GetType().Name}")
            End Try
        End Using

        ' After the stream is exhausted, ReadFields returns Nothing.
        Using parser As New TextFieldParser(New StringReader("only,line"))
            parser.SetDelimiters(",")
            parser.ReadFields()
            Console.WriteLine($"Past the end: ReadFields is Nothing = {parser.ReadFields() Is Nothing}")
        End Using
    End Sub
End Module
OK  : Dune|Frank Herbert|1965
OK  : Em"ma|Jane Austen|1815
OK  : Ulysses|James Joyce|1922
Skip: line 4 is malformed -> "Hamlet,William Shakespeare,1603 (ErrorLineNumber=4)
No quote handling: Em"ma|Jane Austen|1815
Dune|Herbert|1965
Emma|Austen|1815
Beloved|Morrison|1987 extra
SetDelimiters accepted
Wrong configuration: InvalidOperationException
Short line: MalformedLineException
Past the end: ReadFields is Nothing = True

The unterminated quote in the last line of that input hides a bigger pitfall of quoted CSV, which the next program isolates: with a stray quote on line 2 and good lines after it, the parser reports one malformed record whose ErrorLine contains every remaining line, and the loop ends because the data is exhausted. A single bad character in the middle of a file loses the rest of it, so for data from outside validate the field count and the types of each record, as in the catalogue program below, and reject the file rather than guess.

Option Strict On
Imports System.IO
Imports Microsoft.VisualBasic.FileIO

Module Ex22
    Sub Main()
        ' An unterminated quote on line 2 followed by good lines.
        Dim text As String = "a,b" & vbLf & "c,""d" & vbLf & "e,f" & vbLf & "g,h" & vbLf
        Using parser As New TextFieldParser(New StringReader(text))
            parser.SetDelimiters(",")
            Do While Not parser.EndOfData
                Try
                    Console.WriteLine("OK: " & String.Join("|", parser.ReadFields()))
                Catch ex As MalformedLineException
                    Console.WriteLine($"Malformed at line {ex.LineNumber}, ErrorLine=[{parser.ErrorLine.Replace(vbLf, "\n")}]")
                End Try
            Loop
        End Using
        ' Several resources in one Using.
        Using a As New StringReader("one"), b As New StringWriter()
            b.Write(a.ReadToEnd())
            Console.WriteLine("Two resources: " & b.ToString())
        End Using
    End Sub
End Module
OK: a|b
Malformed at line 2, ErrorLine=[c,"d\ne,f\ng,h]
Two resources: one

Setting delimiters on a parser whose TextFieldType is FixedWidth is accepted silently in this run (the property is simply unused), and the later ReadFields fails with InvalidOperationException, because the parser checks the configuration only when it reads.

Writing CSV

Nothing in the libraries writes CSV, so the program does it with StreamWriter. The rule is short: quote a field if it contains the delimiter, a double quote, or a line break; double any quote inside it. Use Invariant formatting for numbers and dates so that the file does not depend on the machine that wrote it, write line endings as CRLF if a Windows spreadsheet will open it, and pick the encoding on purpose (UTF-8 with a BOM for Excel on Windows, otherwise without).

Option Strict On
Imports System.IO
Imports System.Text
Imports Microsoft.VisualBasic.FileIO

Module Ex11
    Private ReadOnly SpecialCharacters As Char() = {","c, """"c, ControlChars.Cr, ControlChars.Lf}

    ' RFC 4180: quote a field that contains a comma, a quote or a line break, and double the quotes inside it.
    Private Function QuoteField(value As String) As String
        If value.IndexOfAny(SpecialCharacters) < 0 Then
            Return value
        End If
        Return """" & value.Replace("""", """""") & """"
    End Function

    Private Function JoinRecord(fields As IEnumerable(Of String)) As String
        Return String.Join(",", fields.Select(AddressOf QuoteField))
    End Function

    Sub Main()
        Dim books As New List(Of String())() From {
            New String() {"978-0-441-17271-9", "Dune", "Frank Herbert", "1965"},
            New String() {"978-0-14-143951-8", "Emma, a Novel", "Jane Austen", "1815"},
            New String() {"978-0-14-243724-7", "The ""Beloved"" Edition", "Toni Morrison", "1987"},
            New String() {"978-0-00-000000-0", "Two" & vbLf & "Lines", "Anonymous", "2000"}
        }

        Dim target As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N") & ".csv")
        Try
            ' Write with an explicit encoding and RFC 4180 line endings (CRLF).
            Using writer As New StreamWriter(target, False, New UTF8Encoding(False)) With {.NewLine = vbCrLf}
                writer.WriteLine(JoinRecord({"Isbn", "Title", "Author", "Year"}))
                For Each book As String() In books
                    writer.WriteLine(JoinRecord(book))
                Next
            End Using
            Console.WriteLine(File.ReadAllText(target).Replace(vbCrLf, "<CRLF>" & vbLf))

            ' Read it back with TextFieldParser and compare with the original data.
            Dim readBack As New List(Of String())()
            Using parser As New TextFieldParser(target, New UTF8Encoding(False))
                parser.SetDelimiters(",")
                parser.HasFieldsEnclosedInQuotes = True
                parser.TrimWhiteSpace = False
                parser.ReadFields() ' header
                Do While Not parser.EndOfData
                    readBack.Add(parser.ReadFields())
                Loop
            End Using
            Dim identical As Boolean = readBack.Count = books.Count
            For i As Integer = 0 To books.Count - 1
                identical = identical AndAlso books(i).SequenceEqual(readBack(i))
            Next
            Console.WriteLine($"Round trip identical: {identical}")
        Finally
            File.Delete(target)
        End Try

        ' Formula injection: a spreadsheet treats a cell that starts with = + - or @ as a formula.
        Dim risky As String = "=HYPERLINK(""http://example.invalid"",""click"")"
        Dim defused As String = If("=+-@".Contains(risky(0)), "'" & risky, risky)
        Console.WriteLine($"Defused for spreadsheets: {QuoteField(defused)}")
    End Sub
End Module
Isbn,Title,Author,Year<CRLF>
978-0-441-17271-9,Dune,Frank Herbert,1965<CRLF>
978-0-14-143951-8,"Emma, a Novel",Jane Austen,1815<CRLF>
978-0-14-243724-7,"The ""Beloved"" Edition",Toni Morrison,1987<CRLF>
978-0-00-000000-0,"Two
Lines",Anonymous,2000<CRLF>

Round trip identical: True
Defused for spreadsheets: "'=HYPERLINK(""http://example.invalid"",""click"")"

The last two lines are a security note rather than part of the format: a spreadsheet interprets a cell that starts with =, +, - or @ as a formula, so CSV that contains user-supplied text can run formulas when opened (CSV injection). Prefixing such cells with an apostrophe, as the program does, makes the spreadsheet treat them as text; it changes the data, so apply it only to files meant for spreadsheets.

A catalogue round trip with validation

The shelf catalogue as a small, complete program: save the books atomically, load them with TextFieldParser, validate every record, and keep a list of rejected lines with the reason instead of failing the whole load.

Option Strict On
Imports System.Globalization
Imports System.IO
Imports System.Text
Imports Microsoft.VisualBasic.FileIO

Module Ex18
    Public Class Book
        Public Sub New(isbn As String, title As String, author As String, year As Integer, price As Decimal)
            Me.Isbn = isbn
            Me.Title = title
            Me.Author = author
            Me.Year = year
            Me.Price = price
        End Sub

        Public ReadOnly Property Isbn As String
        Public ReadOnly Property Title As String
        Public ReadOnly Property Author As String
        Public ReadOnly Property Year As Integer
        Public ReadOnly Property Price As Decimal
    End Class

    Private Function Quote(value As String) As String
        Return If(value.IndexOfAny({","c, """"c, ControlChars.Cr, ControlChars.Lf}) >= 0, """" & value.Replace("""", """""") & """", value)
    End Function

    ' Write to a temporary file and move it over the target, so a crash never leaves a half-written catalog.
    Sub Save(books As IEnumerable(Of Book), target As String)
        Dim temp As String = target & ".tmp"
        Using writer As New StreamWriter(temp, False, New UTF8Encoding(False)) With {.NewLine = vbCrLf}
            writer.WriteLine("Isbn,Title,Author,Year,Price")
            For Each b As Book In books
                writer.WriteLine(String.Join(",", {Quote(b.Isbn), Quote(b.Title), Quote(b.Author),
                    b.Year.ToString(CultureInfo.InvariantCulture), b.Price.ToString(CultureInfo.InvariantCulture)}))
            Next
        End Using
        File.Move(temp, target, overwrite:=True)
    End Sub

    Function Load(source As String, ByRef rejected As List(Of String)) As List(Of Book)
        Dim result As New List(Of Book)()
        rejected = New List(Of String)()
        Using parser As New TextFieldParser(source, New UTF8Encoding(False))
            parser.SetDelimiters(",")
            parser.HasFieldsEnclosedInQuotes = True
            parser.ReadFields() ' header
            Do While Not parser.EndOfData
                Dim startLine As Long = parser.LineNumber
                Try
                    Dim f As String() = parser.ReadFields()
                    Dim year As Integer
                    Dim price As Decimal
                    If f.Length <> 5 Then
                        rejected.Add($"line {startLine}: expected 5 fields, found {f.Length}")
                    ElseIf Not Integer.TryParse(f(3), NumberStyles.None, CultureInfo.InvariantCulture, year) Then
                        rejected.Add($"line {startLine}: year '{f(3)}' is not a number")
                    ElseIf Not Decimal.TryParse(f(4), NumberStyles.Number, CultureInfo.InvariantCulture, price) Then
                        rejected.Add($"line {startLine}: price '{f(4)}' is not a number")
                    Else
                        result.Add(New Book(f(0), f(1), f(2), year, price))
                    End If
                Catch ex As MalformedLineException
                    rejected.Add($"line {ex.LineNumber}: malformed ({ex.GetType().Name})")
                End Try
            Loop
        End Using
        Return result
    End Function

    Sub Main()
        Dim target As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N") & ".csv")
        Try
            Save({New Book("978-0-441-17271-9", "Dune", "Frank Herbert", 1965, 9.99D),
                  New Book("978-0-14-143951-8", "Emma, a Novel", "Jane Austen", 1815, 7.5D),
                  New Book("978-0-14-243724-7", "Beloved", "Toni Morrison", 1987, 12D)}, target)
            ' A hand-edited line with a bad year and one with a missing field.
            File.AppendAllText(target, "978-0-00-000000-1,Broken,Someone,19x5,5" & vbCrLf & "978-0-00-000000-2,Short,Author" & vbCrLf)

            Dim rejected As List(Of String) = Nothing
            Dim books As List(Of Book) = Load(target, rejected)
            For Each b As Book In books
                Console.WriteLine($"{b.Isbn}  {b.Title,-14} {b.Author,-14} {b.Year}  {b.Price.ToString("0.00", CultureInfo.InvariantCulture)}")
            Next
            Console.WriteLine($"Loaded {books.Count}, rejected {rejected.Count}")
            For Each reason As String In rejected
                Console.WriteLine("  " & reason)
            Next
        Finally
            File.Delete(target)
            File.Delete(target & ".tmp")
        End Try
    End Sub
End Module
978-0-441-17271-9  Dune           Frank Herbert  1965  9.99
978-0-14-143951-8  Emma, a Novel  Jane Austen    1815  7.50
978-0-14-243724-7  Beloved        Toni Morrison  1987  12.00
Loaded 3, rejected 2
  line 5: year '19x5' is not a number
  line 6: expected 5 fields, found 3

The loader uses Integer.TryParse with NumberStyles.None and the invariant culture, so a hand-edited 19x5 is a rejected line and not an exception, and the price is parsed with CultureInfo.InvariantCulture to match the writer. The writer creates a .tmp file first and uses File.Move(..., overwrite:=True), so the old catalogue stays intact until the new one is complete.

Binary files

Binary formats are sequences of bytes with a layout you define. BinaryWriter and BinaryReader read and write the primitive types in little-endian order, strings with a length prefix, and Decimal as 16 bytes; they are a fine choice for private formats and a poor choice for anything another system must read, where a documented format (JSON, protobuf, a database) is better. BitConverter and System.Buffers.Binary.BinaryPrimitives convert between numbers and byte arrays, the latter with an explicit byte order.

Option Strict On
Imports System.IO
Imports System.Text

Module Ex12
    Sub Main()
        Dim root As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N"))
        Directory.CreateDirectory(root)
        Try
            Dim target As String = Path.Combine(root, "loans.dat")

            ' BinaryWriter/BinaryReader: primitives in little-endian order, strings prefixed with a 7-bit encoded length.
            Using writer As New BinaryWriter(File.Create(target), Encoding.UTF8)
                writer.Write(&H4C4F414E)        ' magic number
                writer.Write(CShort(1))         ' format version
                writer.Write("Dune")
                writer.Write(14.5D)             ' fine in Decimal
                writer.Write(New Date(2026, 1, 15).Ticks)
                writer.Write(True)
            End Using
            Dim bytes As Byte() = File.ReadAllBytes(target)
            Console.WriteLine($"{bytes.Length} bytes: {Convert.ToHexString(bytes)}")

            Using reader As New BinaryReader(File.OpenRead(target), Encoding.UTF8)
                Dim magic As Integer = reader.ReadInt32()
                Dim version As Short = reader.ReadInt16()
                Dim title As String = reader.ReadString()
                Dim fine As Decimal = reader.ReadDecimal()
                Dim ticks As Long = reader.ReadInt64()
                Dim flag As Boolean = reader.ReadBoolean()
                Console.WriteLine($"magic={magic:X}, version={version}, title={title}, fine={fine}, date={New Date(ticks):yyyy-MM-dd}, flag={flag}")
                Console.WriteLine($"At end: {reader.BaseStream.Position = reader.BaseStream.Length}")
                Try
                    reader.ReadInt32()
                Catch ex As EndOfStreamException
                    Console.WriteLine("Reading past the end: EndOfStreamException")
                End Try
            End Using

            ' Byte order is explicit with BitConverter and BinaryPrimitives.
            Dim number As Integer = &H01020304
            Console.WriteLine($"IsLittleEndian: {BitConverter.IsLittleEndian}")
            Console.WriteLine($"BitConverter:  {Convert.ToHexString(BitConverter.GetBytes(number))}")
            Dim big(3) As Byte
            System.Buffers.Binary.BinaryPrimitives.WriteInt32BigEndian(big, number)
            Console.WriteLine($"Big endian:    {Convert.ToHexString(big)}")

            ' Random access: patch one value in place.
            Using stream As New FileStream(target, FileMode.Open, FileAccess.ReadWrite)
                stream.Position = 4
                Dim versionBytes(1) As Byte
                stream.ReadExactly(versionBytes, 0, 2)
                Console.WriteLine($"Version before: {BitConverter.ToInt16(versionBytes, 0)}")
                stream.Position = 4
                stream.Write(BitConverter.GetBytes(CShort(2)), 0, 2)
            End Using
            Console.WriteLine($"Version after:  {BitConverter.ToInt16(File.ReadAllBytes(target), 4)}")

            Console.WriteLine($"Header magic as text: {Encoding.ASCII.GetString(File.ReadAllBytes(target), 0, 4)}")
        Finally
            Directory.Delete(root, recursive:=True)
        End Try
    End Sub
End Module
36 bytes: 4E414F4C01000444756E65910000000000000000000000000001000080C006C953DE0801
magic=4C4F414E, version=1, title=Dune, fine=14.5, date=2026-01-15, flag=True
At end: True
Reading past the end: EndOfStreamException
IsLittleEndian: True
BitConverter:  04030201
Big endian:    01020304
Version before: 1
Version after:  2
Header magic as text: NAOL

Stream.Read(buffer, offset, count) may return fewer bytes than requested, even when more data follows. Code that ignores the return value reads garbage; the analyzer CA2022 reports it as an error under TreatWarningsAsErrors, which is how this program came to use ReadExactly (.NET 7), the method that either fills the buffer or throws EndOfStreamException. The magic number in the program is stored little-endian, so it shows in the file as NAOL, not LOAN.

Streams over memory, compression and safe updates

Everything above works on any Stream. A MemoryStream turns a byte array into a stream and back; GZipStream, DeflateStream, BrotliStream and ZipArchive wrap another stream; StringReader and StringWriter give the text API without files, which is what unit tests use to feed a parser. A chain of wrappers is written inside out with nested Using blocks.

Option Strict On
Imports System.IO
Imports System.IO.Compression
Imports System.Text

Module Ex14
    Sub Main()
        ' Streams compose: a StreamWriter over a GZipStream over a MemoryStream.
        Dim compressed As Byte()
        Dim text As String = String.Concat(Enumerable.Repeat("Dune;Frank Herbert;1965" & vbLf, 200))
        Using memory As New MemoryStream()
            Using gzip As New GZipStream(memory, CompressionLevel.Optimal, leaveOpen:=True)
                Using writer As New StreamWriter(gzip, New UTF8Encoding(False))
                    writer.Write(text)
                End Using
            End Using
            compressed = memory.ToArray()
        End Using
        Console.WriteLine($"Original {Encoding.UTF8.GetByteCount(text)} bytes, gzip {If(compressed.Length < 200, "under 200", "200 or more")} bytes")
        Using source As New MemoryStream(compressed)
            Using gzip As New GZipStream(source, CompressionMode.Decompress)
                Using reader As New StreamReader(gzip, Encoding.UTF8)
                    Console.WriteLine($"Decompressed equal: {reader.ReadToEnd() = text}")
                End Using
            End Using
        End Using

        ' StringReader and StringWriter give text APIs without files, which is what tests need.
        Using sw As New StringWriter()
            sw.WriteLine("line 1")
            sw.Write("line 2")
            Dim lines As String() = sw.ToString().Split(New String() {Environment.NewLine}, StringSplitOptions.None)
            Console.WriteLine($"StringWriter produced {lines.Length} lines")
        End Using

        ' CopyTo moves bytes between any two streams.
        Using input As New MemoryStream(Encoding.ASCII.GetBytes("copy me"))
            Using output As New MemoryStream()
                input.CopyTo(output)
                Console.WriteLine($"CopyTo copied {output.Length} bytes; input position {input.Position}")
            End Using
        End Using

        ' The safe update pattern: write a temporary file, then replace the target in one step.
        Dim root As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N"))
        Directory.CreateDirectory(root)
        Try
            Dim target As String = Path.Combine(root, "catalog.txt")
            File.WriteAllText(target, "version 1")
            Dim temp As String = target & ".tmp"
            File.WriteAllText(temp, "version 2")
            File.Move(temp, target, overwrite:=True)
            Console.WriteLine($"Atomic replace: {File.ReadAllText(target)}, temp left behind: {File.Exists(temp)}")

            ' Flush(True) asks the operating system to write to the device, not just to its cache.
            Using stream As New FileStream(target, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)
                stream.Write(Encoding.ASCII.GetBytes("durable"))
                stream.Flush(flushToDisk:=True)
            End Using
            Console.WriteLine(File.ReadAllText(target))

            ' Never build a path from untrusted input without checking where it lands.
            Dim requested As String = "../../etc/passwd"
            Dim resolved As String = Path.GetFullPath(Path.Combine(root, requested))
            Dim inside As Boolean = resolved.StartsWith(Path.GetFullPath(root) & Path.DirectorySeparatorChar, StringComparison.Ordinal)
            Console.WriteLine($"Request '{requested}' stays inside the root: {inside}")

            ' Symbolic links and Unix permissions are visible too.
            If Not OperatingSystem.IsWindows() Then
                File.SetUnixFileMode(target, UnixFileMode.UserRead Or UnixFileMode.UserWrite)
                Console.WriteLine($"UnixFileMode: {File.GetUnixFileMode(target)}")
            End If
        Finally
            Directory.Delete(root, recursive:=True)
        End Try
    End Sub
End Module
Original 4800 bytes, gzip under 200 bytes
Decompressed equal: True
StringWriter produced 2 lines
CopyTo copied 7 bytes; input position 7
Atomic replace: version 2, temp left behind: False
durable
Request '../../etc/passwd' stays inside the root: False
UnixFileMode: UserWrite, UserRead

Four habits from the example. Replace, do not rewrite: write the new content to a temporary file in the same folder and move it over the target, so that readers see either the whole old file or the whole new one; a crash while writing leaves the temporary file, not a truncated catalogue. Flush to the device when it matters: Flush empties the .NET buffer, Flush(True) also asks the operating system to write to the disk. Check where a path lands: Path.GetFullPath(Path.Combine(root, requested)) resolves .., and testing that the result starts with the root plus a separator rejects traversal such as ../../etc/passwd; compare with StringComparison.Ordinal (or OrdinalIgnoreCase on a case-insensitive file system). Set permissions on the file, not on the folder, with File.SetUnixFileMode on Linux and macOS (.NET 7) or access control lists on Windows; a file created by File.WriteAllText gets the process’s default permissions, which are often more open than a file of secrets should have.

Legacy numbered files: FileOpen, PrintLine, Input and Put

Visual Basic 6 and earlier opened files with a number: Open "x.txt" For Output As #1, then Print #1, ..., Input #1, ..., Line Input #1, ..., Put #1, Get #1 and Close #1. The Microsoft.VisualBasic runtime keeps a compatible implementation as ordinary functions (FileOpen, PrintLine, Print, Write, WriteLine, Input, LineInput, EOF, LOF, Loc, Seek, FilePut, FileGet, FileClose, FreeFile, Reset) so that migrated code compiles. Recognise them; do not write them in new code. The program below is a tested recognition aid: it shows what the functions write and what goes wrong.

Option Strict On
Imports System.IO

Module Ex13
    ' Recognition only: the numbered-file model inherited from BASIC, still implemented by Microsoft.VisualBasic.
    Sub Main()
        Dim target As String = Path.Combine(Path.GetTempPath(), "shelf-" & Guid.NewGuid().ToString("N") & ".txt")
        Try
            Dim number As Integer = FreeFile()
            FileOpen(number, target, OpenMode.Output)
            PrintLine(number, "Dune", 1965)           ' Print # : plain text
            Write(number, "Emma", 1815, True)         ' Write # : comma-delimited, strings quoted, no line end
            Print(number, vbCrLf)
            WriteLine(number, "Ulysses", 1922, False) ' Write # plus the platform line end
            Console.WriteLine($"LOF while open: {LOF(number)} bytes, Loc={Loc(number)}")
            FileClose(number)
            Console.WriteLine(File.ReadAllText(target).Replace(vbCrLf, "<CRLF>" & vbLf))

            ' Input # reads typed fields written by Write #. It ends a record at CR or CRLF, not at a lone LF.
            Dim second As Integer = FreeFile()
            FileOpen(second, target, OpenMode.Input)
            Dim skip As String = LineInput(second)
            Dim title As String = Nothing
            Dim year As Integer
            Dim flag As Boolean
            Input(second, title)
            Input(second, year)
            Input(second, flag)
            Console.WriteLine($"Skipped [{skip.TrimEnd()}], then title={title}, year={year}, flag={flag}")
            Console.WriteLine($"EOF: {EOF(second)}")
            Try
                Input(second, title)
                Input(second, year)
                Input(second, flag)
            Catch ex As InvalidCastException
                Console.WriteLine("The record that ended with a lone LF did not terminate: InvalidCastException")
            End Try
            FileClose(second)

            ' Random access with Put and Get uses fixed-size records.
            Dim recordFile As String = target & ".dat"
            Dim third As Integer = FreeFile()
            FileOpen(third, recordFile, OpenMode.Random, OpenAccess.ReadWrite, OpenShare.Default, 4)
            FilePut(third, 100, 1)
            FilePut(third, 200, 2)
            FilePut(third, 300, 3)
            Dim value As Integer
            FileGet(third, value, 2)
            Console.WriteLine($"Record 2 = {value}, file length = {LOF(third)} bytes")
            FileClose(third)
            File.Delete(recordFile)

            ' Using a closed or unknown file number is a runtime error.
            Try
                Dim junk As String = LineInput(7)
            Catch ex As Exception
                Console.WriteLine($"Unopened file number: {ex.GetType().Name}")
            End Try
            ' Forgetting FileClose leaves the file locked for others.
            Dim fourth As Integer = FreeFile()
            FileOpen(fourth, target, OpenMode.Output, OpenAccess.Write, OpenShare.LockReadWrite)
            Try
                File.ReadAllText(target)
            Catch ex As IOException
                Console.WriteLine($"Open numbered file blocks other readers: {ex.GetType().Name}")
            End Try
            Reset() ' closes every file opened with FileOpen
        Finally
            File.Delete(target)
        End Try
    End Sub
End Module
LOF while open: 65 bytes, Loc=1
Dune           1965
"Emma",1815,#TRUE#,<CRLF>
"Ulysses",1922,#FALSE#

Skipped [Dune           1965], then title=Emma, year=1815, flag=True
EOF: False
The record that ended with a lone LF did not terminate: InvalidCastException
Record 2 = 200, file length = 12 bytes
Unopened file number: IOException
Open numbered file blocks other readers: IOException

Notice the traps. PrintLine and WriteLine end lines with the platform’s line break (a lone LF on Linux), while Input ends a record at CR or CRLF and treats a lone LF as part of the field, so a file written on Linux by these functions cannot always be read back by Input; Write emits fields separated by commas with strings quoted and booleans as #TRUE# and #FALSE#, and leaves a trailing comma unless WriteLine ends the record; PrintLine lays several items out in 14-column zones and gives a number a leading sign space and a trailing space, as the first line of the output shows; a file number that was not opened raises IOException; a file opened and not closed stays locked (Reset closes all). The mapping to the modern API is direct.

Legacy Modern

Open ... For Output/Append As #n with Print #n

StreamWriter in Using

Open ... For Input As #n with Line Input #n and EOF(n)

StreamReader in Using, ReadLine, EndOfStream

Input #n, a, b (typed fields)

TextFieldParser, or Split plus TryParse

Write #n (quoted comma output)

A CSV writer as above

Open ... For Binary, Get, Put, Seek

FileStream, BinaryReader, BinaryWriter

Open ... For Random (fixed records)

FileStream with Seek(record * size), or a database

FileLen, Dir, Kill, MkDir, RmDir, FileCopy, Name ... As ...

FileInfo.Length, Directory.EnumerateFiles, File.Delete, Directory.CreateDirectory, Directory.Delete, File.Copy, File.Move

FileSystemObject (COM), Scripting.FileSystemObject

System.IO; see The VB Family: VB6, VBA and VBScript for VBScript code

Differences from C#

Topic Visual Basic C#

Namespace imports

System.IO is not an implicit import; Microsoft.VisualBasic is

System.IO is in the default ImplicitUsings

Disposal

Using x As New StreamReader(p) …​ End Using; several resources Using a As New A(), b As New B()

using var x = new StreamReader(p); (declaration form) or a block

Async main

No Async Sub Main; bridge with GetAwaiter().GetResult()

async Task Main

Async streams

No Await For Each; drive IAsyncEnumerator(Of T) by hand

await foreach

Await in Catch or Finally

Not allowed (BC36943)

Allowed since C# 6

CSV

TextFieldParser is in the language’s own namespace

Same class, but Microsoft.VisualBasic.FileIO has to be referenced with using

My.Computer.FileSystem and numbered files

Available (with limits above)

Absent

String literals for paths

No escape sequences, so "C:\data" is exact; quotes doubled ""

Backslashes need @"" or \\; raw string literals exist

Name case

file and File are the same identifier: a local named file hides the type File

Case-sensitive

Where the books are outdated

Two points in the books' file chapters need correcting for .NET 10. The serial-port class SerialPort is presented as built in; since .NET 5 it lives in the System.IO.Ports NuGet package, which the project must reference (and a serial port is a hardware feature that containers and many servers lack, so tests should not depend on one). And the beginner example that calls File.Create without disposing the returned stream is the leak demonstrated above; the correct form is the Using block shown there. The numbered-file model (FileOpen, PrintLine, Input and the others) that the books describe is kept for compatibility, with the limits shown in its section, and the file code in books written for the .NET Framework has three more current-platform differences that this page covers where they matter: Encoding.Default is UTF-8 on .NET, not the machine’s ANSI code page; My.Computer is available only in project types that define it; and the UIOption and RecycleOption overloads work only on Windows.

Errors the compiler reports

Message Cause and fix

BC30451: 'File' is not declared

Imports System.IO is missing; the same for Path, Directory and Stream. BC30002: Type 'TextFieldParser' is not defined means Imports Microsoft.VisualBasic.FileIO is missing.

BC30561: 'SearchOption' is ambiguous

System.IO and Microsoft.VisualBasic.FileIO both define it; qualify the name.

BC30456: 'Computer' is not a member of 'ShelfDocs.My'

My.Computer needs a project type that defines it (Windows Forms); use FileSystem directly.

BC36943: 'Await' cannot be used inside a 'Catch' statement, a 'Finally' statement, or a 'SyncLock' statement

Move the Await after the Try block.

CA2022 (error under TreatWarningsAsErrors)

Stream.Read result ignored; use ReadExactly, or check the return value.

Option Strict On

Module Ex19
    Sub Main()
        Dim text As String = File.ReadAllText("shelf.txt")
        Dim parser As New TextFieldParser("shelf.csv")
        Console.WriteLine(Path.Combine("a", "b") & text)
    End Sub
End Module
Ex19.vb(5,30): error BC30451: 'File' is not declared. It may be inaccessible due to its protection level.
Ex19.vb(6,27): error BC30002: Type 'TextFieldParser' is not defined.
Ex19.vb(7,27): error BC30451: 'Path' is not declared. It may be inaccessible due to its protection level.

Pitfalls checklist

  • Dispose every stream; File.Create and File.Open return open streams. Prefer the one-call File helpers for small files.

  • Build paths with Path.Combine or Path.Join; never with & and a literal separator. A rooted second argument makes Combine discard the first.

  • Resolve and check any path that includes untrusted input before opening it.

  • Do not rely on enumeration order; sort when the output must be stable.

  • State the encoding for any file another program reads. Use UTF-8 without a BOM unless a spreadsheet on Windows will open the file.

  • A relative path depends on the current directory, not on the executable’s directory.

  • Replace files by writing a temporary file and moving it over the target.

  • FileSystemWatcher events are hints: they can repeat, overflow and arrive before the file is complete.

  • TextFieldParser reads but does not write; an unterminated quote can swallow the rest of the file, so validate field counts and types.

  • UIOption and RecycleOption overloads work only on Windows; use the plain overloads in portable code.

  • Numbered-file functions are for migrated code; Input does not treat a lone LF as a record end.

  • Await is not allowed in Catch or Finally; Async Sub Main and Await For Each do not exist in Visual Basic.

  • A CSV exported for spreadsheets needs formula-injection protection when it includes user text.

  • Tests should not touch the real file system when StringReader, StringWriter or MemoryStream will do.