© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
G. ByrneTarget C#https://doi.org/10.1007/978-1-4842-8619-7_18

18. Serialization

Gerard Byrne1  
(1)
Belfast, Ireland
 

Serialization and Deserialization

In Chapter 13 we gained knowledge of classes and objects. This chapter will extend our knowledge and explain how we can save an object so it can be recreated when required. The processes we will investigate are called serialization and deserialization. By serializing we are saving the state of the object. We will also require some of the knowledge we gained in Chapter 16 on file handling as we will write binary data , XML data, and JSON data to a file as part of the serialization process.

Serialization is a process to convert an object into a stream of bytes so that the bytes can be written into a file. We will normally do this so the serialized data can be stored in a database or sent across a network, for example, to a message queue to form part of a transaction process. The byte stream created for XML and JSON is platform independent; it is an object serialized on one platform that can be deserialized on a different platform. All fields of type private, public, and internal will be serialized.

In an enterprise we may wish to send the serialized data from one domain to another, to a Rest API web service that could then store it in a database or deserialize it and use the details in some business logic. To ensure the serialization process works without an error, there are a number of things we need to ensure:
  • The class being serialized must have the [Serializable] attribute above the class. In this example the class will be called Customer , so the code will look like Listing 18-1.

  [Serializable]
  public class Customer
  {
  }
Listing 18-1

Serializable class

  • The class will have fields or properties that will have get and set accessors, but it is the fields that are serialized, and each of the three formats will be treated differently. Binary serialization will use the public and private fields including readonly members, XML will use the public fields and properties, and JSON will use the public properties.

  • The class with the Main() method will instantiate the class.

  • The formatter class is used to serialize the object to the required format, for example, binary, XML, or JSON.

  • A file stream object is created to hold the bytes that are created after a named file has been created and opened for writing.

  • The Serialize() method is then used to serialize to a stream, and we are using the FileStream class for this, as we discussed in a previous chapter.

  • Finally, the stream must be closed.

Deserialization

Deserialization is the process of taking the serialized data, which is a stream, and returning it to an object as defined by the class. We will use FileStream to read it from the disk.

Attribute [NonSerialized]

When we serialize, there may be some values we do not want to save to the file. These values may contain sensitive data or data that can be calculated again. Adding the attribute [NonSerialized] means that during the serialization process, the relevant member, property, will not be serialized, and as such no data will be written for the nonserialized field. Listing 18-2 shows code where the [NonSerialized] attribute is used.
  [Serializable]
  public class CustomerBinary
  {
    private int customerAccountNumber;
    [NonSerialized] private int customerAge;
    private String customerName;
    private String customerAddress;
    private int customerYearsWithCompany;
  }
Listing 18-2

Serializable class with a NonSerialized field

Important Note

From March 2022 the Microsoft documentation notifies us that

Due to security vulnerabilities in BinaryFormatter , the following methods are now obsolete and produce a compile-time warning with ID SYSLIB0011:

Formatter.Serialize(Stream, Object)

Formatter.Deserialize(Stream)

IFormatter.Serialize(Stream, Object)

IFormatter.Deserialize(Stream)

An alert box labeled warning reads, the binary formatter type is dangerous and is not recommended for data processing.

We will look at serialization and deserialization using the BinaryFormatter not because we can still use it but because we will see existing application code that still uses the BinaryFormatter. As the Microsoft documentation also states

These methods are marked obsolete as part of an effort to wind down usage of BinaryFormatter within the .NET ecosystem.

So, while we code using the BinaryFormatter, we will also look at an alternative solution as suggested by the Microsoft documentation:

Stop using BinaryFormatter in your code. Instead, consider using JsonSerializer or XmlSerializer .

Let’s code some C# and build our programming muscle.

Serialization is about objects , and an object as we know is an instance of a class. So let’s create the class first, with its types, properties, methods, constructor, getters, and setters. The class will be called CustomerBinary.

Add a new project to hold the code for this chapter.
  1. 1.

    Right-click the solution CoreCSharp .

     
  2. 2.

    Choose Add.

     
  3. 3.

    Choose New Project.

     
  4. 4.

    Choose Console App from the listed templates that appear.

     
  5. 5.

    Click the Next button.

     
  6. 6.

    Name the project Chapter18 and leave it in the same location.

     
  7. 7.

    Click the Next button.

     
  8. 8.

    Choose the framework to be used, which in our projects will be .NET 6.0 or higher.

     
  9. 9.

    Click the Create button.

     
Now we should see the Chapter18 project within the solution called CoreCSharp.
  1. 10.

    Right-click the project Chapter18 in the Solution Explorer panel.

     
  2. 11.

    Click the Set as Startup Project option.

     
Notice how the Chapter18 project name has been made to have bold text, indicating that it is the new startup project and that it is the Program.cs file within it that will be executed when we run the debugging.
  1. 12.

    Right-click the Program.cs file in the Solution Explorer window.

     
  2. 13.

    Choose Rename.

     
  3. 14.

    Change the name to CustomerBinary.cs.

     
  4. 15.

    Press the Enter key.

     
  5. 16.

    Double-click the CustomerBinary.cs file to open it in the editor window.

     
  6. 17.

    Amend the code, as in Listing 18-3, with the namespace and class.

     
namespace Chapter18
{
    [Serializable]
    internal class CustomerBinary
    {
    } // End of CustomerBinary class
} // End of Chapter18 namespace
Listing 18-3

Serializable class CustomerBinary

As we are creating a CustomerBinary class that will be instantiated and become a CustomerBinary object, which we will serialize and deserialize, we do not need a Main() method. The CustomerBinary class, or more correctly the instance of the class that we will create, will be accessed from another class, which will contain a Main() method . We should be familiar with a class having fields from our previous study of classes and objects, but in this class, we will use the special attributes [Serializable] and [NonSerializable].
  1. 18.

    Amend the code, as in Listing 18-4, to add the class fields.

     
namespace Chapter18
{
  [Serializable]
  internal class CustomerBinary
  {
    /***********************************************************
    The [NonSeriazable] attribute is a 'modifier' which can be
    used in serialization. When we serialize there may be some
    values we do not want to save to the file.
    These values may contain sensitive data or data that can be
    calculated again.
    Adding the attribute [NonSerialized] means that during the
    serialization process the relevant member (type) will not be
    serialized and no data at all will be written for the member.
    The [NonSeriazable] attribute assists us with the important
    role of meeting security constraints e.g. when we do not
    want to expose private data when we serialize.
    ***********************************************************/
    private int customerAccountNumber;
    private int customerAge;
    private string customerName;
    private string customerAddress;
    private int customerYearsWithCompany;
  } // End of Customer class
} // End of Chapter18 namespace
Listing 18-4

