Serialization is a fundamental concept in C# that enables the transformation of objects into a format suitable for storage or transmission. While built-in serializers like System.Text.Json
, XmlSerializer
, and BinaryFormatter
cover many common scenarios, they often fall short when dealing with complex object graphs, security constraints, or performance requirements.
This blog post delves deep into custom serialization in C#, equipping you with the knowledge to tailor object persistence to your specific needs.
Why Custom Serialization Matters
Custom serialization is essential when:
You need full control over the serialization process.
Built-in serializers do not support complex scenarios.
Performance optimization is critical.
You need to ensure data security and compliance.
Backward and forward compatibility is required.
Understanding Built-in Serialization Options
Before diving into custom serialization, let's review the built-in serialization mechanisms in .NET:
1. Binary Serialization (Obsolete)
Traditionally, .NET
used BinaryFormatter
for binary serialization, but it is not recommended due to security vulnerabilities.
[Serializable]
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
Note: Avoid
BinaryFormatter
in new projects; use safer alternatives like JSON or Protobuf.
2. JSON Serialization
.NET Core provides System.Text.Json
, a high-performance JSON serializer.
var json = JsonSerializer.Serialize(new Person { Name = "John", Age = 30 });
Limitation: Does not support private fields and requires custom converters for special scenarios.
3. XML Serialization
Useful for interoperability but limited in handling private members.
var xmlSerializer = new XmlSerializer(typeof(Person));
Implementing Custom Serialization
When built-in serializers do not meet your needs, you can implement custom serialization using:
1. Implementing ISerializable
Interface
The ISerializable
interface provides granular control over how objects are serialized.
[Serializable]
public class Employee : ISerializable
{
public string Name { get; set; }
public int Salary { get; set; }
public Employee() { }
protected Employee(SerializationInfo info, StreamingContext context)
{
Name = info.GetString("Name");
Salary = info.GetInt32("Salary");
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Name", Name);
info.AddValue("Salary", Salary);
}
}
Best Practice: Implement
ISerializable
only when necessary and handle versioning carefully.
2. Custom JSON Serialization with JsonConverter
When System.Text.Json
does not support a required format, use JsonConverter
.
public class EmployeeJsonConverter : JsonConverter<Employee>
{
public override Employee Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string name = "";
int salary = 0;
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.PropertyName)
{
string propertyName = reader.GetString();
reader.Read();
if (propertyName == "EmployeeName")
name = reader.GetString();
else if (propertyName == "Salary")
salary = reader.GetInt32();
}
}
return new Employee { Name = name, Salary = salary };
}
public override void Write(Utf8JsonWriter writer, Employee value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString("EmployeeName", value.Name);
writer.WriteNumber("Salary", value.Salary);
writer.WriteEndObject();
}
}
Use Case: When API contracts require custom JSON structures.
3. Custom XML Serialization with IXmlSerializable
For custom XML formatting, implement IXmlSerializable
.
public class Department : IXmlSerializable
{
public string Name { get; set; }
public int EmployeesCount { get; set; }
public XmlSchema GetSchema() => null;
public void ReadXml(XmlReader reader)
{
reader.MoveToContent();
Name = reader.GetAttribute("Name");
EmployeesCount = int.Parse(reader.GetAttribute("EmployeesCount"));
reader.Read();
}
public void WriteXml(XmlWriter writer)
{
writer.WriteAttributeString("Name", Name);
writer.WriteAttributeString("EmployeesCount", EmployeesCount.ToString());
}
}
Best Practice: Ensure XML structure compatibility with external systems.
Performance Optimization Strategies
Use Binary Serialization Alternatives (e.g.,
MessagePack
,Protobuf
for high-speed serialization).Avoid Over-Serialization (Serialize only required properties, avoid deep object graphs).
Compress Serialized Data (Use
GZipStream
to reduce payload size).Parallel Serialization (Use async serialization to improve throughput).
Handling Complex Scenarios
1. Circular References
Use ReferenceHandler.Preserve
in System.Text.Json
:
var options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve };
2. Versioning Support
Implement versioning strategies to maintain backward compatibility:
info.AddValue("Version", 1);
3. Secure Serialization
Avoid serializing sensitive data.
Use cryptographic signing to prevent tampering.
Ensure deserialization does not execute arbitrary code.
Real-World Use Cases
Enterprise APIs: Custom JSON converters for strict API contract compliance.
Caching Strategies: Serialize objects efficiently for in-memory caches.
Data Persistence: Store application state with version-aware serialization.
Conclusion
Custom serialization in C# is a powerful technique when built-in options do not meet application needs. By leveraging ISerializable
, JsonConverter
, and IXmlSerializable
, developers gain full control over object persistence, ensuring performance, security, and flexibility. Understanding when and how to implement custom serialization can significantly enhance the maintainability and efficiency of your applications.