CodeGym /Các khóa học /C# SELF /Làm việc với đối tượng lồng nhau và collections

Làm việc với đối tượng lồng nhau và collections

C# SELF
Mức độ, Bài học
Có sẵn

1. Giới thiệu

Serialize một object đơn giản giống như gửi một tấm bưu thiếp: đơn giản, không có gì bị mất. Nhưng thường object của chúng ta trở thành "album gia đình": có các đối tượng lồng nhau, collections và thậm chí collections lồng trong collections.

Tưởng tượng user của chúng ta không chỉ là một Person với tên và tuổi, mà ví dụ có một bộ contacts (Contact), vài địa chỉ nhà/cơ quan, và có thể một field kiểu List<Pet> nếu người đó thích thú cưng. Tất cả đều là đối tượng lồng nhau và collections.

Các serializer trong .NET xử lý chuyện này thế nào? Cần lưu ý gì khi serialize một cây object phức tạp? Có sập mọi thứ nếu trong collection có collection khác không? Hôm nay mình sẽ giải thích rõ những gì mong đợi — và nên làm gì trong các trường hợp khó.

Ý nghĩa của "lồng nhau" trong ngữ cảnh .NET

Đối tượng lồng nhau là đơn giản là một object khác được dùng như property hoặc field bên trong object chính của bạn. Ví dụ, đây là model user mở rộng:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public List<Contact> Contacts { get; set; }         // Bộ sưu tập của các đối tượng lồng nhau
    public Address? HomeAddress { get; set; }           // Một đối tượng lồng nhau (nullable)
}
public class Contact
{
    public string Type { get; set; }                    // Ví dụ: Email hoặc Phone
    public string Value { get; set; }
}
public class Address
{
    public string City { get; set; }
    public string Street { get; set; }
}

Chú ý — một người có thể có nhiều contacts, còn address thường chỉ một (và có thể là null). Đây là kịch bản cơ bản của nhiều model.

2. Serialize objects và collections với System.Text.Json

Ví dụ đơn giản: serialize và deserialize một collection

Thử xem nó hoạt động như thế nào trong thực tế:

using System.Text.Json;

var person = new Person
{
    Name = "Anna",
    Age = 28,
    Contacts = new List<Contact>
    {
        new Contact { Type = "Email", Value = "anna@example.com" },
        new Contact { Type = "Phone", Value = "+1234567890" }
    },
    HomeAddress = new Address { City = "Berlin", Street = "Alexanderplatz, 1" }
};

string json = JsonSerializer.Serialize(person, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

Kết quả sẽ xấp xỉ như sau:

{
  "Name": "Anna",
  "Age": 28,
  "Contacts": [
    {
      "Type": "Email",
      "Value": "anna@example.com"
    },
    {
      "Type": "Phone",
      "Value": "+1234567890"
    }
  ],
  "HomeAddress": {
    "City": "Berlin",
    "Street": "Alexanderplatz, 1"
  }
}

Serializer xử lý lồng nhau rất ngon. Nếu một property là object khác, nó sẽ serialize nó "lồng" vào. Nếu là list thì serialize như array.

Thực tế: Serialize tự động hoạt động cho bất kỳ collection và đối tượng lồng nhau nào, miễn là chúng public và có getter/setter public (get/set).

Deserialize: hoạt động "mặc định"

string jsonInput = /* JSON mà ta vừa có được */;
Person deserializedPerson = JsonSerializer.Deserialize<Person>(jsonInput);
Console.WriteLine(deserializedPerson.Name); // "Anna"
Console.WriteLine(deserializedPerson.Contacts[0].Type); // "Email"

Mọi thứ hoạt động — contacts trở lại từ JSON array thành List<Contact>, HomeAddress trở lại thành object Address.

3. Cách serialize collections: List, arrays, Dictionary

Thường trong model của bạn không chỉ có lists (List<T>), mà còn dictionaries (Dictionary<TKey, TValue>) hoặc mảng nhiều chiều.

Ví dụ với array

public class Team
{
    public string Name { get; set; }
    public Person[] Members { get; set; }
}

var team = new Team
{
    Name = "Nhóm phát triển",
    Members = new[]
    {
        new Person { Name = "Aleksey", Age = 31 },
        new Person { Name = "Ekaterina", Age = 27 }
    }
};
string json = JsonSerializer.Serialize(team, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON:

{
  "Name": "Nhóm phát triển",
  "Members": [
    {
      "Name": "Aleksey",
      "Age": 31,
      "Contacts": null,
      "HomeAddress": null
    },
    {
      "Name": "Ekaterina",
      "Age": 27,
      "Contacts": null,
      "HomeAddress": null
    }
  ]
}

Serialize collection giống như đóng gói một đống bưu thiếp giống nhau: mỗi phần tử là một "tấm bưu thiếp" riêng.

Ví dụ với Dictionary

public class Phonebook
{
    public Dictionary<string, string> Phones { get; set; }
}

var phonebook = new Phonebook
{
    Phones = new Dictionary<string, string>
    {
        { "Andrey", "+12998887766" },
        { "Mariya", "+12882223344" }
    }
};

string json = JsonSerializer.Serialize(phonebook, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON:

{
  "Phones": {
    "Andrey": "+12998887766",
    "Mariya": "+12882223344"
  }
}

Trong JSON, dictionary trở thành object với các khóa động.

4. Collections lồng nhau (List trong List, arrays of arrays)

.NET có thể serialize cả những "matrioshka" như vậy:

public class Zoo
{
    public List<List<Animal>> AnimalGroups { get; set; }
}
public class Animal { public string Name { get; set; } }

var zoo = new Zoo
{
    AnimalGroups = new List<List<Animal>>
    {
        new List<Animal> { new Animal { Name = "Sư tử" }, new Animal { Name = "Hổ" } },
        new List<Animal> { new Animal { Name = "Gấu" } }
    }
};

string json = JsonSerializer.Serialize(zoo, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON:

{
  "AnimalGroups": [
    [ { "Name": "Sư tử" }, { "Name": "Hổ" } ],
    [ { "Name": "Gấu" } ]
  ]
}

Cũng tương tự khi deserialize ngược lại.

5. Đặc điểm serialize đối tượng lồng nhau với XmlSerializer

Ví dụ serialize đối tượng lồng nhau

using System.Xml.Serialization;
using System.IO;

var person = new Person
{
    Name = "Ivan",
    Age = 35,
    HomeAddress = new Address { City = "Bonn", Street = "Beethoven str., 100" },
    Contacts = new List<Contact>
    {
        new Contact { Type = "Email", Value = "ivan@domain.de" }
    }
};

var serializer = new XmlSerializer(typeof(Person));
using var writer = new StringWriter();
serializer.Serialize(writer, person);
Console.WriteLine(writer.ToString());

Kết quả:

<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Name>Ivan</Name>
  <Age>35</Age>
  <Contacts>
    <Contact>
      <Type>Email</Type>
      <Value>ivan@domain.de</Value>
    </Contact>
  </Contacts>
  <HomeAddress>
    <City>Bonn</City>
    <Street>Beethoven str., 100</Street>
  </HomeAddress>
</Person>

Đặc điểm của collections trong XmlSerializer

Theo mặc định XmlSerializer serialize collection như thẻ "Contacts", bên trong thẻ đó mỗi phần tử sẽ có thẻ "Contact" riêng. Loại collection phải public, và phần tử của nó phải là kiểu có thể serialize được.

Điểm quan trọng. Nếu collection rỗng, XML vẫn sẽ chứa thẻ rỗng:

<Contacts />

Collection được hỗ trợ. XmlSerializer hỗ trợ List<T>, mảng T[], các collection implement ICollection<T>. Nó không hỗ trợ ví dụ như Dictionary<TKey, TValue>! Để serialize dictionary bạn sẽ cần bọc dictionary thành list cặp key-value hoặc dùng kỹ thuật/biblioteka ngoài.

6. Hỗ trợ objects và collections trong Newtonsoft.Json

Newtonsoft.Json hỗ trợ lồng nhau linh hoạt hơn một chút. Trong 99% trường hợp mọi thứ hoạt giống như System.Text.Json, nhưng có vài điểm cộng: có thể serialize cả private fields (nếu cấu hình), dictionaries với key phức tạp hơn, dynamic types, và thậm chí circular references (với các setting đặc biệt).

Ví dụ

using Newtonsoft.Json;

var person = new Person
{
    Name = "Pavel",
    Age = 40,
    Contacts = new List<Contact>
    {
        new Contact { Type = "SMS", Value = "+10000000013" }
    }
};
string json = JsonConvert.SerializeObject(person, Formatting.Indented);
Console.WriteLine(json);

Khác biệt. Nếu object có ví dụ private fields, bạn có thể serialize chúng bằng cách cấu hình ContractResolver. Nhưng với các collections và objects lồng nhau cơ bản thì mọi thứ tự động hoạt động "mặc định".

7. Các lỗi điển hình, bẫy và lưu ý

Lỗi 1: Đối tượng lồng nhau là null.
Nếu trong object bạn serialize có vài property chưa được gán (null), thì trong JSON hoặc XML chúng có thể không xuất hiện, hoặc sẽ được ghi là null. Ví dụ, nếu HomeAddress của Personnull, thì trong JSON:

"HomeAddress": null

Trong XML mặc định thẻ đó sẽ không có — bạn có thể thêm nó bằng cấu hình serializer ([XmlElement(IsNullable=true)]).

Lỗi 2: Serialize private properties/fields.
Mặc định hầu hết serializers (JSON và XML) chỉ làm việc với properties public. Nếu bạn muốn serialize private hoặc protected fields, phải public chúng hoặc cấu hình serializer (ví dụ qua ContractResolver trong Newtonsoft.Json).

Lỗi 3: Lớp không có constructor mặc định.
Trong XML-serialization (và thường cả JSON), lớp nên có constructor public không tham số. Nếu không có, sẽ có exception khi deserialize.

Lỗi 4: Serialize dictionaries trong XML.
XmlSerializer không hỗ trợ trực tiếp serialize Dictionary<TKey, TValue>. Đây thường là điều bất ngờ. Giải pháp có thể là bọc dictionary vào list cặp hoặc dùng serializer khác.

Lỗi 5: Circular references.
Nếu một object tham chiếu đến object khác mà object kia lại tham chiếu ngược lại (ví dụ parent và child), serialize "vòng tròn" có thể gây StackOverflow (StackOverflowException) hoặc exception về circular reference. Ở JSON serializers có thể cấu hình để xử lý, nhưng thường đúng hơn là nên xem lại thiết kế object.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Serialization và deserialization cấu trúc phức tạp
Serialization và deserialization cấu trúc phức tạp
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION