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 |
|---|---|---|
|
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 |
|
An object per file or folder with |
You query the same entry several times, or filter a listing by size or date |
|
Bytes with position, seeking, sharing and a chosen access mode |
Random access, binary formats, locking, large files |
|
Text over any stream, with an encoding |
Line-by-line processing and large text files |
|
Primitive values over any stream, in a fixed byte order |
Your own binary formats |
|
The same APIs over memory |
Tests, building a payload before sending it |
|
A stream that compresses or decompresses another stream |
Archives, network payloads |
|
Events when files in a folder change |
Reacting to drops into an inbox folder |
|
Recursive copy and delete, wildcards, text helpers, optional Windows UI |
Migrating code that used |
|
Delimited and fixed-width text with quote handling |
Reading CSV without a third-party package |
|
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
Source: .NET API: File Class
The exceptions are part of the contract, and which one you get tells you what to do.
| Exception | Typical cause and response |
|---|---|
|
The file does not exist. |
|
A folder in the path does not exist. |
|
No permission, a read-only file, or (as in the example) a path that names a directory where a file was expected. |
|
The file is in use, the target already exists ( |
|
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. |
|
The path is empty, contains an invalid character, or has an invalid format (a colon in the wrong place on Windows). |
|
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
Source: .NET API: Path Class
| Member | Behaviour worth knowing |
|---|---|
|
Joins with the platform separator, but if |
|
Joins and never discards: a rooted |
|
Resolves |
|
The reverse: how to reach |
|
String operations. The extension includes the leading dot; |
|
The temporary folder, a created empty file with a unique name, and a random name without creating anything. |
|
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. |
|
|
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
Source: .NET API: FileStream Class
The mode, access and sharing parameters of the FileStream constructor decide what the open does.
FileMode |
Behaviour |
|---|---|
|
Creates a new file; throws |
|
Creates the file, or truncates it to zero length if it exists. |
|
Opens an existing file; |
|
Opens the file or creates an empty one. |
|
Opens an existing file and empties it. |
|
Opens or creates the file and positions at the end; allowed only with |
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
Source: .NET API: File.Create Method
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)
Source: .NET: Character encoding in .NET
| Encoding | Notes | Bytes for the text above |
|---|---|---|
|
UTF-8 without a BOM; the default of |
21 |
|
UTF-8 that writes the three-byte BOM |
24 |
|
UTF-16 little-endian with the BOM |
42 |
|
UTF-16 big-endian with the BOM |
42 |
|
ISO-8859-1, one byte per character for the first 256 code points. Available since .NET 5; |
20 |
|
Seven bits; every character above 127 is replaced with |
20 |
Windows code pages ( |
Not built in on .NET Core: register |
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
Source: .NET: Asynchronous file I/O
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 |
|
The last record may or may not end with a line break |
Both work; |
An optional header record |
The parser does not treat the first record specially: read it with |
Fields are separated by commas |
Any delimiter works with |
A field that contains a comma, a double quote or a line break must be enclosed in double quotes |
|
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 |
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 |
|---|---|
|
|
|
One or more delimiter strings. With none set, |
|
Widths in characters for fixed-width text; a width of zero or less on the last column means "the rest of the line". |
|
|
|
|
|
Lines that start with one of the tokens (after leading white space) are skipped entirely. |
|
The next record as |
|
Raw access that bypasses field splitting. |
|
|
|
The number of the next line to be read (1-based); |
|
The text and number of the last line that could not be parsed. |
|
Releases the reader; wrap the parser in |
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
Source: .NET API: TextFieldParser Class
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
Source: .NET API: BinaryWriter Class
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
Source: .NET API: GZipStream Class
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 |
|---|---|
|
|
|
|
|
|
|
A CSV writer as above |
|
|
|
|
|
|
|
|
Differences from C#
| Topic | Visual Basic | C# |
|---|---|---|
Namespace imports |
|
|
Disposal |
|
|
Async main |
No |
|
Async streams |
No |
|
|
Not allowed (BC36943) |
Allowed since C# 6 |
CSV |
|
Same class, but |
|
Available (with limits above) |
Absent |
String literals for paths |
No escape sequences, so |
Backslashes need |
Name case |
|
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 |
|---|---|
|
|
|
|
|
|
|
Move the |
|
|
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.
Source: .NET API: System.IO Namespace
Pitfalls checklist
-
Dispose every stream;
File.CreateandFile.Openreturn open streams. Prefer the one-callFilehelpers for small files. -
Build paths with
Path.CombineorPath.Join; never with&and a literal separator. A rooted second argument makesCombinediscard 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.
-
FileSystemWatcherevents are hints: they can repeat, overflow and arrive before the file is complete. -
TextFieldParserreads but does not write; an unterminated quote can swallow the rest of the file, so validate field counts and types. -
UIOptionandRecycleOptionoverloads work only on Windows; use the plain overloads in portable code. -
Numbered-file functions are for migrated code;
Inputdoes not treat a lone LF as a record end. -
Awaitis not allowed inCatchorFinally;Async Sub MainandAwait For Eachdo 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,StringWriterorMemoryStreamwill do.
References
-
FileStream Class, StreamReader Class, StreamWriter Class, MemoryStream Class
-
TextFieldParser Class and Parsing text files with the TextFieldParser object
-
Microsoft.VisualBasic.FileIO.FileSystem Class and Drives, directories, and files (Visual Basic)
-
RFC 4180: Common Format and MIME Type for Comma-Separated Values (CSV Files)