Chapter 17 - Serialization

XmlSerializer

Attribute-based serialization - Getting started

void Main()
{
  Person p = new Person();
  p.Name = "Stacey"; p.Age = 30;

  var xs = new XmlSerializer (typeof (Person));

  using (Stream s = File.Create ("person.xml"))
    xs.Serialize (s, p);

  Person p2;
  using (Stream s = File.OpenRead ("person.xml"))
    p2 = (Person)xs.Deserialize (s);

  Console.WriteLine (p2.Name + " " + p2.Age);   // Stacey 30
  
  File.ReadAllText ("person.xml").Dump ("XML");
}

public class Person
{
  public string Name;
  public int Age;
}

Attribute-based serialization - attributes names and namespaces

void Main()
{
  Person p = new Person();
  p.Name = "Stacey"; p.Age = 30;

  var xs = new XmlSerializer (typeof (Person));

  using (Stream s = File.Create ("person.xml"))
    xs.Serialize (s, p);

  Person p2;
  using (Stream s = File.OpenRead ("person.xml"))
    p2 = (Person)xs.Deserialize (s);

  Console.WriteLine (p2.Name + " " + p2.Age);   // Stacey 30
  
  File.ReadAllText ("person.xml").Dump ("XML");
}

[XmlRoot ("Candidate", Namespace = "http://mynamespace/test/")]
public class Person
{
  [XmlElement ("FirstName")] public string Name;
  [XmlAttribute ("RoughAge")] public int Age;
}

Attribute-based serialization - XML element order

void Main()
{
  Person p = new Person();
  p.Name = "Stacey"; p.Age = 30;

  var xs = new XmlSerializer (typeof (Person));

  using (Stream s = File.Create ("person.xml"))
    xs.Serialize (s, p);

  Person p2;
  using (Stream s = File.OpenRead ("person.xml"))
    p2 = (Person)xs.Deserialize (s);

  Console.WriteLine (p2.Name + " " + p2.Age);   // Stacey 30
  
  File.ReadAllText ("person.xml").Dump ("XML");
}

public class Person
{
  [XmlElement (Order = 2)] public string Name;
  [XmlElement (Order = 1)] public int Age;
}

Subclasses and Child Objects - subclassing root type

void Main()
{
  var p = new Student { Name = "Stacey" };
  SerializePerson (p, "person.xml");
  File.ReadAllText ("person.xml").Dump ("XML");
}

public void SerializePerson (Person p, string path)
{
  XmlSerializer xs = new XmlSerializer (typeof (Person));
  using (Stream s = File.Create (path))
    xs.Serialize (s, p);
}

[XmlInclude (typeof (Student))]
[XmlInclude (typeof (Teacher))]
public class Person { public string Name; }

public class Student : Person { }
public class Teacher : Person { }

Subclasses and Child Objects - serializing child objects

void Main()
{
  Person p = new Person { Name = "Stacey" };
  p.HomeAddress.Street = "Odo St";
  p.HomeAddress.PostCode = "6020";

  SerializePerson (p, "person.xml");
  File.ReadAllText ("person.xml").Dump ("XML");
}

public void SerializePerson (Person p, string path)
{
  XmlSerializer xs = new XmlSerializer (typeof (Person));
  using (Stream s = File.Create (path))
    xs.Serialize (s, p);
}

public class Person
{
  public string Name;
  public Address HomeAddress = new Address();
}

public class Address { public string Street, PostCode; }

Subclasses and Child Objects - subclassing child objects - option 1

void Main()
{
  Person p = new Person { Name = "Stacey" };
  p.HomeAddress.Street = "Odo St";
  p.HomeAddress.PostCode = "6020";

  SerializePerson (p, "person.xml");
  File.ReadAllText ("person.xml").Dump ("XML");
}

public void SerializePerson (Person p, string path)
{
  XmlSerializer xs = new XmlSerializer (typeof (Person));
  using (Stream s = File.Create (path))
    xs.Serialize (s, p);
}

[XmlInclude (typeof (AUAddress))]
[XmlInclude (typeof (USAddress))]
public class Address { public string Street, PostCode; }

public class USAddress : Address { }
public class AUAddress : Address { }

public class Person
{
  public string Name;
  public Address HomeAddress = new USAddress();
}

Subclasses and Child Objects - subclassing child objects - option 2

void Main()
{
  Person p = new Person { Name = "Stacey" };
  p.HomeAddress.Street = "Odo St";
  p.HomeAddress.PostCode = "6020";

  SerializePerson (p, "person.xml");
  File.ReadAllText ("person.xml").Dump ("XML");
}

public void SerializePerson (Person p, string path)
{
  XmlSerializer xs = new XmlSerializer (typeof (Person));
  using (Stream s = File.Create (path))
    xs.Serialize (s, p);
}

public class Address { public string Street, PostCode; }

public class USAddress : Address { }
public class AUAddress : Address { }

public class Person
{
  public string Name;

  [XmlElement ("Address", typeof (Address))]
  [XmlElement ("AUAddress", typeof (AUAddress))]
  [XmlElement ("USAddress", typeof (USAddress))]
  public Address HomeAddress = new USAddress();
}

Serializing Collections

void Main()
{
  Person p = new Person { Name = "Stacey" };
  p.Addresses.Add (new Address { Street = "My Street", PostCode = "1234" });
  p.Addresses.Add (new Address { Street = "My Way", PostCode = "2345" });

  SerializePerson (p, "person.xml");
  File.ReadAllText ("person.xml").Dump ("XML");
}

public void SerializePerson (Person p, string path)
{
  XmlSerializer xs = new XmlSerializer (typeof (Person));
  using (Stream s = File.Create (path))
    xs.Serialize (s, p);
}

public class Person
{
  public string Name;
  public List<Address> Addresses = new List<Address>();
}

public class Address { public string Street, PostCode; }

Serializing Collections - renaming elements

void Main()
{
  Person p = new Person { Name = "Stacey" };
  p.Addresses.Add (new Address { Street = "My Street", PostCode = "1234" });
  p.Addresses.Add (new Address { Street = "My Way", PostCode = "2345" });

  SerializePerson (p, "person.xml");
  File.ReadAllText ("person.xml").Dump ("XML");
}

public void SerializePerson (Person p, string path)
{
  XmlSerializer xs = new XmlSerializer (typeof (Person));
  using (Stream s = File.Create (path))
    xs.Serialize (s, p);
}

public class Person
{
  public string Name;

  [XmlArray ("PreviousAddresses")]
  [XmlArrayItem ("Location")]
  public List<Address> Addresses = new List<Address>();
}

public class Address { public string Street, PostCode; }

Serializing Collections - without outer element

void Main()
{
  Person p = new Person { Name = "Stacey" };
  p.Addresses.Add (new Address { Street = "My Street", PostCode = "1234" });
  p.Addresses.Add (new Address { Street = "My Way", PostCode = "2345" });

  SerializePerson (p, "person.xml");
  File.ReadAllText ("person.xml").Dump ("XML");
}

public void SerializePerson (Person p, string path)
{
  XmlSerializer xs = new XmlSerializer (typeof (Person));
  using (Stream s = File.Create (path))
    xs.Serialize (s, p);
}

public class Person
{
  public string Name;

  [XmlElement ("Address")]
  public List<Address> Addresses = new List<Address>();
}

public class Address { public string Street, PostCode; }

Serializing Collections - subclassed elements with type attribute

void Main()
{
  Person p = new Person { Name = "Stacey" };
  p.Addresses.Add (new USAddress { Street = "My Street", PostCode = "90210" });
  p.Addresses.Add (new AUAddress { Street = "My Way", PostCode = "6000" });

  SerializePerson (p, "person.xml");
  File.ReadAllText ("person.xml").Dump ("XML");
}

public void SerializePerson (Person p, string path)
{
  XmlSerializer xs = new XmlSerializer (typeof (Person));
  using (Stream s = File.Create (path))
    xs.Serialize (s, p);
}

[XmlInclude (typeof (AUAddress))]
[XmlInclude (typeof (USAddress))]
public class Address { public string Street, PostCode; }

public class USAddress : Address { }
public class AUAddress : Address { }

public class Person
{
  public string Name;

  [XmlElement ("Address")]
  public List<Address> Addresses = new List<Address>();
}

Serializing Collections - subclassed elements with name

void Main()
{
  Person p = new Person { Name = "Stacey" };
  p.Addresses.Add (new USAddress { Street = "My Street", PostCode = "90210" });
  p.Addresses.Add (new AUAddress { Street = "My Way", PostCode = "6000" });

  SerializePerson (p, "person.xml");
  File.ReadAllText ("person.xml").Dump ("XML");
}

public void SerializePerson (Person p, string path)
{
  XmlSerializer xs = new XmlSerializer (typeof (Person));
  using (Stream s = File.Create (path))
    xs.Serialize (s, p);
}

[XmlInclude (typeof (AUAddress))]
[XmlInclude (typeof (USAddress))]
public class Address { public string Street, PostCode; }

public class USAddress : Address { }
public class AUAddress : Address { }

public class Person
{
  public string Name;

  [XmlElement ("Address")]
  public List<Address> Addresses = new List<Address>();
}

Serializing Collections - subclassed elements with name (no outer element)

void Main()
{
  Person p = new Person { Name = "Stacey" };
  p.Addresses.Add (new USAddress { Street = "My Street", PostCode = "90210" });
  p.Addresses.Add (new AUAddress { Street = "My Way", PostCode = "6000" });

  SerializePerson (p, "person.xml");
  File.ReadAllText ("person.xml").Dump ("XML");
}

public void SerializePerson (Person p, string path)
{
  XmlSerializer xs = new XmlSerializer (typeof (Person));
  using (Stream s = File.Create (path))
    xs.Serialize (s, p);
}

public class Address { public string Street, PostCode; }

public class USAddress : Address { }
public class AUAddress : Address { }

public class Person
{
  public string Name;

  [XmlElement ("Address",   typeof (Address))]
  [XmlElement ("AUAddress", typeof (AUAddress))]
  [XmlElement ("USAddress", typeof (USAddress))]
  public List<Address> Addresses = new List<Address>();
}

Interoperating with IXmlSerializable

void Main()
{
  Person p = new Person 
  {
    Name = "Stacey",
    HomeAddress = new Address { Street = "My Street", PostCode = "90210" }
  };

  SerializePerson (p, "person.xml");
  File.ReadAllText ("person.xml").Dump ("XML");
}

public void SerializePerson (Person p, string path)
{
  XmlSerializer xs = new XmlSerializer (typeof (Person));
  using (Stream s = File.Create (path))
    xs.Serialize (s, p);
}

public class Person
{
  public string Name;
  public Address HomeAddress;
}

public class Address : IXmlSerializable
{
  public string Street, PostCode;

  public XmlSchema GetSchema() { return null; }

  public void ReadXml (XmlReader reader)
  {
    reader.ReadStartElement();
    Street = reader.ReadElementContentAsString ("Street", "");
    PostCode = reader.ReadElementContentAsString ("PostCode", "");
    reader.ReadEndElement();
  }

  public void WriteXml (XmlWriter writer)
  {
    writer.WriteElementString ("Street", Street);
    writer.WriteElementString ("PostCode", PostCode);
  }
}
JsonSerializer

Getting started

void Main()
{
  var p = new Person { Name = "Ian" };
  string json = JsonSerializer.Serialize (p,
                  new JsonSerializerOptions() { WriteIndented = true });
  json.Dump();
  
  Person p2 = JsonSerializer.Deserialize<Person> (json);
  p2.Dump();
}

public class Person
{
  public string Name { get; set; }
}

Serializing child objects

void Main()
{
  var home = new Address { Street = "1 Main St.", PostCode = "11235" };
  var work = new Address { Street = "4 Elm Ln.", PostCode = "31415" };
  
  var p = new Person { Name = "Ian", HomeAddress = home, WorkAddress = work };

  Console.WriteLine (JsonSerializer.Serialize (p,
                   new JsonSerializerOptions { WriteIndented = true }));
}

public class Address
{
  public string Street { get; set; }
  public string PostCode { get; set; }
}

public class Person
{
  public string Name { get; set; }
  public Address HomeAddress { get; set; }
  public Address WorkAddress { get; set; }
}

Serializing child objects - object references

void Main()
{
  var home = new Address { Street = "1 Main St.", PostCode = "11235" };
  
  var p = new Person { Name = "Ian", HomeAddress = home, WorkAddress = home };

  Console.WriteLine (JsonSerializer.Serialize (p,
                   new JsonSerializerOptions { WriteIndented = true }));
}

public class Address
{
  public string Street { get; set; }
  public string PostCode { get; set; }
}

public class Person
{
  public string Name { get; set; }
  public Address HomeAddress { get; set; }
  public Address WorkAddress { get; set; }
}

Serializing collections

void Main()
{
  var sara = new Person() { Name = "Sara" };
  var ian = new Person() { Name = "Ian" };

  string json = JsonSerializer.Serialize (new[] { sara, ian },
    new JsonSerializerOptions { WriteIndented = true }).Dump ("Json");
    
  Person[] people = JsonSerializer.Deserialize<Person[]>(json);
  people.Dump();
}

public class Person
{
  public string Name { get; set; }
}

Serializing collections - differently typed objects

void Main()
{
  var sara = new Person { Name = "Sara" };
  var addr = new Address { Street = "1 Main St.", PostCode = "11235" };

  string json = JsonSerializer.Serialize (new object[] { sara, addr },
    new JsonSerializerOptions() { WriteIndented = true }).Dump ("JSON");

  var deserialized = JsonSerializer.Deserialize<JsonElement[]>(json);
  foreach (var element in deserialized)
  {
    foreach (var prop in element.EnumerateObject())
      Console.WriteLine ($"{prop.Name}: {prop.Value}");
    Console.WriteLine ("---");
  }
  
}

public class Person
{
  public string Name { get; set; }
}

public class Address
{
  public string Street { get; set; }
  public string PostCode { get; set; }
}

Controlling serialization with attributes

void Main()
{
  var p = new Person { Name = "Ian" };
  string json = JsonSerializer.Serialize (p,
                  new JsonSerializerOptions() { WriteIndented = true });
  json.Dump();
  
  Person p2 = JsonSerializer.Deserialize<Person> (json);
  p2.Dump();
}

public class Person
{
  [JsonPropertyName("FullName")]
  public string Name { get; set; }

  [JsonIgnore]
  public decimal NetWorth { get; set; }   // Not serialized
}

Customizing data conversion

void Main()
{
  var json = @"
  {
    ""Id"":27182,
    ""Name"":""Sara"",
    ""Born"":464572800
  }";
  
  JsonSerializerOptions opts = new JsonSerializerOptions();
  opts.Converters.Add (new UnixTimestampConverter());
  var sara = JsonSerializer.Deserialize<Person> (json, opts);  
  sara.Dump();
}

public class Person
{
  public int Id { get; set; }
  public string Name { get; set; }
  public DateTime Born { get; set; }
}

public class UnixTimestampConverter : JsonConverter<DateTime>
{
  public override DateTime Read (ref Utf8JsonReader reader, Type type,
                                 JsonSerializerOptions options)
  {
    if (reader.TryGetInt32(out int timestamp))
      return new DateTime (1970, 1, 1).AddSeconds (timestamp);

    throw new Exception ("Expected the timestamp as a number.");
  }

  public override void Write (Utf8JsonWriter writer, DateTime value,
                              JsonSerializerOptions options)
  {
    int timestamp = (int)(value - new DateTime(1970, 1, 1)).TotalSeconds;
    writer.WriteNumberValue(timestamp);
  }
}

Customizing data conversion - default

void Main()
{
  var json = @"
  {
    ""Id"":27182,
    ""Name"":""Sara"",
    ""Born"":464572800
  }";
  
  var sara = JsonSerializer.Deserialize<Person> (json);  
  sara.Dump();
}

public class Person
{
  public int Id { get; set; }
  public string Name { get; set; }
  
  [JsonConverter(typeof(UnixTimestampConverter))]
  public DateTime Born { get; set; }
}

public class UnixTimestampConverter : JsonConverter<DateTime>
{
  public override DateTime Read (ref Utf8JsonReader reader, Type type,
                                 JsonSerializerOptions options)
  {
    if (reader.TryGetInt32(out int timestamp))
      return new DateTime (1970, 1, 1).AddSeconds (timestamp);

    throw new Exception ("Expected the timestamp as a number.");
  }

  public override void Write (Utf8JsonWriter writer, DateTime value,
                              JsonSerializerOptions options)
  {
    int timestamp = (int)(value - new DateTime(1970, 1, 1)).TotalSeconds;
    writer.WriteNumberValue(timestamp);
  }
}

Options - TrailingCommas

void Main()
{
  var dylan = new Person_v3()
  {
    Name = "Dylan",
    LuckyNumbers = new List<int>() { 10, 7 },
    Age = 46
  };
  
  JsonSerializerOptions opts = new JsonSerializerOptions();
  opts.WriteIndented = true;
  var json = JsonSerializer.Serialize<Person_v3>(dylan, opts);
  
  "Correct JSON".Dump();
  json.Dump();
  
  var brokenJson = json.Replace("7", "7,").Replace("46", "46,");
  "Broken JSON".Dump(); // Because of trailing commas we just added
  brokenJson.Dump();
  
  "Try to deserialize trailing commas without setting options.".Dump();
  
  try
  {
    var dylanBroken = JsonSerializer.Deserialize<Person_v3>(brokenJson);
  }
  catch (JsonException ex)
  {
    $"As expected, the JSON can't be parsed: {ex.Message}".Dump();
  }
  
  "Deserialize with option AllowTrailingCommas = true".Dump();
  
  var dylanCommaTolerant = JsonSerializer.Deserialize<Person_v3>(brokenJson, 
    new JsonSerializerOptions() { AllowTrailingCommas = true });
  dylanCommaTolerant.Dump();
}

class Person_v3
{
  public string Name { get; set; }
  public List<int> LuckyNumbers { get; set; }
  public int Age { get; set; }
}

Options - ReadCommentHandling

void Main()
{
  string json = @"
  { 
    ""Name"":""Dylan"" // Comment here    
    /* Another comment here */
  }";
  
  var dylan = JsonSerializer.Deserialize<Person>(json, 
    new JsonSerializerOptions() { 
        WriteIndented = true,
        ReadCommentHandling = JsonCommentHandling.Skip
      });  
           
  json.Dump();
  dylan.Dump();
}

class Person
{
  public string Name { get; set; }
}

Options - WriteIndented

void Main()
{ 
  var dylan = new Person()
  {
    Name = "Dylan",
    Birthdate = new DateTime (1996, 9, 7)
  };
  
  var json1 = JsonSerializer.Serialize (dylan, 
    new JsonSerializerOptions { WriteIndented = true });  
  
  // WriteIndented defaults to false
  var json2 = JsonSerializer.Serialize (dylan); 
           
  json1.Dump();
  json2.Dump();
}

class Person
{
  public string Name { get; set; }
  public DateTime Birthdate { get; set; }
}

Options - PropertyNameCaseInsensive

void Main()
{
  string json = "{ \"name\":\"Dylan\" }";
  
  var dylan1 = JsonSerializer.Deserialize<Person>(json, 
    new JsonSerializerOptions() { 
        WriteIndented = true 
      });  
      
  var dylan2 = JsonSerializer.Deserialize<Person>(json, 
    new JsonSerializerOptions() { 
        WriteIndented = true,
        PropertyNameCaseInsensitive = true
      });   
      
  dylan1.Dump();
  dylan2.Dump();
}

class Person
{
  public string Name { get; set; }
}

Options - PropertyNamingPolicy

void Main()
{
  var dylan = new Person { Name = "Dylan" };

  var json = JsonSerializer.Serialize (dylan,
    new JsonSerializerOptions
    {
      PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    });

  var dylan2 = JsonSerializer.Deserialize<Person> (json,
    new JsonSerializerOptions
    {
      PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    });

  dylan.Dump();
  json.Dump();
  dylan2.Dump();
}

class Person
{
  public string Name { get; set; }
}

Options - DictionaryKeyPolicy

void Main()
{
  var dict = new Dictionary<string, string>
  {
    { "ProgramVersion", "1.2" },
    { "PackageName", "Nutshell" }
  };
  
  Console.WriteLine (JsonSerializer.Serialize (dict,
    new JsonSerializerOptions()
    {
      WriteIndented = true,
      DictionaryKeyPolicy = JsonNamingPolicy.CamelCase
    }));
}

class Person
{
  public string Name { get; set; }
  public Dictionary<string, string> SportsTeams { get; set; }
}

Options - Encoder - UnsafeRelaxedJsonEscaping

void Main()
{
  var dylan = "<b>Dylan & Friends</b>";
  
  JsonSerializer.Serialize (dylan).Dump();

  JsonSerializer.Serialize (dylan,
    new JsonSerializerOptions
    {
      Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
    }).Dump();
}

class Person
{
  public string Name { get; set; }
  public Dictionary<string, string> SportsTeams { get; set; }
}

Options - IgnoreNullValues

void Main()
{
  var dylan = new Person { Name = null };

  JsonSerializer.Serialize (dylan, 
    new JsonSerializerOptions { WriteIndented = true }
    ).Dump("default");

  JsonSerializer.Serialize (dylan,
    new JsonSerializerOptions
    { 
      WriteIndented = true,
      IgnoreNullValues = true
    }).Dump ("with IgnoreNullValues");
}

class Person
{
  public string Name { get; set; }
  public int Age 
  { 
    get
    {
      var age = DateTime.Today.Year - Birthdate.Year;
      if (Birthdate.Date > DateTime.Today.AddYears(-age)) age--;
      return age;
    }
  }
  public DateTime Birthdate { get; set; }
}
The Binary Serializer

Getting started

void Main()
{
  Person p = new Person { Name = "George", Age = 25 };

  IFormatter formatter = new BinaryFormatter();

  using (FileStream s = File.Create ("serialized.bin"))
    formatter.Serialize (s, p);
    
  using (FileStream s = File.OpenRead ("serialized.bin"))
  {
    Person p2 = (Person)formatter.Deserialize (s);
    Console.WriteLine (p2.Name + " " + p2.Age);     // George 25
  }
}

[Serializable]
public sealed class Person
{
  public string Name;
  public int Age;
}

[NonSerialized]

void Main()
{
  Person p = new Person { Name = "George", Age = 25 };

  IFormatter formatter = new BinaryFormatter();

  using (FileStream s = File.Create ("serialized.bin"))
    formatter.Serialize (s, p);
    
  using (FileStream s = File.OpenRead ("serialized.bin"))
  {
    Person p2 = (Person)formatter.Deserialize (s);
    Console.WriteLine (p2.Name + " " + p2.Age);     // George 25
  }
}

[Serializable] public sealed class Person
{
  public string Name;
  [NonSerialized] public int Age;
}

[OnDeserializing] and [OnDeserialized]

void Main()
{
  Person p = new Person { Name = "George", DateOfBirth = new DateTime (1990, 1, 1) };

  IFormatter formatter = new BinaryFormatter();

  using (FileStream s = File.Create ("serialized.bin"))
    formatter.Serialize (s, p);

  using (FileStream s = File.OpenRead ("serialized.bin"))
  {
    Person p2 = (Person)formatter.Deserialize (s);
    Console.WriteLine (p2.Name + " " + p2.Age);     // George 25
  }
}

[Serializable]
public sealed class Person
{
  public string Name;
  public DateTime DateOfBirth;

  [NonSerialized] public int Age;
  [NonSerialized] public bool Valid = true;

  public Person() { Valid = true; }

  [OnDeserialized]
  void OnDeserialized (StreamingContext context)
  {
    TimeSpan ts = DateTime.Now - DateOfBirth;
    Age = ts.Days / 365;                         // Rough age in years
  }
}

[OnSerializing] and [OnSerialized]

void Main()
{
  var foo = new Foo { Xml = XDocument.Parse ("<test />") };

  IFormatter formatter = new BinaryFormatter();

  using (FileStream s = File.Create ("serialized.bin"))
    formatter.Serialize (s, foo);

  using (FileStream s = File.OpenRead ("serialized.bin"))
  {
    var f2 = (Foo)formatter.Deserialize (s);
    f2.Xml.Dump();
  }
}

[Serializable]
class Foo
{
  [NonSerialized]
  public XDocument Xml;

  string _xmlString;
  
  [OnSerializing] 
  void OnSerializing (StreamingContext context) => _xmlString = Xml.ToString();

  [OnDeserialized]
  void OnDeserialized (StreamingContext context) => Xml = XDocument.Parse (_xmlString);
}

Implementing ISerializable

void Main()
{
  var team = new Team ("Team", new Player ("Joe"));

  IFormatter formatter = new BinaryFormatter();

  using (FileStream s = File.Create ("serialized.bin"))
    formatter.Serialize (s, team);

  using (FileStream s = File.OpenRead ("serialized.bin"))
  {
    var team2 = (Team)formatter.Deserialize (s);
    team2.Dump();
  }
}

[Serializable]
public class Player
{
  public readonly string Name;
  public Player (string name) => Name = name;
}

[Serializable]
public class Team : ISerializable
{
  public readonly string Name;
  public readonly ImmutableList<Player> Players;

  public Team (string name, params Player[] players) 
  {
    Name = name;
    Players = players.ToImmutableList();
  }

  public virtual void GetObjectData (SerializationInfo si,
                                     StreamingContext sc)
  {
    si.AddValue ("Name", Name);
    si.AddValue ("PlayerData", Players.ToArray());
  }

  protected Team (SerializationInfo si, StreamingContext sc)
  {
    Name = si.GetString ("Name");

    // Deserialize Players to an array to match our serialization:
    Player[] p = (Player[])si.GetValue ("PlayerData", typeof (Player[]));

    // Construct a new List using this array:
    Players = p.ToImmutableList();
  }
}
C# 12 in a Nutshell
Buy from amazon.com Buy print or Kindle edition
Buy from ebooks.com Buy PDF edition
Buy from O'Reilly Read via O'Reilly subscription