Serializable class with fields

We will add a constructor for the class using parameter names of our choosing, which are not the same names as the fields. We will read shortly an explanation of why we are doing this for our learning.
  1. 19.

    Amend the code, as in Listing 18-5, to add the constructor.

     
  private int customerYearsWithCompany;
  /**********************************************************
  Create a constructor for the Customer class.
  The constructor will over-write the default constructor.
  The constructor is used to accept the value passed into it
  from the code used to instantiate the class.
  The values passed into the constructor are used to
  initialise the values of fields (members, variables!).
  The keyword this is used in front of the field names.
  **********************************************************/
  public CustomerBinary(int accountNumberPassedIn, int agePassedIn, string namePassedIn, string addressPassedIn, int yearsPassedIn)
    {
      customerAccountNumber = accountNumberPassedIn;
      customerAge = agePassedIn;
      customerName = namePassedIn;
      customerAddress = addressPassedIn;
      customerYearsWithCompany = yearsPassedIn;
    } // End of Customer constructor
  } // End of CustomerBinary class
} // End of Chapter18 namespace
Listing 18-5

Adding our own constructor

Info

We are now going to create getters and setters for the private members of the CustomerBinary class. As we saw in Chapter 13, private members are not accessible directly from outside the class. To make them available for reading, we use a getter method, and to make them available for changing, we use a setter method.

When using C# there is the concept of a property when we talk about getters and setters, and this property offers us a way to get or set the private fields. In other words, the property gives us the ability to read and write the private fields. The property can have what are called accessors, which are code blocks for the get accessor and the set accessor. Figure 18-1 shows the concept of a property, with its getter and setter for the private field. When creating the property for the member, we can have
  • A get and a set, where we can read the member value and change the member value

  • A get, where we can only read the member value but not change its value

  • A set, where we can only change the member’s value but not read it

A set of codes highlights the private field and the get and set accessors for the member inside the property.

Figure 18-1

Property containing a getter and a setter

Remember, we do not always need to have a get and a set for every member; it will depend on what we need. If we have a lot of members, then it would take us a little time to code each getter and setter, so remember we could use the built-in functionality of Visual Studio 2022 . When we create the getter and setter for each member, we should ask ourselves, “Where do we want them to be located in our code?” The Visual Studio 2022 “shortcut” might add them as a block where we have our cursor, it might add them as a block at the end of the code after the constructor , it might add them as a block after the members, or indeed it might add them individually under the corresponding members.

With C# there are a number of different approaches that have evolved to create the getters and setters for the members we have created. The different approaches are shown in Listings 18-6, 18-7, 18-8, and 18-9.

Approach 1: Probably the More “Dated”
class CustomerBinary
{
  // Private member, field, variable
  private int customerAccountNumber;
  // Get and set accessors for the member are inside the property
  public int CustomerAccountNumber
  {
    get
      {
        return customerAccountNumber;
      }
    set
      {
        customerAccountNumber = value;
      }
  } // End of property for the customerAccountNumber
} // End of CustomerBinary class
Listing 18-6

Get by returning the variable or assign the new value

Approach 2: Available from C# 2
class CustomerBinary
{
  /*
    A Private member, field, variable with the get and set
    being written beside the member.
    We will now have a member and its corresponding
    getter and setter.
  */
  public int CustomerAccountNumber
    {
      get;
      set;
    }
} // End of CustomerBinary class
Listing 18-7

Use get; and set; in the auto-implemented properties

Or if we only wanted a getter so that the member is readable from outside the class but only settable from within the class using the setter, we could code it as in Listing 18-8.
  class CustomerBinary
  {
    /*
     Private member, field, variable with the get and set
     being written beside the member.
     We will now have a getter but the setter is private.
    */
    public int CustomerAccountNumber
    {
      get;
      private set;
    }
  } // End of CustomerBinary class
Listing 18-8

Use get; and a private set;

We could also have a private get and public set, and we can also have properties marked as public, private, protected, internal, protected internal, or private protected.

Approach 3: Available from C# 7

In C# 7 we were introduced to the concept of expression-bodied members, which were aimed at providing a quicker or shorter way to define properties and methods. The fat arrow, =>, can therefore be used with properties that consists of only one expression. As we know
  • A get accessor does one thing: it gets the value of the member.

  • A set accessor does one thing: it sets the value of the member.

Therefore, the fat arrow, =>, can be used within our get and set accessors and it also allows us to remove the curly braces and the return.
class Customer
{
  /*
    Private member, field, variable with the get and set
    being written beside the member.
    We will now have a member and its corresponding
    getter and setter.
  */
  private int customerAccountNumber;
  public int CustomerAccountNumber
  {
    get => customerAccountNumber;
    set => customerAccountNumber = value;
  }
} // End of Customer class
Listing 18-9

Use get and set with the fat arrow =>

Yes, we might be thinking, That sounds good. It is indeed good; it might even be awesome. But we are learning to program, and it might just be a little too much for us to understand now. Either way, we will keep the declaring of get and set accessors straightforward, and when we understand the concepts, we can start using the shorter expression-bodied member style.
  1. 20.

    Amend the code, as in Listing 18-10, to add a getter and setter for each of the private properties of the CustomerBinary class.

     
    } // End of CustomerBinary constructor
    // Property for each member/field
    public int CustomerAccountNumber
    {
      get { return customerAccountNumber; }
      set { customerAccountNumber = value; }
    }// End of CustomerAccountNumber property
    public int CustomerAge
    {
      get { return customerAge; }
      set { customerAge = value; }
    }// End of CustomerAge property
    public string CustomerName
    {
      get { return customerName; }
      set { customerName = value; }
    }// End of CustomerName property
    public string CustomerAddress
    {
      get { return customerAddress; }
      set { customerAddress = value; }
    }// End of CustomerAddress property
    public int CustomerYearsWithCompany
    {
      get { return customerYearsWithCompany; }
      set { customerYearsWithCompany = value; }
    }// End of CustomerYearsWithCompany property
  } // End of Customer class
} // End of Chapter18 namespace
Listing 18-10

