1. Giới thiệu
Hãy tưởng tượng một ứng dụng cho cửa hàng sách: đối tượng trong đó không chỉ là sách, mà còn có authors với tiểu sử, publishers xuất bản sách, employees, sections... Nếu serialization chỉ hỗ trợ object "phẳng", app của chúng ta sẽ dậm chân tại chỗ như một cuốn sổ tay. Trong dự án thực tế dữ liệu gần như luôn là nhiều cấp và lồng nhau. Vì vậy biết cách tuần tự hóa và giải tuần tự cấu trúc phân cấp là kỹ năng tách biệt dev trung bình và một chuyên gia .NET về serialization.
Mình sẽ giải thích cách các serializer hiện đại của C# (ví dụ System.Text.Json) cho phép lưu không chỉ cây đối tượng mà cả "một khu rừng". Cũng sẽ nói cách thiết kế class để serialization thuận tiện, để sau này không phải lôi dữ liệu thủ công từng mẩu.
2. Mô hình hoá cấu trúc phân cấp
Mở rộng model của chúng ta. Trong ví dụ trước có Book, Author và class Library chứa danh sách sách. Bây giờ thêm một cấp nữa: mỗi author có danh sách sách họ đã viết (đúng, dữ liệu bị trùng — lát nữa sẽ bàn hậu quả!), và library có thêm danh sách employees Employee.
Sơ đồ các lớp
Trông khoảng như sau:
classDiagram
class Library {
List~Book~ Books
List~Employee~ Employees
string Name
}
class Book {
string Title
Author Author
int Year
}
class Author {
string Name
int BirthYear
List~Book~ Books
}
class Employee {
string Name
string Position
}
Library "1" -- "many" Book
Library "1" -- "many" Employee
Book "1" -- "1" Author
Author "1" -- "many" Book
Cách tiếp cận này cho phép xây hệ thống thư viện đầy đủ. Có rủi ro xuất hiện "tham chiếu vòng" (ví dụ author có books, book có author: serialization vô hạn!). Chúng ta sẽ bàn vấn đề này ở phần sau.
3. Ví dụ hiện thực các lớp
Trước hết mô tả các class C# cho model. Kèm theo comment:
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
public class Library
{
public string Name { get; set; }
public List<Book> Books { get; set; } = new();
public List<Employee> Employees { get; set; } = new();
}
public class Book
{
public string Title { get; set; }
public int Year { get; set; }
public Author Author { get; set; }
}
public class Author
{
public string Name { get; set; }
public int BirthYear { get; set; }
// QUAN TRỌNG! Trường này tiềm ẩn "nguy hiểm": nó có thể tạo tham chiếu vòng.
// Nhưng để minh hoạ mình bật nó.
public List<Book> Books { get; set; } = new();
}
public class Employee
{
public string Name { get; set; }
public string Position { get; set; }
}
4. Xây dựng cấu trúc: tạo thư viện
Bây giờ tạo một thư viện với sách, authors và employees để kiểm thử serialization.
// Tạo authors
var author1 = new Author { Name = "Vilyam Golding", BirthYear = 1911 };
var author2 = new Author { Name = "John Updike", BirthYear = 1932 };
var author3 = new Author { Name = "Jerome Salinger", BirthYear = 1919 };
// Tạo sách
var book1 = new Book { Title = "Le to quan", Year = 1954, Author = author1 };
var book2 = new Book { Title = "Kentavr", Year = 1963, Author = author2 };
var book3 = new Book { Title = "Na proposte vo rzhi", Year = 1951, Author = author3 };
// Thêm sách cho các author
author1.Books.Add(book1);
author2.Books.Add(book2);
author3.Books.Add(book3);
// Tạo nhân viên
var emp1 = new Employee { Name = "Ivan Ivanov", Position = "Bibliotekar" };
var emp2 = new Employee { Name = "Sergey Sergeev", Position = "Direktor" };
// Tổng hợp thư viện
var library = new Library
{
Name = "Gorodskaya biblioteka",
Books = new List<Book> { book1, book2, book3 },
Employees = new List<Employee> { emp1, emp2 }
};
Tất nhiên trong code thực tế bạn sẽ tự động hoá việc tạo object và mapping quan hệ để không phải quản lý thủ công từng cuốn sách tới author. Nhưng ví dụ này là đủ.
5. Tuần tự hóa sang JSON
Sử dụng cách cổ điển với System.Text.Json:
using System.Text.Json;
// Tuần tự hoá library thành JSON
var options = new JsonSerializerOptions
{
WriteIndented = true, // định dạng đẹp với indent
ReferenceHandler = ReferenceHandler.IgnoreCycles // tránh vòng lặp!
};
string json = JsonSerializer.Serialize(library, options);
// In ra kết quả
Console.WriteLine(json);
Điểm thú vị: Nếu không dùng tuỳ chọn ReferenceHandler.IgnoreCycles, serialization sẽ vòng lặp — vì author có books, book có author, nên serializer sẽ đi qua lại mãi cho đến khi dừng (và ném exception). Tuỳ chọn IgnoreCycles giải quyết vấn đề: khi gặp object đã được tuần tự hoá phía trên cây, nó sẽ ghi null thay vì tuần tự hoá lại.
JSON sẽ trông như thế nào?
{
"Name": "Gorodskaya biblioteka",
"Books": [
{
"Title": "Kentavr",
"Year": 1963,
"Author": {
"Name": "John Updike",
"BirthYear": 1932,
"Books": [
{
"Title": "Kentavr",
"Year": 1963,
"Author": null
},
{
"Title": "Istvikskie vedmy",
"Year": 1984,
"Author": null
}
]
}
},
{
"Title": "Istvikskie vedmy",
"Year": 1984,
"Author": {
"Name": "John Updike",
"BirthYear": 1932,
"Books": [
{
"Title": "Kentavr",
"Year": 1963,
"Author": null
},
{
"Title": "Istvikskie vedmy",
"Year": 1984,
"Author": null
}
]
}
}
],
"Employees": [
{
"Name": "Ivan Ivanov",
"Position": "Bibliotekar"
},
{
"Name": "Sergey Sergeev",
"Position": "Direktor"
}
]
}
Lưu ý: bên trong mảng Books author của cuốn sách không còn thông tin author đầy đủ — thay vào đó Author: null. Đó là cách cắt vòng lặp khi tuần tự hoá.
6. Giải tuần tự hoá về lại đối tượng
Bây giờ giải tuần tự hoá dữ liệu trở lại:
// Khôi phục object từ JSON
var libraryCopy = JsonSerializer.Deserialize<Library>(json, options);
Console.WriteLine(libraryCopy.Name); // "Gorodskaya biblioteka"
Console.WriteLine($"Knig: {libraryCopy.Books.Count}");
Console.WriteLine($"Sotrudnikov: {libraryCopy.Employees.Count}");
Nhưng! Việc khôi phục tham chiếu vòng không còn đảm bảo 100%: các cuốn sách lồng trong danh sách author sẽ có trường Author bằng null, vì serializer đã cắt chuỗi để tránh lồng vô hạn.
Điểm quan trọng: Tuần tự hoá các liên kết phức tạp (ví dụ parent — children — parent) bằng serializer mặc định luôn là một trade-off: hoặc mất một số liên kết, hoặc phải khôi phục thủ công sau khi giải tuần tự.
7. Tham chiếu vòng (lồng nhau vs vòng)
Nếu trong cấu trúc có trường hợp object tham chiếu lại chính nó qua chuỗi các object khác (tham chiếu vòng) — các serializer chuẩn như System.Text.Json và Newtonsoft.Json, đặc biệt trong chế độ strong-typing, phản ứng khác nhau. Trước khi có tuỳ chọn ReferenceHandler.IgnoreCycles serialization sẽ dừng lại với exception "ReferenceLoopHandling detected". Bây giờ nó chỉ ghi null thay cho tham chiếu lặp lại.
Vấn đề ở đây là gì?
Ưu: mã của bạn không crash vì lỗi.
Nhược: sau khi giải tuần tự bạn phải khôi phục một phần liên kết thủ công. Ví dụ, nếu graph người dùng tham chiếu lẫn nhau (ví dụ employee và manager), sau giải tuần tự một vài tham chiếu có thể bị null.
8. Thiết kế cấu trúc phức tạp để dễ serialization
Nếu bạn biết trước object có thể tạo vòng, hoặc cần đảm bảo sau khi khôi phục tất cả liên kết đúng như ban đầu — tốt hơn là lưu identifier thay vì object trực tiếp.
Ví dụ: lưu identifier thay vì tham chiếu
Thay đổi class Book để tham chiếu author bằng identifier:
public class Book
{
public string Title { get; set; }
public int Year { get; set; }
public int AuthorId { get; set; }
}
Thay vì danh sách sách trong author — dùng danh sách id sách. Để khôi phục mối quan hệ sau khi giải tuần tự bạn sẽ phải "map" theo id, nhưng bù lại tránh được vòng lặp nguy hiểm.
Tại sao điều này quan trọng trong dự án thực tế?
- Databases gần như luôn dùng identifiers vì dễ thao tác khi export/import.
- REST API cũng trao đổi id thay vì các cấu trúc lồng phức tạp.
9. Collection lồng nhau: tuần tự hoá cây
Trường hợp phổ biến — các hierarchy có độ sâu tuỳ ý: cây folder, cấu trúc menu, catalog sản phẩm với subcategories.
Ví dụ class cho "cây":
public class Folder
{
public string Name { get; set; }
public List<Folder> Children { get; set; } = new();
}
Tạo cây:
var root = new Folder
{
Name = "Root",
Children = new List<Folder>
{
new Folder { Name = "Sub1", Children = { new Folder { Name = "Sub1-1" } } },
new Folder { Name = "Sub2" }
}
};
Tuần tự hoá và in:
string jsonTree = JsonSerializer.Serialize(root, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonTree);
JSON cho cây kiểu này sẽ cho thấy rõ cấu trúc phân cấp với mảng lồng nhau.
10. Những lưu ý về tuần tự hoá mảng và list
Nếu một property là array (T[]) hoặc collection (List<T>), serialization sẽ biến nó thành mảng JSON bình thường.
public class Shop
{
public string Name { get; set; }
public string[] Departments { get; set; }
}
var shop = new Shop
{
Name = "Universam",
Departments = new[] { "Ovoschi", "Frukty", "Myaso" }
};
string jsonShop = JsonSerializer.Serialize(shop, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(jsonShop);
JSON sẽ xấp xỉ như sau:
{
"Name": "Universam",
"Departments": [
"Ovoschi",
"Frukty",
"Myaso"
]
}
11. Ảnh hưởng của attributes lên các đối tượng lồng nhau
Nếu dùng [JsonIgnore] cho properties trong các đối tượng lồng nhau, chúng sẽ không xuất hiện trong JSON kết quả bất kể cấp độ lồng.
public class SecretBook : Book
{
[JsonIgnore]
public string SecretCode { get; set; }
}
Cách này thường dùng để che dữ liệu private: nếu không muốn tuần tự hoá một số field trong object, chỉ cần thêm attribute — và dữ liệu đó sẽ bị bỏ.
12. Lời khuyên thực tế
- Phỏng vấn thường hỏi: "Làm thế nào để tuần tự hoá cây (Tree)?" và "Xử lý tham chiếu vòng thế nào?". Chuẩn bị ví dụ với ReferenceHandler.IgnoreCycles và lưu id.
- Trong dự án thương mại tuần tự hoá orders, invoices, users, product catalogs, báo cáo phức tạp. Độ lồng xuất hiện ở khắp nơi.
- Nếu làm việc với graphs hoặc trees, cố tránh vòng lặp hoặc dùng id.
- Nếu dùng API ngoài, thống nhất format của các cấu trúc lồng trước để tránh bất ngờ khi parse JSON.
GO TO FULL VERSION