Step‑by‑Step Guide to ISerializable in C#: Create Your Own Serialization Logic

Serialization is a fundamental concept in C# that allows objects to be converted into a format that can be stored or transmitted and later reconstructed. The default serialization mechanisms in .NET, such as binary, XML, and JSON serialization, are sufficient for many use cases. However, there are situations where custom serialization logic is required. This is where the ISerializable interface comes into play.

In this step-by-step guide, we will explore how to implement the ISerializable interface in C# to define custom serialization logic. We'll cover:

  • What ISerializable is and when to use it.

  • How to implement ISerializable in your classes.

  • Handling custom serialization logic.

  • Security considerations and best practices.

Let's dive in!

What is ISerializable?

The ISerializable interface is part of the System.Runtime.Serialization namespace in .NET and provides a way to control the serialization process of an object. This is particularly useful when:

  • You need fine-grained control over how an object is serialized and deserialized.

  • Your class contains sensitive data that shouldn't be serialized by default.

  • Your object has non-serializable members that require custom handling.

  • You need to support backward compatibility for different object versions.

ISerializable Interface Definition

The ISerializable interface defines a single method:

public interface ISerializable
{
    void GetObjectData(SerializationInfo info, StreamingContext context);
}

Classes implementing ISerializable must define this method to specify how their data should be serialized.

Implementing ISerializable in C#

Let's create a simple example of a User class that implements ISerializable.

Step 1: Define the Class and Implement ISerializable

using System;
using System.Runtime.Serialization;
using System.Security.Permissions;

[Serializable]  // Required for serialization to work
public class User : ISerializable
{
    public string Name { get; set; }
    public int Age { get; set; }
    private string Password; // Private field not exposed publicly

    public User(string name, int age, string password)
    {
        Name = name;
        Age = age;
        Password = password;
    }

    // This method is responsible for serialization
    [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        info.AddValue("Name", Name);
        info.AddValue("Age", Age);
        info.AddValue("Password", EncryptPassword(Password)); // Encrypt sensitive data
    }

    // Constructor to handle deserialization
    protected User(SerializationInfo info, StreamingContext context)
    {
        Name = info.GetString("Name");
        Age = info.GetInt32("Age");
        Password = DecryptPassword(info.GetString("Password"));
    }

    private string EncryptPassword(string password) => Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(password));
    private string DecryptPassword(string encryptedPassword) => System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(encryptedPassword));
}

Step 2: Serialize and Deserialize the Object

Now, let's serialize and deserialize the User object using the BinaryFormatter.

using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

class Program
{
    static void Main()
    {
        User user = new User("John Doe", 30, "SuperSecretPassword");

        // Serialize
        BinaryFormatter formatter = new BinaryFormatter();
        using (FileStream stream = new FileStream("user.dat", FileMode.Create))
        {
            formatter.Serialize(stream, user);
        }

        // Deserialize
        using (FileStream stream = new FileStream("user.dat", FileMode.Open))
        {
            User deserializedUser = (User)formatter.Deserialize(stream);
            Console.WriteLine($"Name: {deserializedUser.Name}, Age: {deserializedUser.Age}");
        }
    }
}

Best Practices for Using ISerializable

1. Always Implement a Protected Constructor

.NET requires a special constructor for deserialization. This constructor should be protected to prevent external instantiation.

2. Mark Classes as [Serializable]

Even though ISerializable provides custom serialization logic, adding the [Serializable] attribute is still recommended to avoid unexpected issues.

3. Handle Security Concerns

Serialization can expose sensitive data. Always consider encrypting sensitive fields before storing them.

4. Avoid BinaryFormatter in New Applications

Microsoft has deprecated BinaryFormatter due to security risks. Consider using safer alternatives like System.Text.Json or Newtonsoft.Json.

5. Use OnDeserialized for Post-Processing

If your class needs additional setup after deserialization, you can use the OnDeserialized attribute:

[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
    Console.WriteLine("Deserialization completed.");
}

Conclusion

The ISerializable interface provides a powerful way to control object serialization in C#. By implementing custom serialization logic, developers can ensure data integrity, security, and backward compatibility. While BinaryFormatter was traditionally used for serialization, modern applications should prefer safer serialization techniques like System.Text.Json.

By following best practices, you can efficiently use ISerializable to serialize and deserialize complex objects while maintaining security and performance.