Getters and setters for the private properties

Serializing the Object

Now that we have the class that is to be serialized, we will create a class that will perform the serialization on the instance of the class, the object. So let’s create the class called SerializedCustomer and add the required code.
  1. 1.

    Right-click the Chapter18 project in the editor window.

     
  2. 2.

    Choose Add.

     
  3. 3.

    Choose Class

     
  4. 4.

    Change the name to SerializedCustomer.cs.

     
  5. 5.

    Click the Add button.

     
  6. 6.

    The SerializedCustomer class code will appear in the editor window. Amend the code to add the Main() method, as in Listing 18-11.

     
namespace Chapter18
{
  internal class SerializedCustomer
  {
    static void Main(string[] args)
    {
    }//End of Main() method
  } //End of SerializedCustomer class
} //End of Chapter18 namespace
Listing 18-11

Class template code with a Main() method

  1. 7.

    Amend the code, as in Listing 18-12, to add some comments about serialization. You may choose to leave these out and go to the next step.

     
namespace Chapter18
{
  internal class SerializedCustomer
  {
  /*
  Serialization is a process to convert an object into a stream
  of bytes so that the bytes can be written into a file or
  elsewhere.
  We will normally do this so the serialized data can be used to
  store the data in a database or for sending it across a network
  e.g. to a message queue to form part of a transaction process.
  The byte stream created for XML and JSON is platform independent, it is an object serialized on one platform that can be de serialized on a
  different platform.
  */
    static void Main(string[] args)
    {
    }//End of Main() method
Listing 18-12

Add comments

We will now create an instance of the CustomerBinary class, the object, passing to the constructor the initial values for the properties. We will create this code inside the Main() method .
  1. 8.

    Amend the code, as in Listing 18-13.

     
    static void Main(string[] args)
    {
      /*********************************************************
      Create an instance of the Customer class passing in the
      initial values that will be used to set the values of the
      members (fields) in the Customer object being created.
      As a matter of good practice we will use a .ser extension
      for the file name.
      *********************************************************/
        CustomerBinary myCustomerObject = new CustomerBinary(123456, 45, "Gerry", "1 Any Street, Belfast, BT1 ANY", 10);
Listing 18-13

Instantiate the class, passing it values

We will now create an instance of the BinaryFormatter class. We will see that this formatter is used to give us the method we need when we wish to serialize the object.
  1. 9.

    Amend the code, as in Listing 18-14.

     
    Customer myCustomerObject = new Customer(123456, 45, "Gerry", "1 Any Street, Belfast, BT1 ANY", 10);
      IFormatter formatterForTheClass = new BinaryFormatter();
    }//End of Main() method
Listing 18-14

Instantiate the BinaryFormatter class, which we will use

  1. 10.

    Add the code in Listing 18-15, to import the required namespaces for the BinaryFormatter and IFormatter.

     
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
Listing 18-15

Add the required imports

When we studied file handling in Chapter 16, we saw that the FileStream class could be used to read a file or write to a file. In instantiating the FileStream class, the created object can have four parameters:
  • filename, the name , path, and extension of the file that will hold the data

    For example, CustomerSerializedData.ser

  • file mode, the mode in which to open the file

    For example, Open, Create, Append

  • file access, the access given to this file

    For example, Read, Write, ReadWrite

Now we will create a file stream, which will be used to create the file, so we will be using the property Create and we will use the property Write so we can write to the newly created file.
  1. 11.

    Amend the code, as in Listing 18-16.

     
      IFormatter formatterForTheClass = new BinaryFormatter();
      Stream streamToHoldTheData = new FileStream("CustomerSerializedData.ser", FileMode.Create, FileAccess.Write);
    }//End of Main() method
Listing 18-16

Use FileStream to create the file that will hold the serialized data

Now we will call the Serialize() method of the formatter class, passing it the stream that will hold the data and the object to be serialized. We will then close the file stream.
  1. 12.

    Amend the code, as in Listing 18-17.

     
      Stream streamToHoldTheData =
        new FileStream("CustomerSerializedData.ser",
        FileMode.Create, FileAccess.Write);
      formatterForTheClass.Serialize(streamToHoldTheData, myCustomerObject);
      streamToHoldTheData.Close();
    }//End of Main() method
Listing 18-17

Call the Serialize() method of the FileStream class

  1. 13.

    Click the File menu.

     
  2. 14.

    Choose Save All.

     
  3. 15.

    Click the Debug menu.

     
  4. 16.

    Choose Start Without Debugging.

     
  5. 17.

    Press any key to close the console window that appears.

     
  6. 18.

    In the Solution Explorer, click the Chapter18 project.

     
  7. 19.

    Click the Show All Files icon, as shown in Figure 18-2.

     

A window titled solution explorer exhibits various icons, show all files is highlighted and has the following text, Solution CoreCSharp 15 of 15 projects.

Figure 18-2

Show All Files

  1. 20.

    Click the Refresh button , as shown in Figure 18-3.

     

A window labeled solution explorer highlights the sync with the active document icon on the toolbar.

Figure 18-3

Refresh or sync

The serialized file should be displayed in the net6.0 folder, which is in the Debug folder, which is inside the bin folder, as shown in Figure 18-4.

A navigation pane depicts the directory of chapter 18 with expanded folders and highlights the serialized customer file in the net 6.0 folder.

Figure 18-4

Serialized file has been written

Brilliant! We have a serialized file. The serialized file contains the state of the instance class; in other words, it has the customer details that we supplied when we used the constructor.

Deserializing the Serialized File to a Class

  1. 1.

    Right-click the Chapter18 project in the Solution Explorer window.

     
  2. 2.

    Choose Add.

     
  3. 3.

    Choose Class

     
  4. 4.

    Change the name to DeserializedFileToCustomerObject.cs.

     
  5. 5.

    Click the Add button.

     
  6. 6.

    The DeserializedFileToCustomerObject class code will appear in the editor window.

     
  7. 7.

    Now add a Main() method as shown in Listing 18-18.

     
namespace Chapter18
{
  internal class DeserializedFileToCustomerObject
  {
    static void Main(string[] args)
    {
    }//End of Main() method
  } //End of DeserializedFileToCustomerObject class
} //End of Chapter18 namespace
Listing 18-18

Class with the Main() method

  1. 8.

    Amend the code, as in Listing 18-19, to add comments about deserialization. You may choose to leave these out and go to the next step.

     
