#!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? Children { get; set; } protected decimal Salary { get; set; } } #!csharp using System.Xml.Serialization; // XmlSerializer #!csharp // create an object graph List 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? loadedPeople = await NewJson.DeserializeAsync(utf8Json: jsonLoad, returnType: typeof(List)) as List; 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( 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));