mirror of
https://github.com/markjprice/cs11dotnet7.git
synced 2025-12-06 05:32:03 +01:00
676 lines
15 KiB
Plaintext
676 lines
15 KiB
Plaintext
#!markdown
|
|
|
|
# Chapter 9 - Working with Files, Streams, and Serialization
|
|
- Managing the filesystem
|
|
- Reading and writing with streams
|
|
- Encoding and decoding text
|
|
- Serializing object graphs
|
|
- Controlling JSON processing
|
|
|
|
#!markdown
|
|
|
|
# Managing the filesystem
|
|
|
|
Your applications will often need to perform input and output operations with files and
|
|
directories in different environments. The `System` and `System.IO` namespaces contain classes
|
|
for this purpose.
|
|
|
|
#!markdown
|
|
|
|
## Handling cross-platform environments and filesystems
|
|
|
|
#!csharp
|
|
|
|
using static System.Console;
|
|
using static System.IO.Directory;
|
|
using static System.IO.Path;
|
|
using static System.Environment;
|
|
|
|
#!csharp
|
|
|
|
using System.IO;
|
|
|
|
#!csharp
|
|
|
|
WriteLine("{0,-33} {1}", arg0: "Path.PathSeparator",
|
|
arg1: PathSeparator);
|
|
WriteLine("{0,-33} {1}", arg0: "Path.DirectorySeparatorChar",
|
|
arg1: DirectorySeparatorChar);
|
|
WriteLine("{0,-33} {1}", arg0: "Directory.GetCurrentDirectory()",
|
|
arg1: GetCurrentDirectory());
|
|
WriteLine("{0,-33} {1}", arg0: "Environment.CurrentDirectory",
|
|
arg1: CurrentDirectory);
|
|
WriteLine("{0,-33} {1}", arg0: "Environment.SystemDirectory",
|
|
arg1: SystemDirectory);
|
|
WriteLine("{0,-33} {1}", arg0: "Path.GetTempPath()",
|
|
arg1: GetTempPath());
|
|
|
|
WriteLine("GetFolderPath(SpecialFolder");
|
|
WriteLine("{0,-33} {1}", arg0: " .System)",
|
|
arg1: GetFolderPath(SpecialFolder.System));
|
|
WriteLine("{0,-33} {1}", arg0: " .ApplicationData)",
|
|
arg1: GetFolderPath(SpecialFolder.ApplicationData));
|
|
WriteLine("{0,-33} {1}", arg0: " .MyDocuments)",
|
|
arg1: GetFolderPath(SpecialFolder.MyDocuments));
|
|
WriteLine("{0,-33} {1}", arg0: " .Personal)",
|
|
arg1: GetFolderPath(SpecialFolder.Personal));
|
|
|
|
#!markdown
|
|
|
|
## Managing drives
|
|
|
|
#!csharp
|
|
|
|
WriteLine("{0,-30} | {1,-10} | {2,-7} | {3,18} | {4,18}",
|
|
"NAME", "TYPE", "FORMAT", "SIZE (BYTES)", "FREE SPACE");
|
|
|
|
foreach (DriveInfo drive in DriveInfo.GetDrives())
|
|
{
|
|
if (drive.IsReady)
|
|
{
|
|
WriteLine(
|
|
"{0,-30} | {1,-10} | {2,-7} | {3,18:N0} | {4,18:N0}",
|
|
drive.Name, drive.DriveType, drive.DriveFormat,
|
|
drive.TotalSize, drive.AvailableFreeSpace);
|
|
}
|
|
else
|
|
{
|
|
WriteLine("{0,-30} | {1,-10}", drive.Name, drive.DriveType);
|
|
}
|
|
}
|
|
|
|
#!markdown
|
|
|
|
## Managing directories
|
|
|
|
#!csharp
|
|
|
|
// define a directory path for a new folder
|
|
// starting in the user's folder
|
|
string newFolder = Combine(
|
|
GetFolderPath(SpecialFolder.Personal),
|
|
"Code", "Chapter09", "NewFolder");
|
|
|
|
WriteLine($"Working with: {newFolder}");
|
|
|
|
// check if it exists
|
|
WriteLine($"Does it exist? {Exists(newFolder)}");
|
|
|
|
// create directory
|
|
WriteLine("Creating it...");
|
|
CreateDirectory(newFolder);
|
|
WriteLine($"Does it exist? {Exists(newFolder)}");
|
|
Write("Confirm the directory exists, and then execute the next notebook cell.");
|
|
|
|
#!csharp
|
|
|
|
// delete directory
|
|
WriteLine("Deleting it...");
|
|
Delete(newFolder, recursive: true);
|
|
WriteLine($"Does it exist? {Exists(newFolder)}");
|
|
|
|
#!markdown
|
|
|
|
## Managing files
|
|
|
|
#!csharp
|
|
|
|
// define a directory path to output files
|
|
// starting in the user's folder
|
|
string dir = Combine(
|
|
GetFolderPath(SpecialFolder.Personal),
|
|
"Code", "Chapter09", "OutputFiles");
|
|
|
|
CreateDirectory(dir);
|
|
|
|
// define file paths
|
|
string textFile = Combine(dir, "Dummy.txt");
|
|
string backupFile = Combine(dir, "Dummy.bak");
|
|
WriteLine($"Working with: {textFile}");
|
|
|
|
// check if a file exists
|
|
WriteLine($"Does it exist? {File.Exists(textFile)}");
|
|
|
|
// create a new text file and write a line to it
|
|
StreamWriter textWriter = File.CreateText(textFile);
|
|
textWriter.WriteLine("Hello, C#!");
|
|
textWriter.Close(); // close file and release resources
|
|
WriteLine($"Does it exist? {File.Exists(textFile)}");
|
|
|
|
// copy the file, and overwrite if it already exists
|
|
File.Copy(sourceFileName: textFile,
|
|
destFileName: backupFile, overwrite: true);
|
|
WriteLine(
|
|
$"Does {backupFile} exist? {File.Exists(backupFile)}");
|
|
Write("Confirm the files exist, and then run the next notebook cell.");
|
|
|
|
#!csharp
|
|
|
|
// delete file
|
|
File.Delete(textFile);
|
|
WriteLine($"Does it exist? {File.Exists(textFile)}");
|
|
|
|
// read from the text file backup
|
|
WriteLine($"Reading contents of {backupFile}:");
|
|
StreamReader textReader = File.OpenText(backupFile);
|
|
WriteLine(textReader.ReadToEnd());
|
|
textReader.Close();
|
|
|
|
#!markdown
|
|
|
|
## Managing paths
|
|
|
|
#!csharp
|
|
|
|
// Managing paths
|
|
WriteLine($"Folder Name: {GetDirectoryName(textFile)}");
|
|
WriteLine($"File Name: {GetFileName(textFile)}");
|
|
WriteLine("File Name without Extension: {0}",
|
|
GetFileNameWithoutExtension(textFile));
|
|
WriteLine($"File Extension: {GetExtension(textFile)}");
|
|
WriteLine($"Random File Name: {GetRandomFileName()}");
|
|
WriteLine($"Temporary File Name: {GetTempFileName()}");
|
|
|
|
#!markdown
|
|
|
|
## Getting file information
|
|
|
|
#!csharp
|
|
|
|
FileInfo info = new(backupFile);
|
|
WriteLine($"{backupFile}:");
|
|
WriteLine($"Contains {info.Length} bytes");
|
|
WriteLine($"Last accessed {info.LastAccessTime}");
|
|
WriteLine($"Has readonly set to {info.IsReadOnly}");
|
|
|
|
#!markdown
|
|
|
|
## Controlling how you work with files
|
|
|
|
#!csharp
|
|
|
|
FileInfo info = new(backupFile);
|
|
WriteLine("Is the backup file compressed? {0}",
|
|
info.Attributes.HasFlag(FileAttributes.Compressed));
|
|
|
|
#!markdown
|
|
|
|
# Reading and writing with streams
|
|
|
|
#!markdown
|
|
|
|
## Writing to text streams
|
|
|
|
#!csharp
|
|
|
|
static class Viper
|
|
{
|
|
// define an array of Viper pilot call signs
|
|
public static string[] Callsigns = new[]
|
|
{
|
|
"Husker", "Starbuck", "Apollo", "Boomer",
|
|
"Bulldog", "Athena", "Helo", "Racetrack"
|
|
};
|
|
}
|
|
|
|
#!csharp
|
|
|
|
// define a file to write to
|
|
string textFile = Combine(CurrentDirectory, "streams.txt");
|
|
|
|
// create a text file and return a helper writer
|
|
StreamWriter text = File.CreateText(textFile);
|
|
|
|
// enumerate the strings, writing each one
|
|
// to the stream on a separate line
|
|
foreach (string item in Viper.Callsigns)
|
|
{
|
|
text.WriteLine(item);
|
|
}
|
|
text.Close(); // release resources
|
|
|
|
// output the contents of the file
|
|
WriteLine("{0} contains {1:N0} bytes.",
|
|
arg0: textFile,
|
|
arg1: new FileInfo(textFile).Length);
|
|
|
|
WriteLine(File.ReadAllText(textFile));
|
|
|
|
#!markdown
|
|
|
|
## Writing to XML streams
|
|
|
|
#!csharp
|
|
|
|
using System.Xml;
|
|
|
|
#!csharp
|
|
|
|
#nullable enable
|
|
|
|
FileStream? xmlFileStream = null;
|
|
XmlWriter? xml = null;
|
|
|
|
try
|
|
{
|
|
// define a file to write to
|
|
string xmlFile = Combine(CurrentDirectory, "streams.xml");
|
|
|
|
// create a file stream
|
|
xmlFileStream = File.Create(xmlFile);
|
|
|
|
// wrap the file stream in an XML writer helper
|
|
// and automatically indent nested elements
|
|
xml = XmlWriter.Create(xmlFileStream,
|
|
new XmlWriterSettings { Indent = true });
|
|
|
|
// write the XML declaration
|
|
xml.WriteStartDocument();
|
|
// write a root element
|
|
xml.WriteStartElement("callsigns");
|
|
|
|
// enumerate the strings writing each one to the stream
|
|
foreach (string item in Viper.Callsigns)
|
|
{
|
|
xml.WriteElementString("callsign", item);
|
|
}
|
|
|
|
// write the close root element
|
|
xml.WriteEndElement();
|
|
|
|
// close helper and stream
|
|
xml.Close();
|
|
xmlFileStream.Close();
|
|
|
|
// output all the contents of the file
|
|
WriteLine("{0} contains {1:N0} bytes.",
|
|
arg0: xmlFile,
|
|
arg1: new FileInfo(xmlFile).Length);
|
|
|
|
WriteLine(File.ReadAllText(xmlFile));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// if the path doesn't exist the exception will be caught
|
|
WriteLine($"{ex.GetType()} says {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
if (xml != null)
|
|
{
|
|
xml.Dispose();
|
|
WriteLine("The XML writer's unmanaged resources have been disposed.");
|
|
|
|
if (xmlFileStream != null)
|
|
{
|
|
xmlFileStream.Dispose();
|
|
WriteLine("The file stream's unmanaged resources have been disposed.");
|
|
}
|
|
}
|
|
}
|
|
|
|
#!markdown
|
|
|
|
## Simplifying disposal by using the using statement
|
|
|
|
#!csharp
|
|
|
|
using FileStream file2 = File.OpenWrite(
|
|
Path.Combine(CurrentDirectory, "file2.txt"));
|
|
|
|
using StreamWriter writer2 = new(file2);
|
|
|
|
try
|
|
{
|
|
writer2.WriteLine("Welcome, .NET!");
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
WriteLine($"{ex.GetType()} says {ex.Message}");
|
|
}
|
|
|
|
#!markdown
|
|
|
|
## Compressing streams
|
|
|
|
#!csharp
|
|
|
|
using System.IO.Compression; // BrotliStream, GZipStream, CompressionMode
|
|
|
|
#!csharp
|
|
|
|
static void WorkWithCompression(bool useBrotli = true)
|
|
{
|
|
string fileExt = useBrotli ? "brotli" : "gzip";
|
|
|
|
// compress the XML output
|
|
string filePath = Combine(
|
|
CurrentDirectory, $"streams.{fileExt}");
|
|
|
|
FileStream file = File.Create(filePath);
|
|
|
|
Stream compressor;
|
|
|
|
if (useBrotli)
|
|
{
|
|
compressor = new BrotliStream(file, CompressionMode.Compress);
|
|
}
|
|
else
|
|
{
|
|
compressor = new GZipStream(file, CompressionMode.Compress);
|
|
}
|
|
|
|
using (compressor)
|
|
{
|
|
using (XmlWriter xml = XmlWriter.Create(compressor))
|
|
{
|
|
xml.WriteStartDocument();
|
|
xml.WriteStartElement("callsigns");
|
|
foreach (string item in Viper.Callsigns)
|
|
{
|
|
xml.WriteElementString("callsign", item);
|
|
}
|
|
}
|
|
} // also closes the underlying stream
|
|
|
|
// output all the contents of the compressed file
|
|
WriteLine("{0} contains {1:N0} bytes.",
|
|
filePath, new FileInfo(filePath).Length);
|
|
|
|
WriteLine($"The compressed contents:");
|
|
WriteLine(File.ReadAllText(filePath));
|
|
|
|
// read a compressed file
|
|
WriteLine("Reading the compressed XML file:");
|
|
file = File.Open(filePath, FileMode.Open);
|
|
|
|
Stream decompressor;
|
|
if (useBrotli)
|
|
{
|
|
decompressor = new BrotliStream(
|
|
file, CompressionMode.Decompress);
|
|
}
|
|
else
|
|
{
|
|
decompressor = new GZipStream(
|
|
file, CompressionMode.Decompress);
|
|
}
|
|
|
|
using (decompressor)
|
|
{
|
|
using (XmlReader reader = XmlReader.Create(decompressor))
|
|
{
|
|
while (reader.Read())
|
|
{
|
|
// check if we are on an element node named callsign
|
|
if ((reader.NodeType == XmlNodeType.Element)
|
|
&& (reader.Name == "callsign"))
|
|
{
|
|
reader.Read(); // move to the text inside element
|
|
WriteLine($"{reader.Value}"); // read its value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#!csharp
|
|
|
|
WorkWithCompression();
|
|
WorkWithCompression(useBrotli: false);
|
|
|
|
#!markdown
|
|
|
|
# Encoding and decoding text
|
|
|
|
Change the `ConsoleKey` value to D1 to D5 to select the encoding that you want to use.
|
|
|
|
#!csharp
|
|
|
|
// choose an encoding
|
|
Write("Press a number to choose an encoding: ");
|
|
ConsoleKey number = ConsoleKey.D3; // ReadKey(intercept: false).Key;
|
|
WriteLine();
|
|
WriteLine();
|
|
|
|
Encoding encoder = number switch
|
|
{
|
|
ConsoleKey.D1 => Encoding.ASCII,
|
|
ConsoleKey.D2 => Encoding.UTF7,
|
|
ConsoleKey.D3 => Encoding.UTF8,
|
|
ConsoleKey.D4 => Encoding.Unicode,
|
|
ConsoleKey.D5 => Encoding.UTF32,
|
|
_ => Encoding.Default
|
|
};
|
|
|
|
// define a string to encode
|
|
string message = "Café cost: £4.39";
|
|
|
|
// encode the string into a byte array
|
|
byte[] encoded = encoder.GetBytes(message);
|
|
|
|
// check how many bytes the encoding needed
|
|
WriteLine("{0} uses {1:N0} bytes.",
|
|
encoder.GetType().Name, encoded.Length);
|
|
WriteLine();
|
|
|
|
// enumerate each byte
|
|
WriteLine($"BYTE HEX CHAR");
|
|
foreach (byte b in encoded)
|
|
{
|
|
WriteLine($"{b,4} {b.ToString("X"),4} {(char)b,5}");
|
|
}
|
|
// decode the byte array back into a string and display it
|
|
string decoded = encoder.GetString(encoded);
|
|
WriteLine(decoded);
|
|
|
|
#!markdown
|
|
|
|
# Serializing object graphs
|
|
|
|
Serialization is the process of converting a live object into a sequence of bytes using a specified
|
|
format. Deserialization is the reverse process.
|
|
|
|
#!csharp
|
|
|
|
#nullable enable
|
|
|
|
public class Person
|
|
{
|
|
public Person() { } // required to use XmlSerializer
|
|
|
|
public Person(decimal initialSalary)
|
|
{
|
|
Salary = initialSalary;
|
|
}
|
|
|
|
public string? FirstName { get; set; }
|
|
public string? LastName { get; set; }
|
|
public DateTime DateOfBirth { get; set; }
|
|
public HashSet<Person>? Children { get; set; }
|
|
protected decimal Salary { get; set; }
|
|
}
|
|
|
|
#!csharp
|
|
|
|
using System.Xml.Serialization; // XmlSerializer
|
|
|
|
#!csharp
|
|
|
|
// create an object graph
|
|
List<Person> people = new()
|
|
{
|
|
new(30000M)
|
|
{
|
|
FirstName = "Alice",
|
|
LastName = "Smith",
|
|
DateOfBirth = new(1974, 3, 14)
|
|
},
|
|
new(40000M)
|
|
{
|
|
FirstName = "Bob",
|
|
LastName = "Jones",
|
|
DateOfBirth = new(1969, 11, 23)
|
|
},
|
|
new(20000M)
|
|
{
|
|
FirstName = "Charlie",
|
|
LastName = "Cox",
|
|
DateOfBirth = new(1984, 5, 4),
|
|
Children = new()
|
|
{
|
|
new(0M)
|
|
{
|
|
FirstName = "Sally",
|
|
LastName = "Cox",
|
|
DateOfBirth = new(2000, 7, 12)
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
#!csharp
|
|
|
|
// create object that will format a List of Persons as XML
|
|
XmlSerializer xs = new(people.GetType());
|
|
WriteLine(people.GetType());
|
|
|
|
// create a file to write to
|
|
string path = Combine(CurrentDirectory, "people.xml");
|
|
WriteLine(path);
|
|
|
|
using (FileStream stream = File.Create(path))
|
|
{
|
|
// serialize the object graph to the stream
|
|
xs.Serialize(stream, people);
|
|
}
|
|
|
|
WriteLine("Written {0:N0} bytes of XML to {1}",
|
|
arg0: new FileInfo(path).Length,
|
|
arg1: path);
|
|
WriteLine();
|
|
|
|
// Display the serialized object graph
|
|
WriteLine(File.ReadAllText(path));
|
|
|
|
#!markdown
|
|
|
|
## Serializing with JSON
|
|
|
|
#!csharp
|
|
|
|
#r "nuget:Newtonsoft.Json,13.0.1"
|
|
|
|
#!csharp
|
|
|
|
// create a file to write to
|
|
string jsonPath = Combine(CurrentDirectory, "people.json");
|
|
|
|
using (StreamWriter jsonStream = File.CreateText(jsonPath))
|
|
{
|
|
// create an object that will format as JSON
|
|
Newtonsoft.Json.JsonSerializer jss = new();
|
|
// serialize the object graph into a string
|
|
jss.Serialize(jsonStream, people);
|
|
}
|
|
WriteLine();
|
|
WriteLine("Written {0:N0} bytes of JSON to: {1}",
|
|
arg0: new FileInfo(jsonPath).Length,
|
|
arg1: jsonPath);
|
|
|
|
// Display the serialized object graph
|
|
WriteLine(File.ReadAllText(jsonPath));
|
|
|
|
#!csharp
|
|
|
|
using NewJson = System.Text.Json.JsonSerializer;
|
|
|
|
#!csharp
|
|
|
|
using (FileStream jsonLoad = File.Open(jsonPath, FileMode.Open))
|
|
{
|
|
// deserialize object graph into a List of Person
|
|
List<Person>? loadedPeople =
|
|
await NewJson.DeserializeAsync(utf8Json: jsonLoad,
|
|
returnType: typeof(List<Person>)) as List<Person>;
|
|
|
|
if (loadedPeople is not null)
|
|
{
|
|
foreach (Person p in loadedPeople)
|
|
{
|
|
WriteLine("{0} has {1} children.",
|
|
p.LastName, p.Children?.Count ?? 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#!markdown
|
|
|
|
## Controlling JSON processing
|
|
|
|
#!csharp
|
|
|
|
using System.Text.Json; // JsonSerializer
|
|
using System.Text.Json.Serialization; // [JsonInclude]
|
|
|
|
#!csharp
|
|
|
|
#nullable enable
|
|
|
|
public class Book
|
|
{
|
|
// constructor to set non-nullable property
|
|
public Book(string title)
|
|
{
|
|
Title = title;
|
|
}
|
|
|
|
// properties
|
|
public string Title { get; set; }
|
|
public string? Author { get; set; }
|
|
|
|
// fields
|
|
[JsonInclude] // include this field
|
|
public DateOnly PublishDate;
|
|
|
|
[JsonInclude] // include this field
|
|
public DateTimeOffset Created;
|
|
|
|
public ushort Pages;
|
|
}
|
|
|
|
#!csharp
|
|
|
|
Book csharp10 = new(title:
|
|
"C# 10 and .NET 6 - Modern Cross-platform Development")
|
|
{
|
|
Author = "Mark J Price",
|
|
PublishDate = new(year: 2021, month: 11, day: 9),
|
|
Pages = 823,
|
|
Created = DateTimeOffset.UtcNow,
|
|
};
|
|
|
|
JsonSerializerOptions options = new()
|
|
{
|
|
IncludeFields = true, // includes all fields
|
|
PropertyNameCaseInsensitive = true,
|
|
WriteIndented = true,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
};
|
|
|
|
string filePath = Combine(CurrentDirectory, "book.json");
|
|
|
|
using (Stream fileStream = File.Create(filePath))
|
|
{
|
|
JsonSerializer.Serialize<Book>(
|
|
utf8Json: fileStream, value: csharp10, options);
|
|
}
|
|
|
|
WriteLine("Written {0:N0} bytes of JSON to {1}",
|
|
arg0: new FileInfo(filePath).Length,
|
|
arg1: filePath);
|
|
WriteLine();
|
|
|
|
// Display the serialized object graph
|
|
WriteLine(File.ReadAllText(filePath));
|