internal class DeserializedFileToCustomerObject
{
/*
De-serialization is the process of taking the serialized data
(file) and returning it to an object as defined by the class.
When we serialized, there may be some values we do not want to
  save to the file. These values may contain sensitive data or
  data that can be calculated again. Adding the attribute
  [NonSerialized] means that during the serialization process
  the relevant member (field) will not be  serialized and as
  such the data will be ignored and no data for the field
  will be written.
  */
    static void Main(string[] args)
Listing 18-19

Comments about deserialization

  1. 9.

    Amend the code, as in Listing 18-20, to create an instance of the CustomerBinary class, the object, and set it null.

     
    static void Main(string[] args)
    {
      CustomerBinary myCustomer = null;
    }//End of Main() method
Listing 18-20

Create an instance of the CustomerBinary class

In the serialization code, we created an instance for the BinaryFormatter and based it on the interface IFormatter . Remember we said program to an interface. In this example however, we will create an instance of the BinaryFormatter that is based on the BinaryFormatter class, just to show a different approach. Both approaches are perfectly acceptable.

We will now create an instance of the BinaryFormatter so that later we can use the method that allows us to deserialize the object.
  1. 10.

    Amend the code, as in Listing 18-21.

     
    static void Main(string[] args)
    {
      CustomerBinary myCustomer = null;
      BinaryFormatter binaryFormatterForTheClass = new BinaryFormatter();
    }//End of Main() method
Listing 18-21

Create an instance of the BinaryFormatter

  1. 11.

    Add the code in Listing 18-22, to import the required namespace for the BinaryFormatter.

     
using System.Runtime.Serialization.Formatters.Binary;
namespace Chapter18
Listing 18-22

Add the required import

We will now create an instance of the FileStream , giving it the serialized filename and the Open and Read properties for its parameters.
  1. 12.

    Amend the code, as in Listing 18-23.

     
      BinaryFormatter binaryFormatterForTheClass =
        new BinaryFormatter();
      FileStream fileStreamToHoldTheData = new FileStream("CustomerSerializedData.ser",FileMode.Open, FileAccess.Read);
    }//End of Main() method
Listing 18-23

Create a FileStream to allow file opening and reading

In the serialization code , we did not use a try catch block, which could have been a problem if there was an error. We should always use a try catch block when working with files, and we saw this when we looked at exception handling. In the code in Listing 18-24, we will use a try catch block while we attempt to read the serialized file we created during the serialization process.
  1. 13.

    Amend the code, as in Listing 18-24.

     
      FileStream fileStreamToHoldTheData =
        new FileStream("CustomerSerializedData.ser",
        FileMode.Open, FileAccess.Read);
      try
      {
       using (fileStreamToHoldTheData)
       {
        myCustomer =  (CustomerBinary)binaryFormatterForTheClass.Deserialize(fileStreamToHoldTheData);
       } // End of the using block
      } // End of the try block
      catch
      {
      }// End of the catch block
    }//End of Main() method
Listing 18-24

Try catch block while reading the serialized file

Now that we have the deserialized data in a CustomerBinary object, we can display it using the property method of each member. In reality we will only use the get accessor to get the value and then display it along with a relevant message. The format for using the get accessor is to simply call the property of the member, for example:
  • CustomerName calls the CustomerName property that will return the customerName private field.

  • CustomerAge uses the get accessor of the customerAge member.

The format for using the set accessor, if we were to use it, is to simply assign the property of the member to a value, for example:
  • CustomerName = "WHO" would use the set accessor of the customerName member.

  • CustomerAge = 21 would use the set accessor of the customerAge member.

So let us now display the details obtained from the deserialized file. We will display each member of the deserialized CustomerBinary class to ensure that it contains the data that was written to the file during the serialization process.
  1. 14.

    Amend the code, as in Listing 18-25.

     
try
{
 using (fileStreamToHoldTheData)
 {
  myCustomer =
(Customer)binaryFormatterForTheClass.Deserialize(fileStreamToHoldTheData);
Console.WriteLine("Customer Details");
Console.WriteLine("Customer Name: " + myCustomer.CustomerName);
Console.WriteLine("Customer Age: " + myCustomer.CustomerAge);
Console.WriteLine("Customer Account No: " + myCustomer.CustomerAccountNumber);
Console.WriteLine("Customer Address: " + myCustomer.CustomerAddress);
Console.WriteLine("Customer Years a Customer: " + myCustomer.CustomerYearsWithCompany);
        } // End of the using block
      } // End of the try block
      catch
      {
      }// End of the catch block
Listing 18-25

Display the details of the deserialized CustomerBinary class

  1. 15.

    Amend the code, as in Listing 18-26, to add a message in the catch block.

     
      catch
      {
        Console.WriteLine("Error creating the Customer from the serialized file");
      }// End of the catch block
Listing 18-26

Catch block message

  1. 16.

    Right-click the Chapter18 project in the Solution Explorer panel.

     
  2. 17.

    Choose Properties.

     
  3. 18.

    Set the Startup object to be the Chapter18.DeserializedFileToCustomerObject in the drop-down list, as shown in Figure 18-5.

     

A section of a window labeled startup object depicts a drop-down list, with chapter 18, deserialized file to customer object highlighted and selected.

Figure 18-5

Set the startup program

  1. 19.

    Close the Properties window.

     
  2. 20.

    Click the File menu.

     
  3. 21.

    Choose Save All.

     
  4. 22.

    Click the Debug menu.

     
  5. 23.

    Choose Start Without Debugging.

     
The console window will appear, as shown in Figure 18-6, and display the CustomerBinary object details , confirming that the deserialization has been successful.

A console window depicts the deserialized customer data, name, age, account number, address, and years as a customer, with the executable file path.

Figure 18-6

Details from the deserialized file

  1. 24.

    Press any key to close the console window that appears.

     

Access Modifier [NonSerialized]

At the start of the chapter, we read that when we serialize, there may be some values we do not want to save to the file because the values may contain sensitive data or data that can be calculated again. We read that by adding the [NonSerialized] attribute to a field, the data will not be serialized. Now we will code using the [NonSerialized] attribute on the customerAge field, which has been designated as “secret,” and confirm that the data is not written to the serialized file.
  1. 25.

    Open the CustomerBinary.cs class.

     
  2. 26.

    Amend the code, as in Listing 18-27.

     
    private int customerAccountNumber;
    [NonSerialized] private int customerAge;
    private String customerName;
    private String customerAddress;
    private int customerYearsWithCompany;
Listing 18-27

NonSerializable member

  1. 27.

    Click the File menu.

     
  2. 28.

    Choose Save All.

     

Now set the SerializedCustomer.cs file as the Startup object and run the code again to create the new version of the CustomerSerializedData.ser file with the default value being written.

Now set the Startup object back to the DeserializedFileToCustomerObject.
  1. 29.

    Click the Debug menu.

     
  2. 30.

    Choose Start Without Debugging.

     
The console window will appear and display the CustomerBinary object details, as shown in Figure 18-7, confirming that the deserialization has been successful and that age has the default value.

A console window depicts the deserialized file, and highlights the nonserialized private int customer age, with a default value of zero.

Figure 18-7

Details from the deserialized file with age nonserialized

Brilliant! We can serialize and deserialize a class, or strictly speaking the instance of the class. But, as we were cautioned at the start of the chapter, due to security vulnerabilities in BinaryFormatter, the methods are now obsolete, so we will now look at serialization in a different way, using XML.

Serialization Using XML

When we perform XML serialization , the serialization only applies to public fields and property values of an object, and the serialization does not include any type information no methods or private fields will be serialized. If we need to serialize all private fields, public fields, and properties of an object, then we can use the DataContractSerializer rather than XML serialization, but this will not be covered in this book.

When serializing an object to XML, certain rules apply:

  • The class needs to have a default constructor. In the CustomerBinary class that we created earlier, we coded our own constructor, thereby overwriting the default constructor. This means that we will need to add a default constructor, a constructor that is parameterless.

  • Only the appropriate public fields and properties of the class will be serialized.

  1. 1.

    Right-click the CustomerBinary.cs file in the Solution Explorer panel.

     
  2. 2.

    Choose Copy.

     
  3. 3.

    Right-click the Chapter18 project in the Solution Explorer panel.

     
  4. 4.

    Choose Paste.

     
  5. 5.

    Right-click the new CustomerBinary – Copy.cs file.

     
  6. 6.

    Choose Rename and rename the file as CustomerXML.cs.

     
Now we must ensure that the class name and constructor name are the same, CustomerXML , and that the class has an access modifier of public.
  1. 7.

    Amend the CustomerXML file as in Listing 18-28, which has had the comments removed for ease of reading.

     
using System;
namespace Chapter18
{
  [Serializable]
  public class CustomerXML
  {
   private int customerAccountNumber;
   [NonSerialized] private int customerAge;
   private string customerName;
   private string customerAddress;
   private int customerYearsWithCompany;
  public CustomerXML(int accountNumberPassedIn, int agePassedIn,
  String namePassedIn, String addressPassedIn, int yearsPassedIn)
    {
Listing 18-28

Change class name and constructor name to CustomerXML

  1. 8.

    Amend the file to include a default constructor, as in Listing 18-29.

     
 public class CustomerXML
 {
  private int customerAccountNumber;
  [NonSerialized] private int customerAge;
  private string customerName;
  private string customerAddress;
  private int customerYearsWithCompany;
  public CustomerXML()
  {
  }
  public CustomerXML(int accountNumberPassedIn, int agePassedIn,
  String namePassedIn, String addressPassedIn, int yearsPassedIn)
  {
Listing 18-29

Added a default constructor

Now we will make all fields public and remove the accessors.
  1. 9.

    Amend the customerAge field to remove the [NonSerializable] and make all the fields public, as in Listing 18-30.

     
    public int customerAccountNumber;
    public int customerAge;
    public string customerName;
    public string customerAddress;
    public int customerYearsWithCompany;
    public CustomerXML()
    {
    }
Listing 18-30

Make all fields public

In Listing 18-31 that follows, the comment has been changed to make the code more relevant to XML serialization, but we do not need to change the comment in our copied file if we do not wish to do so.
  1. 10.

    Amend the code to remove the unnecessary accessors, as in Listing 18-31.

     
using System;
namespace Chapter18
{
    [Serializable]
    public class CustomerXML
    {
     /***********************************************************
      The fields are public because in XML serialization only the
      public fields and properties will be serialized
     ***********************************************************/
      public int customerAccountNumber;
      public int customerAge;
      public string customerName;
      public string customerAddress;
      public int customerYearsWithCompany;
    public CustomerXML()
    {
    }
    /**********************************************************
    Create a constructor for the Customer class.
    The constructor will over-write the default constructor.
    The constructor is used to accept the value passed into it
    from the code used to instantiate the class.
    The values passed into the constructor are used to
    initialise the values of fields (members, variables!).
    The keyword this is used in front of the field names.
    **********************************************************/
    public CustomerXML(int accountNumberPassedIn, int agePassedIn, string namePassedIn, string addressPassedIn, int yearsPassedIn)
    {
      customerAccountNumber = accountNumberPassedIn;
      customerAge = agePassedIn;
      customerName = namePassedIn;
      customerAddress = addressPassedIn;
      customerYearsWithCompany = yearsPassedIn;
    } // End of Customer constructor
  } // End of CustomerXML class
} // End of Chapter18 namespace
Listing 18-31

Class with getters and setters removed

Now that the class being serialized to XML has the required elements, a default constructor, and public fields, we can create the serialize and deserialize code, which we will do in a similar way to binary serialization, using two separate classes.

Creating the Serialization Code

Before writing any code , we will look at the steps to be followed in order to create the code required to serialize the class object. These steps will be similar to the code we used in binary serialization:
  • Create an instance of the CustomerXML class using our custom constructor to pass values to the fields in the class:

CustomerXML myCustomerObject =
new CustomerXML(123456, 45, "Gerry", "1 Any Street, " +
"Belfast, BT1 ANY", 10);
  • Create an instance of the XmlSerializer informing it that we are using a class of type CustomerXML. This is like binary serialization when we used the BinaryFormatter or IFormatter:

XmlSerializer myXMLSerialiser = new XmlSerializer(typeof(CustomerXML));
  • Create an instance of StreamWriter and pass it the name of the file we wish to add the XML to, in our case CustomerSerialisedData.xml:

StreamWriter myStreamWriter = new StreamWriter("CustomerSerialisedData.xml");
  • Call the serialize method of the XmlSerializer , passing it the StreamWriter name and the instance of the object to be serialized:

myXMLSerialiser.Serialize(myStreamWriter, myCustomerObject);
  • Close the StreamWriter instance:

myStreamWriter.Close();
Now we can code the steps.
  1. 11.

    Right-click the Chapter18 project in the Solution Explorer.

     
  2. 12.

    Choose Add.

     
  3. 13.

    Choose Class.

     
  4. 14.

    Name the class XMLSerialisation.cs .

     
  5. 15.

    Click the Add button.

     
  6. 16.

    Amend the code, as in Listing 18-32, to add a Main() method and code the steps needed to serialize to XML.

     
using System.Xml.Serialization;
namespace Chapter18
{
  public class XMLSerialisation
  {
    static void Main(string[] args)
    {
      /*********************************************************
      Create an instance of the Customer class passing in the
      initial values that will be used to set the values of the
      members (fields) in the Customer object being created.
      As a matter of good practice we will use a .ser extension
      for the file name.
      *********************************************************/
      CustomerXML myCustomerObject =
          new CustomerXML(123456, 45, "Gerry", "1 Any Street, " +
          "Belfast, BT1 ANY", 10);
      // Create an instance of the XmlSerializer
      XmlSerializer myXMLSerialiser = new
                     XmlSerializer(typeof(CustomerXML));
      //Create an instance of the StreamWriter using the xml file
      StreamWriter myStreamWriter = new
                     StreamWriter("CustomerSerialisedData.xml");
      // Serialize the object using the StreamWriter
     myXMLSerialiser.Serialize(myStreamWriter, myCustomerObject);
      // Close the StreamWriter
      myStreamWriter.Close();
    } // End of Main() method
  } // End of XMLSerialisation class
} // End of Chapter18 namespace
Listing 18-32

Adding the code to serialize the CustomerXML object

  1. 17.

    Right-click the Chapter18 project in the Solution Explorer panel.

     
  2. 18.

    Choose Properties.

     
  3. 19.

    Set the Startup object to be the XMLSerialisation in the drop-down list.

     
  4. 20.

    Exit the Properties window.

     
  5. 21.

    Click the File menu.

     
  6. 22.

    Choose Save All.

     
  7. 23.

    Click the Debug menu.

     
  8. 24.

    Choose Start Without Debugging.

     
  9. 25.

    Press the Enter key to close the console window.

     
The serialized file should be displayed in the net6.0 folder, which is in the Debug folder, which is inside the bin folder, as shown in Figure 18-8.

A navigation pane depicts the chapter 18 directory with expanded folders and highlights the customer serialized data X M L file in the net 6.0 folder.

Figure 18-8

XML serialized file

Brilliant! We have a serialized file containing XML, as shown in Listing 18-33. The XML file contains the state of the instance class; in other words, it has the CustomerXML details that we supplied when we used the constructor.
<?xml version="1.0" encoding="UTF-8"?>
<CustomerXML xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <customerAccountNumber>123456</customerAccountNumber> <customerAge>45</customerAge>
<customerName>Gerry</customerName>
<customerAddress>1 Any Street, Belfast, BT1 ANY</customerAddress> <customerYearsWithCompany>10</customerYearsWithCompany>
</CustomerXML>
Listing 18-33

The XML data from the CustomerSerialisedData.xml file execution

Creating the Deserialization Code

Before writing any code , we will look at the steps to be followed in order to create the code required to deserialize, convert the XML to a class object. These steps will be similar to the code we used in binary deserialization:
  • Create an instance of the CustomerXML class setting its value to null:

CustomerXML myCustomer = null;
  • Create an instance of the XmlSerializer informing it that we are using a class of type CustomerXML, same code as in the serialization class:

XmlSerializer myXMLSerialiser = new XmlSerializer(typeof(CustomerXML));
  • Create an instance of StreamReader and pass it the name of the file we wish to read the XML from, in our case CustomerSerialisedData.xml:

StreamReader myStreamReader = new StreamReader("CustomerSerialisedData.xml");
  • Call the deserialize method of the XmlSerializer, passing it the StreamReader name, then cast the returned value to a CustomerXML object, and assign this to the myCustomer instance of the CustomerXML object we created in the first step:

try
{
myCustomer = (CustomerXML)mySerialser.Deserialize(myStreamReader);
} // End of the try block
catch
{
Console.WriteLine("Error creating the Customer" +
     " from the serialised file");
}// End of the catch block
  • As the deserialization may cause an exception, we will add the code within a try catch block.

  • Now we will display the details of the object created from the XML file by calling the accessor from the myCustomer instance of the CustomerXML class, for example, customer name would be displayed using the code

Console.WriteLine("Customer Name: " +
      myCustomer.customerName);
We are not using accessors, so be careful with the field name it has no capital letter.
  • Close the StreamReader instance:

myStreamReader.Close();
Now we can code the steps.
  1. 26.

    Right-click the Chapter18 project in the Solution Explorer panel.

     
  2. 27.

    Choose Add.

     
  3. 28.

    Choose Class.

     
  4. 29.

    Name the class XMLDeserialisation.cs.

     
  5. 30.

    Click the Add button.

     
  6. 31.

    Amend the code, as in Listing 18-34, to add a Main() method and code the steps needed to deserialize to a CustomerXML object and then display the details of the customer.

     
using System.Xml.Serialization;
namespace Chapter18
{
  internal class XMLDeserialisation
  {
    static void Main(string[] args)
    {
    /*
    De-serialisation is the process of taking the serialized data
    (file) and returning it to an object as defined by the class.
    */
      // Create an instance of the Customer class
      CustomerXML myCustomer = null;
      // Create an instance of the XmlSerializer
XmlSerializer mySerialser = new XmlSerializer(typeof(CustomerXML));
    // Create an instance of the StreamReader using the xml file
      StreamReader myStreamReader = new StreamReader("CustomerSerialisedData.xml");
 try
 {
  myCustomer = (CustomerXML)mySerialser.Deserialize(myStreamReader);
  Console.WriteLine("Deserialize completed"); ;
  Console.WriteLine("Customer Details");
  Console.WriteLine("Customer Name: " + myCustomer.customerName);
  Console.WriteLine("Customer Age: " + myCustomer.customerAge);
  Console.WriteLine("Customer Account No: " +
          myCustomer.customerAccountNumber);
  Console.WriteLine("Customer Address: " +
          myCustomer.customerAddress);
  Console.WriteLine("Customer Years a Customer: " +
          myCustomer.customerYearsWithCompany);
 } // End of the try block
 catch
  {
    Console.WriteLine("Error creating the Customer" +
      " from the serialised file");
  }// End of the catch block
  myStreamReader.Close();
} // End of Main() method
  } // End of XMLDeserialisation class
} // End of Chapter18 namespace
Listing 18-34

Adding the code to deserialize XML to a CustomerXML object

  1. 32.

    Right-click the Chapter18 project in the Solution Explorer panel.

     
  2. 33.

    Choose Properties.

     
  3. 34.

    Set the Startup object to be the XMLDeserialisation in the drop-down list.

     
  4. 35.

    Exit the Properties window.

     
  5. 36.

    Click the File menu.

     
  6. 37.

    Choose Save All.

     
  7. 38.

    Click the Debug menu.

     
  8. 39.

    Choose Start Without Debugging.

     
The deserialized object should be displayed as shown in Figure 18-9.

A console window depicts the X M L data deserialized to a customer X M L object, with 45 as the customer age value.

Figure 18-9

XML file returned as a CustomerXML object

  1. 40.

    Press the Enter key to close the console window.

     

Brilliant! We can now serialize and deserialize C# objects in two different ways. XML is widely used in the commercial environment, but there is also another widely used format called JSON, and we will now complete our chapter by looking at how we can serialize and deserialize using the JSON format.

Serialization Using JSON

JSON is an acronym for JavaScript Object Notation and it is a widely used format to represent information. JSON can represent our data in a very easy-to-read format. Many applications in the commercial world will use JSON to represent data and transfer it as part of Hypertext Transfer Protocol (HTTP) requests and responses. But remember the important note at the start of the chapter where we saw the quote from the Microsoft site:

Stop using BinaryFormatter in your code. Instead, consider using JsonSerializer or XmlSerializer .

So now we will look at the JSON option. There are different “tools” we can use to serialize with JSON, but we will use the System.Text.Json namespace since it was specially created by Microsoft to be included as a built-in library from .NET Core 3.0. By using this library, we will not need to use external libraries, and we will have access to methods such as Serialize() , Deserialize() , SerializeAsync() , and DeserializeAsync() . All this is all we need.
  1. 1.

    Right-click the Chapter18 project in the Solution Explorer panel.

     
  2. 2.

    Choose Add.

     
  3. 3.

    Choose Class.

     
  4. 4.

    Name the class JSONSerialisation.cs.

     
  5. 5.

    Click the Add button.

     
  6. 6.

    Right-click the Chapter18 project in the Solution Explorer panel.

     
  7. 7.

    Choose Add.

     
  8. 8.

    Choose Class.

     
  9. 9.

    Name the class CustomerJSON.cs.

     
  10. 10.

    Click the Add button.

     
We will amend the CustomerJSON class to add the members with a get and set attached and a constructor to set the member values. We will also use the attribute [JsonIgnore] so that the customerAge field is not serialized. [JsonIgnore] therefore is the JSON equivalent of the [NonSerialized] attribute, which we used in a previous example.
  1. 11.

    Double-click the CustomerJSON file to open it in the editor window.

     
  2. 12.

    Amend the code as shown in Listing 18-35 to create the class and use a different way to get and set the member values.

     
using System.Text.Json.Serialization;
namespace Chapter18
{
  public class CustomerJSON
  {
    /***********************************************************
    The [JsonIgnore] attribute is a 'modifier' which can be
    used in JSON serialization to ensure the member (type) will
    not be serialized.
    ***********************************************************/
    public int CustomerAccountNumber { get; set; }
    [JsonIgnore]
    public int CustomerAge { get; set; }
    public String CustomerName { get; set; }
    public String CustomerAddress { get; set; }
    public int CustomerYearsWithCompany { get; set; }
    /**********************************************************
    Create a constructor for the Customer class.
    The constructor will over-write the default constructor.
    The constructor is used to accept the value passed into it
    from the code used to instantiate the class.
    The values passed into the constructor are used to
    initialise the values of fields (members, variables!).
    The keyword this is used in front of the field names.
    **********************************************************/
    public CustomerJSON(int customerAccountNumber,
      int customerAge, String customerName,
      String customerAddress, int customerYearsWithCompany)
    {
      this.CustomerAccountNumber = customerAccountNumber;
      this.CustomerAge = customerAge;
      this.CustomerName = customerName;
      this.CustomerAddress = customerAddress;
      this.CustomerYearsWithCompany = customerYearsWithCompany;
    } // End of Customer constructor
  } // End of CustomerJSON class
} // End of Chapter18 namespace
Listing 18-35

Class that has auto-implemented properties and [JsonIgnore]

We will amend the JSONSerialisation class :
  • Add a Main() method

  • Inside the Main() method , we will create an instance of the class CustomerJSON by passing values to the constructor.

  • Call the JSON Serialize() method , passing it the instance of our CustomerJSON and assigning the returned JSON to a string variable called jsonString.

  • Display the returned JSON to the console.

Let’s code these steps as shown in Listing 18-36.
  1. 13.

    Amend the code, as in Listing 18-36, to add the Main() method and the class instantiation and perform the assignment.

     
using System.Text.Json;
namespace Chapter18
{
  internal class JSONSerialisation
  {
    public static  void Main()
    {
      CustomerJSON myCustomer =
        new CustomerJSON(123456, 45, "Gerry",
        "1 Any Street, Belfast, BT1 ANY", 10);
      //Serialize
      string jsonString =
        JsonSerializer.Serialize<CustomerJSON>(myCustomer);
      Console.WriteLine(jsonString);
    } // End of Main() method
  } // End of JSONSerialisation class
} // End of Chapter18 namespace
Listing 18-36

Adding a Main() method and other code

  1. 14.

    Click the File menu.

     
  2. 15.

    Choose Save All.

     
  3. 16.

    Right-click the Chapter18 project in the Solution Explorer panel.

     
  4. 17.

    Choose Properties.

     
  5. 18.

    Set the Startup object to be the JSONSerialisation in the drop-down list.

     
  6. 19.

    Exit the Properties window.

     
  7. 20.

    Click the Debug menu.

     
  8. 21.

    Choose Start Without Debugging.

     
The console window will appear and display the object details in JSON format, as shown in Figure 18-10, confirming that the serialization has been successful with the customer age not included.

A console window depicts the J S O N serialization of the customer details, excluding the customer's age.

Figure 18-10

JSON format from the serialization displayed

  1. 22.

    Press the Enter key to close the console window.

     

Great, but we haven’t written the JSON to a file. Obviously, we could have created the code for that within the code shown in Listing 18-36, but we will achieve it through a new method that we will create, and this will allow us to look at serializing using an asynchronous approach.

We will amend the class to

  • Add an async method called CreateJSON() , which will accept a CustomerJSON object.

  • Declare a string, assigning it the name of the file to be used.

  • Use an instance of the FileStream class to create the file this is our stream.

  • Call the SerializeAsync() method , passing it the stream and the instance of our CustomerJSON object.

  • Dispose of the unmanaged resource of the stream.

  • Display the contents of the JSON file to the console.

  1. 23.

    Amend the code, as in Listing 18-37, to add the new method outside the Main() method but inside the namespace.

     
    } // End of Main() method
    public static async Task CreateJSON(CustomerJSON myCustomer)
    {
      string fileName = "Customer.json";
      using FileStream createStream = File.Create(fileName);
      await JsonSerializer.SerializeAsync(createStream, myCustomer);
      await createStream.DisposeAsync();
      Console.WriteLine(File.ReadAllText(fileName));
    } // End of CreateJSON() method
  } // End of JSONSerialisation class
} // End of Chapter18 namespace
Listing 18-37

Adding a CreateJSON() method

  1. 24.

    Amend the code, as in Listing 18-38, to call the CreateJSON() method from within the Main() method, passing it the myCustomer object. The Main() method will need to be async so that we can await properly.

     
    public static async Task Main()
    {
      CustomerJSON myCustomer = new CustomerJSON(123456, 45, "Gerry", "1 Any Street, Belfast, BT1 ANY", 10);
      //Serialize
      string jsonString = JsonSerializer.Serialize<CustomerJSON>(myCustomer);
      Console.WriteLine(jsonString);
      await CreateJSON(myCustomer);
    } // End of Main() method
    public static async Task CreateJSON(CustomerJSON myCustomer)
    {
Listing 18-38

Adding a call to the CreateJSON() method

  1. 25.

    Click the File menu.

     
  2. 26.

    Choose Save All.

     
  3. 27.

    Click the Debug menu.

     
  4. 28.

    Choose Start Without Debugging.

     
The console window will appear and display the object details in JSON format, as shown in Figure 18-10.
  1. 29.

    Press the Enter key to close the console window.

     
We also wrote the code so that a file was created, so we should also look in the net6.0 folder, inside the Debug folder, inside the bin folder, and see that the Customer.json file has been created.
  1. 30.

    Double-click the Customer.json file to open it in the editor window.

     
A raw form as displayed in Visual Studio 2022 is shown in Listing 18-39, while a “pretty” form of the Customer.json is shown in Listing 18-40.
{"customerAccountNumber":123456,"customerName":"Gerry","customerAddress":"1 Any Street, Belfast, BT1 ANY","customerYearsWithCompany":10}
Listing 18-39

JSON file contents as shown in Visual Studio 2022

{
  "CustomerAccountNumber": 123456,
  "CustomerName": "Gerry",
  "CustomerAddress": "1 Any Street, Belfast, BT1 ANY",
  "CustomerYearsWithCompany": 10
}
Listing 18-40

JSON file contents in “pretty” format

Notice CustomerAge was a [JsonIgnore] field so it does not appear.

We will amend the class to

  • Add a method called ReadJSON().

  • Declare a string, assigning it the name of the file to be used.

  • Use an instance of the FileStream class to open and read the file this is our stream.

  • Call the DeSerialize() method, passing it the stream.

  • Display the contents of the JSON file to the console.

  1. 31.

    Amend the code, as in Listing 18-41, to add the new method outside the Main() method but inside the namespace.

     
} // End of CreateJSON() method
public static void ReadJSON()
{
 string fileName = "Customer.json";
 using FileStream myStream = File.OpenRead(fileName);
 CustomerJSON myCustomer = JsonSerializer.Deserialize<CustomerJSON>(myStream);
Console.WriteLine("Customer Details");
Console.WriteLine("Customer Name: " + myCustomer.CustomerName);
Console.WriteLine("Customer Age: " + myCustomer.CustomerAge);
Console.WriteLine("Customer Account No: " + myCustomer.CustomerAccountNumber);
Console.WriteLine("Customer Address: " + myCustomer.CustomerAddress);
Console.WriteLine("Customer Years a Customer: " +
myCustomer.CustomerYearsWithCompany);
 } // End of ReadJSON() method
  } // End of JSONSerialisation class
} // End of Chapter18 namespace
Listing 18-41

Adding a ReadJSON() method

  1. 32.

    Amend the code, as in Listing 18-42, to call the ReadJSON() method.

     
      await CreateJSON(myCustomer);
      ReadJSON();
    } // End of Main() method
Listing 18-42

Call the ReadJSON() method

  1. 33.

    Click the File menu.

     
  2. 34.

    Choose Save All.

     
  3. 35.

    Click the Debug menu.

     
  4. 36.

    Choose Start Without Debugging.

     
The console window will appear and display the object details, as shown in Figure 18-11.

A console window depicts the customer J S O N object, with J S O N ignore, in square brackets, attribute used on the customer age with a value of 0.

Figure 18-11

Deserialized object showing the [JsonIgnore] attributed worked

  1. 37.

    Press the Enter key to close the console window.

     

Chapter Summary

So, finishing this chapter on object serialization and deserialization, we should be familiar with the use of a class and the instantiation of the class to create an object. We realize that our object, instantiated class, will be treated like all the other objects we have in our code when the application is closed. When we close our application, our object and every other object will not be accessible. We saw in Chapter 16 that we could persist data by writing it to a text file, which is accessible to us after the application stops. So we can now think of serialization as a method to write the object with its real data to a file so we can reuse it at a later stage. We may want to transfer the object, with its state, to another computer over the network or Internet, and through serialization we can use different formats such as binary data, XML data, and JSON data. We also saw that deserialization allows us to reverse the process carried out by serialization, which means converting our serialized byte stream back to our object.

Wow, what an achievement. This is not basic coding. We are doing some wonderful things with our C# code. We should be immensely proud of the learning to date. In finishing this chapter, we have increased our knowledge further and we are advancing to our target.

An illustration of a round target with concentric circles and highlighted inner circles. The text below reads, our target is getting closer.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.103.10