Hello, .NET developers! 👋
Every real application shares two silent goals — reusability and portability. Reusability comes from writing code that can handle different types without rewriting logic. Portability comes from turning objects into transferable data so they can move across files, APIs, or networks. In C#, these two ideas are embodied by Generics (for generalization) and Serialization (for object persistence).
Understanding Generalization — Making Code Reusable and Type-Safe
Generalization means creating a design that works with multiple data types while keeping strong type safety.
In C#, the tool for this is Generics. Instead of writing separate versions of the same class or method for different types, you define one version that adapts to any type at compile time.
Example: Generic Repository
public class Repository<T>
{
private readonly List<T> _items = new();
public void Add(T item) => _items.Add(item);
public IEnumerable<T> GetAll() => _items;
}
public class Customer
{
public string Name { get; set; }
}
public class Product
{
public string Title { get; set; }
}
class Program
{
static void Main()
{
var customerRepo = new Repository<Customer>();
customerRepo.Add(new Customer { Name = "Bhargavi" });
var productRepo = new Repository<Product>();
productRepo.Add(new Product { Title = "Laptop" });
Console.WriteLine(customerRepo.GetAll().First().Name);
}
}
Here, one generic repository works for both Customer and Product.
That’s generalization in action — your logic is abstract, but your type safety remains intact.
If you accidentally try to add a Product into a Repository<Customer>, the compiler will stop you immediately.
Generics are the backbone of modern C# libraries — from List<T> and Dictionary<TKey,TValue> to dependency injection containers.
They prevent runtime casting errors and deliver performance benefits because no boxing or reflection is needed for type conversion.
Constraints — Guiding the Type Parameter
Sometimes, you need to tell the compiler what your generic type can or must do.
That’s where constraints come in. They narrow down what types are allowed as T.
Example: Adding a Constraint
public interface IEntity
{
int Id { get; set; }
}
public class Repository<T> where T : IEntity
{
private readonly List<T> _items = new();
public void Add(T item)
{
_items.Add(item);
Console.WriteLine($"Added entity with ID: {item.Id}");
}
}
The constraint where T : IEntity ensures that only types implementing IEntity can be stored.
This lets you safely access item.Id inside the generic class without reflection or dynamic typing.
Real-World Analogy — The Generic Toolbox
Think of generics like a toolbox that fits interchangeable tools. The toolbox (generic class) is built once, but the tools (types) change depending on the task. You can carry the same box to fix different problems without rebuilding it each time.
Serialization — Turning Objects into Data
Now let’s switch gears. If generalization helps us reuse logic, serialization helps us reuse data. Serialization is the process of converting an object into a format that can be stored (in a file or database) or transmitted (across a network). Deserialization reverses that process — it rebuilds the object from that data.
Common formats include JSON, XML, and binary.
In modern .NET, System.Text.Json is the preferred library for JSON serialization — fast, lightweight, and built-in.
Example: JSON Serialization
using System.Text.Json;
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public string Department { get; set; }
}
class Program
{
static void Main()
{
var emp = new Employee { Id = 1, Name = "Anita", Department = "Finance" };
// Serialize
string json = JsonSerializer.Serialize(emp);
Console.WriteLine("Serialized JSON:");
Console.WriteLine(json);
// Deserialize
var copy = JsonSerializer.Deserialize<Employee>(json);
Console.WriteLine($"Deserialized Employee: {copy.Name} from {copy.Department}");
}
}
The object is first serialized into JSON text and then reconstructed back into an Employee object.
This makes it easy to send employee data through APIs or store it in a file for later use.
The Hidden Magic — Reflection and Attributes
Serialization in .NET relies heavily on reflection — the runtime mechanism that inspects object properties and fields. Attributes let you customize how serialization behaves. For example, you can rename a property, ignore a field, or format a date differently.
Example: Customizing Serialization
using System.Text.Json.Serialization;
public class Order
{
[JsonPropertyName("order_id")]
public int Id { get; set; }
[JsonIgnore]
public string InternalNote { get; set; }
public string Product { get; set; }
}
Here, Id will appear in JSON as order_id, and InternalNote will be skipped entirely.
These small details matter when integrating with third-party APIs or following naming conventions in microservices.
Real-World Scenario — Combining Both
Let’s connect generalization and serialization together. Imagine you’re building a data-sync service that pulls records from different systems — Customers, Orders, and Payments — and stores them as JSON files. With generics, you can create a single serializer service that handles any entity type.
Example: Generic Serializer Service
public class JsonFileSerializer<T>
{
public void Save(string filePath, T data)
{
var json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(filePath, json);
Console.WriteLine($"Saved {typeof(T).Name} data to {filePath}");
}
public T Load(string filePath)
{
var json = File.ReadAllText(filePath);
return JsonSerializer.Deserialize<T>(json);
}
}
Now, your app can handle any model with one reusable class:
var customerSerializer = new JsonFileSerializer<Customer>();
customerSerializer.Save("customer.json", new Customer { Name = "Bhargavi" });
var orderSerializer = new JsonFileSerializer<Order>();
orderSerializer.Save("order.json", new Order { Product = "Laptop" });
This pattern powers many enterprise systems — reusable serialization logic that adapts to any domain model using generics. It’s flexible, type-safe, and fully aligned with clean-architecture principles.
Wrapping Up
Generics make your code flexible and maintainable; serialization makes your data portable and persistent. Together, they let your software think in types but speak in data. Whenever you design a system that needs both reusability and communication — think APIs, data pipelines, or microservices — you’ll find these two concepts working hand in hand.
Comments
Post a Comment