CodeGym /Các khóa học /C# SELF /Source Generators và...

Source GeneratorsSystem.Reflection.Emit

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

1. Giới thiệu

Nói thật nhé: đa số mọi người nghĩ về việc sinh code gần giống như máy cắt trứng — hữu ích nhưng hiếm khi thực sự cần thiết. Tuy nhiên các project .NET hiện đại ngày càng phức tạp, và tự động hóa công việc lặp / theo mẫu trong code không phải là "một cái hay", mà là công cụ quan trọng để nâng chất lượng và năng suất.

Source Generators là cơ chế xuất hiện từ C# 9 và .NET 5. Chúng giống siêu anh hùng ở giai đoạn biên dịch: có thể sinh mã C# rồi mã đó được biên dịch như một phần của project. Chúng không can thiệp vào assembly đã biên dịch tại runtime, mà chỉ mở rộng project bằng các nguồn mới trước khi biên dịch.

Dưới đây là các kịch bản thường gặp khi dùng Source Generators.

Tạo code mẫu tự động

Trong nhiều project bạn phải viết code cùng một kiểu: constructors, phương thức ToString, serializers, property change notification (INotifyPropertyChanged) v.v. Source Generators có thể sinh phần này tự động, giải phóng dev khỏi việc lặp lại và lỗi gõ tay.

Ví dụ: Generator cho ToString

Giả sử bạn viết các lớp DTO (Data Transfer Object) để trao đổi dữ liệu. Mỗi lớp cần một ToString có ý nghĩa, liệt kê các property.

Thay vì copy tay, bạn có thể làm một Source Generator mà với mỗi lớp có attribute [AutoToString] sẽ sinh implementation của ToString. Ví dụ thư viện AutoToString làm như vậy.


// Lớp của bạn với attribute
[AutoToString] 
public partial class Person 
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// Source Generator sẽ sinh (đơn giản hóa):
public partial class Person
{
    public override string ToString() => $"Person: Name={Name}, Age={Age}";
}

Ưu điểm: code luôn readable và tự cập nhật khi thêm property mới.

Đơn giản hóa serialization và deserialization

Source Generators được dùng rộng rãi bên trong thư viện chuẩn System.Text.Json để sinh mã nhanh cho serialization/deserialization. Trước khi có generators, serialization thường dựa vào reflection (chậm), còn giờ thì mã sinh sẵn làm mọi thứ nhanh và an toàn.

Dev được lợi gì? Chỉ cần đánh dấu [JsonSerializable(typeof(MyType))] — generator sẽ tạo mã serialization hiệu năng cao cho kiểu đó.

Sinh mã cho config, mapper, DI container

  • Config: generator tự động tạo classes cấu hình dựa trên file JSON.
  • Mappers: ví dụ project Mapster dùng generator để tạo mapping giữa các kiểu mà không cần copy tay.
  • Dependency Injection: một số container (ví dụ StrongInject) dùng generator để sinh code đăng ký service.

Tích hợp với hạ tầng bên ngoài

Một số generator phân tích tài nguyên ngoại vi (mô tả API, schema GraphQL, protocol Thrift, v.v.) và sinh lớp C# để làm việc với chúng. Điều này tránh việc cập nhật tay khi contract thay đổi.

Kiểm tra và chẩn đoán ở thời điểm biên dịch

Source Generators có thể dùng để kiểm tra code: "Nếu trong project xuất hiện method X nhưng không thỏa điều kiện Y — cảnh báo ở compiler!". Nhiều linter và analyzer hoạt động kiểu này, và generator có thể thêm message hoặc thậm chí chèn mã stub sẵn.

2. Viết Source Generator cho serialization tự động

Xem một ví dụ nhỏ kiểu "thẳng vào vấn đề": generator mà với lớp có attribute [AutoJson] sẽ sinh method để serialize thành JSON.

Mã dưới đây chỉ minh họa; trong thực tế hãy dùng System.Text.Json.SourceGeneration.


// Viết tay:
[AutoJson]
public partial class Book
{
    public string Title { get; set; }
    public int Year { get; set; }
}

// Generator sẽ thêm:
public partial class Book
{
    public string ToJson() => $"{{ \"Title\": \"{Title}\", \"Year\": {Year} }}";
}

Sơ đồ tương tác giữa Source Generator và project

flowchart TD
    A(Source Code của bạn) -->|Trình biên dịch gọi| B(Source Generator)
    B -->|Mã được thêm| C(Các file .cs mới)
    C --> D(Biên dịch dự án)
  • Đầu tiên trình biên dịch (Roslyn) phân tích source của bạn.
  • Sau đó gọi "generator" của bạn (cài ISourceGenerator).
  • Generator thêm các file .cs mới, chúng trở thành một phần của cây biên dịch chung.
  • Kết quả là assembly chứa cả mã của bạn lẫn mã được sinh.

Các nhiệm vụ chuyên biệt: còn gì có thể sinh được

  • Bindings cho native libs (wrapper cho C hoặc WinAPI).
  • AOP: tự động log gọi hàm, aspects (kiểu Fody, PostSharp v.v.).
  • Mô tả API cho generator docs tự động.
  • Xử lý tài nguyên ngoài: SVG, SQL, Razor — generator tạo các lớp strongly-typed để truy cập an toàn về kiểu.

3. Sinh mã động với System.Reflection.Emit

Nếu Source Generators sinh C# trước khi build thì System.Reflection.Emit là công cụ để làm "phép thuật" vào lúc chạy. Chương trình của bạn có thể tự tạo kiểu mới, method và thậm chí assembly — ngay trên fly!

Nghe ghê hả? Một chút thôi. Nhưng đôi khi không có cách nào khác: ví dụ khi bạn viết framework cho dynamic proxy (AOP, profiling, mocking), generator serializer dựa trên dữ liệu runtime, dynamic ORM, v.v.

Khi nào cần Reflection.Emit

  • Kiểu không biết trước (người dùng định nghĩa cấu trúc trên fly).
  • Proxy động (wrapper để bắt các lời gọi).
  • Serialization hiệu năng cao (ví dụ protobuf-net).
  • Plugin và script engine với kịch bản load phức tạp.

Điều gì có thể tạo bằng Reflection.Emit

  • AssemblyBuilder — tạo assembly mới.
  • ModuleBuilder — module trong assembly.
  • TypeBuilder — mô tả kiểu mới.
  • MethodBuilder — method với IL code.
  • PropertyBuilder, FieldBuilder, EventBuilder — property, field, event.

Mini ví dụ: tạo class mới trên fly

using System;
using System.Reflection;
using System.Reflection.Emit;

public static class DynamicTypeGenerator
{
    public static Type GenerateSimpleType(string typeName)
    {
        // 1. Tạo assembly và module
        var assemblyName = new AssemblyName("DynamicAssembly");
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");

        // 2. Tạo class mới
        var typeBuilder = moduleBuilder.DefineType(
            typeName, 
            TypeAttributes.Public | TypeAttributes.Class
        );

        // 3. Thêm property chuỗi công khai Title
        var field = typeBuilder.DefineField("_title", typeof(string), FieldAttributes.Private);
        var prop = typeBuilder.DefineProperty("Title", PropertyAttributes.HasDefault, typeof(string), null);
        var getMethod = typeBuilder.DefineMethod("get_Title", MethodAttributes.Public, typeof(string), Type.EmptyTypes);
        var il = getMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);      // this
        il.Emit(OpCodes.Ldfld, field); // _title
        il.Emit(OpCodes.Ret);
        prop.SetGetMethod(getMethod);

        var setMethod = typeBuilder.DefineMethod("set_Title", MethodAttributes.Public, null, new[] { typeof(string) });
        il = setMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Stfld, field);
        il.Emit(OpCodes.Ret);
        prop.SetSetMethod(setMethod);

        // 4. Xong! Tạo Type
        return typeBuilder.CreateTypeInfo();
    }
}

Bây giờ bạn có thể dùng kiểu này như object C# bình thường, ví dụ qua reflection:

var dynamicType = DynamicTypeGenerator.GenerateSimpleType("Book");
var obj = Activator.CreateInstance(dynamicType);
dynamicType.GetProperty("Title").SetValue(obj, "C# v lichakh");
Console.WriteLine(dynamicType.GetProperty("Title").GetValue(obj)); // C# v lichakh

Đó là cách ORM, proxy, serializer, profiler và một số test framework hoạt động.

4. Những chi tiết hữu ích

Source Generators vs Reflection.Emit: ai thắng?

Source Generators chạy ở thời điểm biên dịch: làm cho source thông minh hơn, và kết quả nằm trong assembly đã biên dịch. Không thể dùng chúng để sinh mã dựa trên dữ liệu chỉ có ở runtime.

Reflection.Emit chạy ở runtime: cho phép tạo assembly, kiểu và phương thức động, nhưng mã đó khó debug và maintain hơn.

Khi nào dùng gì?

So sánh đời thường:

  • Source Generators — như nhà máy, sản xuất các chi tiết sẵn trước khi lắp xe.
  • Reflection.Emit — như kỹ sư hàn thêm động cơ tên lửa vào xe đang chạy.

Đặc điểm và caveat

  • Mã được sinh có thể khó debug. Thường generator có option lưu nguồn ra đĩa — tìm trong thư mục obj\Generated.
  • Reflection.Emit tạo assembly trong memory, chúng không được unload khỏi AppDomain. Dùng AssemblyBuilderAccess.RunAndCollect cho assembly tạm (nếu được hỗ trợ).
  • Đừng lạm dụng Reflection.Emit cho việc đơn giản — đôi khi Source Generator hoặc template code bình thường đơn giản và bền hơn.
  • Generator đòi hiểu Roslyn (với Source Generators) và IL (với Reflection.Emit).

Source GeneratorsReflection.Emit

Tiêu chí Source Generators Reflection.Emit
Được sử dụng Ở thời điểm biên dịch Ở runtime
Kết quả C#-source, là một phần của assembly IL-code, kiểu/assembly mới
Kịch bản thường Tự sinh code mẫu, DI, mapping, serialization Proxy, dynamic ORM, pipeline runtime đặc thù
Độ khó sử dụng Trung bình, cần hiểu Roslyn Cao, cần hiểu IL
Hỗ trợ IDE & debug Tốt (thấy source) Khó
Hiệu năng Rất cao Có thể cao

5. Kịch bản thực tế

1. Sinh API strongly-typed

Tổ chức cung cấp OpenAPI spec. Generator phân tích nó và tạo controller classes, DTO và client code để làm việc với REST API — type-safe và hỗ trợ IntelliSense.

Mã (giả):
// Spec: GET /users -> returns User[]
// Source Generator sẽ sinh:
public class ApiClient
{
    public Task<User[]> GetUsersAsync() { ... }
}

2. Injection/DI tự động (Compile-time IoC)

Generator tạo builder để đăng ký dependency và dựng graph đối tượng. Không cần viết tay services.AddSingleton<IMyService, MyService>().

Mã (giả):
[Injectable]
public class MyService : IMyService { ... }

// Source Generator sẽ sinh:
partial class DIContainer
{
    public void RegisterServices()
    {
        AddSingleton<IMyService, MyService>();
    }
}

3. Proxy động — ví dụ với Reflection.Emit

Thư viện Castle DynamicProxy dựng proxy types bắt lời gọi method — nền tảng cho AOP, logging, tracing, mocking.

Mã (đơn giản):
public interface IBookService { string GetBook(); }
public class BookService : IBookService { public string GetBook() => "C#"; }

var proxy = ProxyGenerator.CreateProxy<IBookService>(new BookService(), interceptor);
proxy.GetBook(); // Lời gọi bị bắt, có thể log/điều chỉnh kết quả

4. Serialization nhanh không dùng reflection

Thay vì dựng mô tả kiểu bằng reflection ở runtime (chậm), Source Generators có thể sinh code serialize/deserialize trước — tối ưu tốc độ và không có overhead.

1
Khảo sát/đố vui
, cấp độ , bài học
Không có sẵn
Phản chiếu
Phản chiếu và kiểu động
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION