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 inBinaryFormatter, 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)
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:
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.
Right-click the solution CoreCSharp
.
2.
Choose Add.
3.
Choose New Project.
4.
Choose Console App from the listed templates that appear.
5.
Click the Next button.
6.
Name the project Chapter18 and leave it in the same location.
7.
Click the Next button.
8.
Choose the framework to be used, which in our projects will be .NET 6.0 or higher.
9.
Click the Create button.
Now we should see the Chapter18 project within the solution called CoreCSharp.
10.
Right-click the project Chapter18 in the Solution Explorer panel.
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.
12.
Right-click the Program.cs file in the Solution Explorer window.
13.
Choose Rename.
14.
Change the name to CustomerBinary.cs.
15.
Press the Enter key.
16.
Double-click the CustomerBinary.cs file to open it in the editor window.
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].
18.
Amend the code, as in Listing 18-4, to add the class 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.
19.
Amend the code, as in Listing 18-5, to add the constructor.
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
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.
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.
Right-click the Chapter18 project in the editor window.
2.
Choose Add.
3.
Choose Class
4.
Change the name to SerializedCustomer.cs.
5.
Click the Add button.
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
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
.
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.
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
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.
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.
Call the Serialize() method
of the FileStream class
13.
Click the File menu.
14.
Choose Save All.
15.
Click the Debug menu.
16.
Choose Start Without Debugging.
17.
Press any key to close the console window that appears.
18.
In the Solution Explorer, click the Chapter18 project.
19.
Click the Show All Files icon, as shown in Figure 18-2.
20.
Click the Refresh button
, as shown in Figure 18-3.
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.
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.
Right-click the Chapter18 project in the Solution
Explorer window.
2.
Choose Add.
3.
Choose Class
4.
Change the name to DeserializedFileToCustomerObject.cs.
5.
Click the Add button.
6.
The DeserializedFileToCustomerObject class code will appear in the editor window.
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
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
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.
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.
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.
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
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
16.
Right-click the Chapter18 project in the Solution Explorer panel.
17.
Choose Properties.
18.
Set the Startup object to be the Chapter18.DeserializedFileToCustomerObject in the drop-down list, as shown in Figure 18-5.
19.
Close the Properties window.
20.
Click the File menu.
21.
Choose Save All.
22.
Click the Debug menu.
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.
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.
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.
29.
Click the Debug menu.
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.
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.
Right-click the CustomerBinary.cs file in the Solution Explorer panel.
2.
Choose Copy.
3.
Right-click the Chapter18 project in the Solution Explorer panel.
4.
Choose Paste.
5.
Right-click the new CustomerBinary – Copy.cs file.
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.
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
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.
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.
10.
Amend the code to remove the unnecessary accessors, as in Listing 18-31.
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:
Adding the code to serialize the CustomerXML object
17.
Right-click the Chapter18 project in the Solution Explorer panel.
18.
Choose Properties.
19.
Set the Startup object to be the XMLSerialisation in the drop-down list.
20.
Exit the Properties window.
21.
Click the File menu.
22.
Choose Save All.
23.
Click the Debug menu.
24.
Choose Start Without Debugging.
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.
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.
<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:
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.
26.
Right-click the Chapter18 project in the Solution Explorer panel.
27.
Choose Add.
28.
Choose Class.
29.
Name the class XMLDeserialisation.cs.
30.
Click the Add button.
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");
Adding the code to deserialize XML to a CustomerXML object
32.
Right-click the Chapter18 project in the Solution Explorer panel.
33.
Choose Properties.
34.
Set the Startup object to be the XMLDeserialisation in the drop-down list.
35.
Exit the Properties window.
36.
Click the File menu.
37.
Choose Save All.
38.
Click the Debug menu.
39.
Choose Start Without Debugging.
The deserialized object should be displayed as shown in Figure 18-9.
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:
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.
Right-click the Chapter18 project in the Solution Explorer panel.
2.
Choose Add.
3.
Choose Class.
4.
Name the class JSONSerialisation.cs.
5.
Click the Add button.
6.
Right-click the Chapter18 project in the Solution Explorer panel.
7.
Choose Add.
8.
Choose Class.
9.
Name the class CustomerJSON.cs.
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.
11.
Double-click the CustomerJSON file to open it in the editor window.
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.
Right-click the Chapter18 project in the Solution Explorer panel.
17.
Choose Properties.
18.
Set the Startup object to be the JSONSerialisation in the drop-down list.
19.
Exit the Properties window.
20.
Click the Debug menu.
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.
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.
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);
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);
public static async Task CreateJSON(CustomerJSON myCustomer)
{
Listing 18-38
Adding a call to the CreateJSON() method
25.
Click the File menu.
26.
Choose Save All.
27.
Click the Debug menu.
28.
Choose Start Without Debugging.
The console window will appear and display the object details in JSON format, as shown in Figure 18-10.
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.
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.
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);
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
33.
Click the File menu.
34.
Choose Save All.
35.
Click the Debug menu.
36.
Choose Start Without Debugging.
The console window will appear and display the object details, as shown in Figure 18-11.
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.