CodeGym /Các khóa học /C# SELF /Giới thiệu về Source Gener...

Giới thiệu về Source Generators

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

1. Giới thiệu

Khi bạn viết code, chắc đôi khi bạn tự nghĩ: "Sao mình lại phải copy cùng một mẫu code cho nhiều class khác nhau thế nhỉ?" Hoặc: "Sao việc serialization, logging, mapping dữ liệu lại có quá nhiều dòng giống nhau?" Thỉnh thoảng thật muốn có ai đó (hoặc cái gì đó) viết phần code lặp đi lặp lại đó giúp mình.

Đó chính là lúc Source Generators xuất hiện — một tính năng mới trong C#, có từ .NET 5 và vẫn phát triển tích cực. Source Generator là một thư viện chạy ở thời điểm biên dịch và có thể sinh động tạo mã C# rồi nhúng vào dự án của bạn trước khi build cuối cùng.

Tại sao cần nó?

  • Tự động hóa phần việc lặp lại: Giúp bạn khỏi phải viết các class/method cùng kiểu (boilerplate).
  • An toàn được trình biên dịch kiểm tra: Mã sinh ra được compile cùng với mã của bạn (khác với T4 hoặc reflection).
  • Hiệu năng cao: Serialization, DI, mapping... hoạt động mà không tốn chi phí reflection ở thời gian chạy (runtime).
  • Hỗ trợ các pattern hiện đại: Triển khai các ý tưởng từng khó/đắt nếu không có code generation.

Source Generators hoạt động “bên dưới nắp” như thế nào?

Source Generator là một thư viện .NET (thường là project kiểu Class Library), hiện thực giao diện ISourceGenerator. Trong quá trình biên dịch, Roslyn chạy tất cả các generator được liên kết, cung cấp cho chúng quyền truy cập vào cây cú pháp của mã nguồn.

Generator phân tích mã của bạn, quyết định cần sinh gì và ở đâu rồi tạo các file C# mới, những file đó sẽ được compiler biên dịch ngay lập tức.

Tự động sinh ToString

Bắt đầu từ ví dụ đơn giản. Giả sử chúng ta có một class với nhiều property và cần triển khai ToString. Viết tay sẽ trông như sau:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public override string ToString()
        => $"Person(Name={Name}, Age={Age})";
}

Nhưng nếu số property tăng lên nhiều — nhìn thôi đã thấy chán, và rất dễ quên cập nhật. Source Generator có thể làm việc này thay bạn!

2. Làm sao để tạo Source Generator của riêng bạn?

Tạo project

Mở JetBrains Rider hoặc Visual Studio, tạo project mới kiểu Class Library (.NET Standard) — thường project như vậy phù hợp làm generator. Sau đó thêm các package NuGet:

  • Microsoft.CodeAnalysis.CSharp
  • Microsoft.CodeAnalysis.Analyzers

Các attribute quan trọng

  • [Generator] — chỉ ra rằng lớp này là một Source Generator.

Mẫu generator đơn giản nhất

Đây là ví dụ tối thiểu hoạt động:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // Có thể đăng ký các hành động bổ sung (không bắt buộc)
    }

    public void Execute(GeneratorExecutionContext context)
    {
        var code = @"
namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => ""Chào thế giới! Mình được tạo tự động!"";
    }
}";
        context.AddSource("HelloWorldGenerator", SourceText.From(code, Encoding.UTF8));
    }
}

Generator đơn giản này luôn thêm class tĩnh HelloWorld với phương thức SayHello khi biên dịch.

Làm sao dùng Source Generators trong ứng dụng chính?

Kết nối project generator như một gói NuGet hoặc Project Reference trong phần Analyzer (xem thêm — tài liệu chính thức).

Mã được sinh có sẵn ngay trong project — không cần include thêm gì, chỉ việc sử dụng:

// Đây sẽ được sinh tự động!
using Generated;

Console.WriteLine(HelloWorld.SayHello());

3. Ví dụ thực tế: tự động sinh ToString

Giả sử chúng ta muốn mọi class được đánh dấu bằng attribute [AutoToString] tự động có implementation của ToString. Cần làm:

  • Tạo attribute của riêng bạn.
  • Phân tích tất cả class có attribute đó.
  • Cho mỗi class như vậy sinh phương thức ToString.

Attribute

[AttributeUsage(AttributeTargets.Class)]
public class AutoToStringAttribute : Attribute
{
}

Sử dụng trong code

[AutoToString]
public class Product
{
    public string Name { get; set; }
    public int Price { get; set; }
}

Logic sinh mã đơn giản

Generator sẽ tìm các class có [AutoToString] và sinh mã xấp xỉ như sau:

public override string ToString() 
    => $"Product(Name={Name}, Price={Price})";

Một đoạn mã thực tế của generator

Ý tưởng cơ bản — duyệt cây cú pháp với Roslyn:

public void Execute(GeneratorExecutionContext context)
{
    // Phân tích tất cả syntax tree
    foreach (var tree in context.Compilation.SyntaxTrees)
    {
        var root = tree.GetRoot();
        // Tìm tất cả class có attribute cần thiết (ví dụ đơn giản!)
        var classes = root.DescendantNodes()
            .OfType<ClassDeclarationSyntax>()
            .Where(c => c.AttributeLists
                         .SelectMany(al => al.Attributes)
                         .Any(a => a.Name.ToString().Contains("AutoToString")));

        foreach (var @class in classes)
        {
            var className = @class.Identifier.Text;
            // Lấy tất cả property của class
            var props = @class.Members
                .OfType<PropertyDeclarationSyntax>()
                .Select(p => p.Identifier.Text)
                .ToArray();

            var toStringCode = string.Join(", ", props.Select(p => $"{p}={{this.{p}}}"));
            var generated = $@"
partial class {className}
{{
    public override string ToString() => $""{className}({toStringCode})"";
}}";

            context.AddSource($"{className}_ToString", SourceText.From(generated, Encoding.UTF8));
        }
    }
}

Lưu ý: trong production người ta thường dùng phân tích chính xác hơn thông qua SemanticModel của Roslyn.

4. Những điểm hữu ích cần biết

Chú ý điều gì

Source Generators không thể sửa mã nguồn hiện có — chỉ được tạo các file mới (ví dụ các class/method partial bổ sung, v.v.). Điều này có nghĩa nếu class của bạn được khai báo là partial, bạn có thể sinh thêm method hoặc property cho nó.

Đôi khi khá khó để parse cú pháp chính xác và xử lý mọi góc cạnh của ngôn ngữ (nested classes, generics, modifiers, v.v.). Tác giả generator phải đảm bảo mã sinh ra compile được và không "làm hỏng" project.

Một cái bẫy nữa — nếu bạn sinh các phương thức để implement interface, đảm bảo file được sinh lại ở mỗi lần build. Nếu không sẽ có lỗi compile lạ. Công cụ hiện đại phần lớn xử lý điều này, nhưng nhớ về nó là quan trọng.

Source Generators vs. reflection

Reflection: chạy ở runtime, tốn tài nguyên, không được compiler kiểm tra, thường chậm với lượng lớn dữ liệu.

Source Generator: sinh mã ở thời điểm compile. Mọi thứ được kiểm tra tĩnh, IDE thấy được method, autocomplete hoạt động, và hiệu năng như mã C# bình thường.

Lợi ích thực tế

  • System.Text.Json: sinh mã serialization/deserialization mà không cần reflection.
  • Thiết kế container DI: ví dụ Microsoft.Extensions.DependencyInjection với generation graph phụ thuộc.
  • Mapper như Mapster: chuyển từ reflection sang generation mapping ở compile-time.
  • Test frameworks: tự sinh test method dựa trên attribute.
  • ASP.NET Minimal APIs (từ .NET 7): sinh endpoint handler.

Cấu hình, tham số và tuỳ chọn

Generator có thể cấu hình qua tham số MSBuild, file bổ sung và convention. Ví dụ có thể sinh ToString khác nhau tuỳ environment (Debug/Release) hoặc cấu hình ứng dụng.

Source Generators liên quan tới công việc thực tế như thế nào

Với dev muốn code sạch và nhanh, đây là công cụ xịn: ít việc lặp lại hơn, nhiều kiểm tra ở compile-time hơn, IDE cho gợi ý và refactor rõ ràng. Biết cách dùng generator giờ đây thường xuất hiện trong phỏng vấn — từ serialization, DI đến mapping.

Vòng đời của Source Generator

Giai đoạn Chuyện gì xảy ra
1. Project được kết nối Generator của bạn được thêm như analyzer/reference
2. Roslyn biên dịch mã nguồn Generator nhận được AST (cây cú pháp trừu tượng)
3. Generator được chạy Thêm các file .cs mới để biên dịch
4. Mọi thứ được biên dịch Các file sinh ra trở thành một phần của assembly của bạn
5. Mã sẵn sàng! Method/class sinh ra có thể gọi được

5. Debug và lỗi thường gặp

Một trong những lỗi phổ biến của người mới viết generator — quên từ khóa partial ở class mà bạn muốn thêm code vào. Nếu không để partial, compiler sẽ không thấy phần code bạn sinh. Đôi khi file sinh ra không được IDE nhận ngay cho tới khi bạn build lại — đừng hoảng.

Cẩn thận khi đặt tên file sinh ra: nếu đặt cùng một Name cho tất cả, chúng sẽ ghi đè nhau. Mẹo: chèn tên class vào tên file, ví dụ context.AddSource($"{className}_ToString", ...).

Lỗi do duplicate attribute import — nếu bạn sinh ra một attribute đã tồn tại trong project chính sẽ gây conflict. Tốt hơn là để attribute cần thiết vào project chung hoặc chỉ sinh code khi thực sự cần.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
Tự động sinh phương thức ToString
Tự động sinh phương thức ToString
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION