CodeGym /Các khóa học /C# SELF /Vấn đề tham chiếu vòng

Vấn đề tham chiếu vòng

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

1. Giới thiệu

Tham chiếu vòng (hoặc circular reference) xảy ra khi một object trực tiếp hoặc gián tiếp chứa tham chiếu tới một object khác, mà object đó cuối cùng lại tham chiếu ngược lại object đầu tiên.

Ví dụ thực tế

Giờ mình thử lấy ví dụ thực tế cho thư viện sách. Giả sử ta có class Book với property Author, còn class Author có property Books kiểu List<Book> để lưu tất cả sách của họ.


public class Author
{
    public string Name { get; set; }
    public int BirthYear { get; set; }
    public List<Book> Books { get; set; } = new List<Book>();
}

public class Book
{
    public string Title { get; set; }
    public Author Author { get; set; }
}

Bây giờ, nếu ta tạo một tác giả và một cuốn sách, thiết lập liên kết, sẽ có "vòng kín":


var author = new Author { Name = "Marsel Prust", BirthYear = 1871 };
var book = new Book { Title = "Ve huong Swan", Author = author };

author.Books.Add(book);

// Xong, giờ author tham chiếu tới book, và book tham chiếu tới author!

Tại sao đó là vấn đề?

Khi bạn serialize một object như vậy sang JSON, serializer đi theo các property. Nó thấy tác giả có books, bên trong lại có author... rồi lại books... tiếp tục vô hạn.

author -> books[] -> author -> books[] ...

Giống như nhìn vào gương trước gương — phản chiếu kéo dài vô tận. Chỉ có điều thay vì phản chiếu đẹp là stack overflow (StackOverflowException).

2. Serializer phản ứng thế nào với tham chiếu vòng?

Lỗi khi serialize

Theo mặc định System.Text.Json không xử lý được tham chiếu vòng. Nếu bạn cố serialize cấu trúc như trên, sẽ nhận được ngoại lệ JsonException: "A possible object cycle was detected".

Ví dụ gây lỗi:


string json = JsonSerializer.Serialize(author); // BÙM! JsonException

Trực quan:

graph TD;
    Author --> Book;
    Book --> Author;

3. Cách giải quyết vấn đề tham chiếu vòng?

Xem vài cách thực tế, mỗi cách có ưu và nhược. Như bạn đoán, không có một "cờ ma thuật" chung cho mọi tình huống (buồn nhỉ).

Xóa vòng trước khi serialize

Đơn giản nhất — đừng để vậy. Trước khi serialize, set null (hoặc bỏ qua) các tham chiếu gây vòng.

Trông như thế này trong thực tế:


// Tạm thời bỏ tham chiếu author -> books
var authorToSerialize = new Author
{
    Name = author.Name,
    BirthYear = author.BirthYear,
    Books = null // hoặc không include property này
};

string json = JsonSerializer.Serialize(authorToSerialize);
// Giờ serialize ổn!

Ưu: đơn giản, nhanh, rõ ràng.
Nhược: mất dữ liệu (sau khi deserialize sẽ không có liên kết ngược).

Dùng attribute [JsonIgnore]

Có thể đánh dấu property tham gia vòng là bị ignore:


public class Author
{
    public string Name { get; set; }
    public int BirthYear { get; set; }
    
    [JsonIgnore] 
    public List<Book> Books { get; set; }
}

Khi serialize author thì books sẽ không có trong JSON. Giống cách trên nhưng declarative, không cần "dọn tay".

Ưu: đơn giản, ít nguy cơ quên.
Nhược: thông tin về sách của author bị mất trong JSON.

Dùng identifier thay vì object lồng nhau

Nếu cần giữ cả hai phía của liên kết (author và book) nhưng không muốn vòng, dùng Id duy nhất thay vì object lồng:


public class Book
{
    public string Title { get; set; }
    public int AuthorId { get; set; } // thay vì Author
}
public class Author 
{
    public int AuthorId { get; set; }
    public string Name { get; set; }
    // không lưu books hoặc lưu danh sách Id của chúng
}

Trong JSON giờ là Id thay vì object. Cách này phổ biến trong DB, REST API và hệ thống có tham chiếu rõ ràng.

Ưu: không có vòng, JSON gọn, liên kết có thể khôi phục bằng Id.
Nhược: phá mô hình đối tượng quen thuộc, cần lookup theo Id khi deserialize.

Bảng so sánh nhỏ:

Cách Giải quyết vòng? Mất dữ liệu? Áp dụng cho
[JsonIgnore] Có Có Khi nesting không quan trọng
Xóa tham chiếu thủ công Có Có Nhanh trước khi serialize
Lưu Id thay vì object Có Không* REST, DB, hệ thống phức tạp

* Dữ liệu không mất, nhưng không có sẵn ngay (cần tìm theo Id).

4. Làm sao để dạy System.Text.Json serialize tham chiếu vòng?

Kể từ .NET 5 JsonSerializerOptions có chế độ cho references: options.ReferenceHandler = ReferenceHandler.Preserve.

Chế độ này dùng các trường đặc biệt $id và $ref cho object lặp.

Ví dụ


var options = new JsonSerializerOptions
{
    WriteIndented = true,
    ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve
};

string json = JsonSerializer.Serialize(author, options);
Console.WriteLine(json);

JSON thu được sẽ giống thế này:

{
  "$id": "1",
  "Name": "Marsel Prust",
  "BirthYear": 1871,
  "Books": {
    "$id": "2",
    "$values": [
      {
        "$id": "3",
        "Title": "Ve huong Swan",
        "Author": {
          "$ref": "1"
        }
      }
    ]
  }
}
  • $id — id duy nhất của object trong JSON
  • $ref — tham chiếu tới object đã serialize trước đó

Khi deserialize mọi thứ sẽ được khôi phục đúng (không vòng vô hạn hay lỗi stack).

Điểm đặc biệt và giới hạn

  • JSON kiểu này lạ với frontend: hầu hết client JS không hiểu $id/$ref nếu không có logic thêm.
  • Kích thước JSON lớn hơn, khó debug bằng mắt hơn.
  • Chỉ hoạt động khi bật rõ ReferenceHandler.Preserve.
  • Không áp dụng cho value types (ở đó không có vòng).

Serialize ngược (deserialize) JSON kiểu này thế nào?

Hoàn toàn tương tự như bình thường, nhưng dùng cùng JsonSerializerOptions:


var deserializedAuthor = JsonSerializer.Deserialize<Author>(json, options);

5. Còn Newtonsoft.Json (Json.NET) thì sao?

Lịch sử thì Newtonsoft.Json xử lý vòng sớm hơn System.Text.Json. Có attribute [JsonObject(IsReference = true)] và cài đặt serialization global.

Attributes cho reference


[JsonObject(IsReference = true)]
public class Author
{
    public string Name { get; set; }
    public List<Book> Books { get; set; }
}

[JsonObject(IsReference = true)]
public class Book
{
    public string Title { get; set; }
    public Author Author { get; set; }
}

Rồi serialize như sau:


var settings = new JsonSerializerSettings
{
    PreserveReferencesHandling = PreserveReferencesHandling.Objects,
    Formatting = Formatting.Indented
};

string json = JsonConvert.SerializeObject(author, settings);

Kết quả là JSON có $id và $ref, tương tự chế độ ReferenceHandler.Preserve.

Tóm nhanh

  • Nếu trao đổi giữa các ứng dụng .NET — bật reference serialization (ReferenceHandler.Preserve hoặc PreserveReferencesHandling).
  • Nếu data gửi tới JavaScript/khách khác — phá vòng: [JsonIgnore], dọn tham chiếu hoặc chuyển sang Id.

6. Làm sao tránh lỗi và headache

Rất nhiều người mới (và cả người có kinh nghiệm) gặp crash của serializer vì vòng. Nhớ: nếu collection/property trỏ lẫn nhau — xem lại kiến trúc model.

Đừng ngại dùng [JsonIgnore] cho property không cần trao đổi bên ngoài.

Hố bẫy kinh điển — serialize các quan hệ "many-to-many" (ví dụ students ↔ courses). Nếu không phá vòng hoặc bật reference serialization thì không chạy được.

Trong REST API thường gửi object "một chiều": ví dụ book biết author, còn author chỉ giữ Id của sách (hoặc hoàn toàn không biết về sách trong contract này).

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Tạo đối tượng với tham chiếu vòng lặp
Tạo đối tượng với tham chiếu vòng lặp
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION