Chuyển tới nội dung chính

Attributes and Reflection

Định nghĩa (Definition)

Thuộc tính (Attribute) là các thẻ siêu dữ liệu (Metadata) mang tính khai báo được áp dụng lên các thực thể chương trình (assembly, kiểu, phương thức, thuộc tính, v.v.) để truyền đạt thông tin cho trình biên dịch, thời gian chạy (Runtime) hoặc các công cụ. Chúng không thay đổi logic chương trình trực tiếp nhưng có thể được truy vấn và xử lý.

Phản chiếu (Reflection) là khả năng kiểm tra và thao tác siêu dữ liệu kiểu (Type Metadata) tại thời điểm chạy (Runtime). Thông qua System.Reflection, mã có thể khám phá các kiểu, gọi phương thức, truy cập thuộc tính và kiểm tra thuộc tính (Attribute) một cách động (Dynamically).

Thuộc tính (Attribute) và phản chiếu (Reflection) có liên kết chặt chẽ: thuộc tính nhúng siêu dữ liệu, và phản chiếu đọc siêu dữ liệu đó.

// Attributes add metadata
[Obsolete("Use ProcessAsync instead.")]
public void Process(int id) { }

// Reflection reads that metadata at runtime
var method = typeof(MyService).GetMethod(nameof(Process));
var attr = method?.GetCustomAttribute<ObsoleteAttribute>();
Console.WriteLine(attr?.Message); // "Use ProcessAsync instead."

Khái niệm cốt lõi (Core Concepts)

Thuộc tính tích hợp sẵn (Built-in Attributes)

Thuộc tính (Attribute)Mục đíchVí dụ
[Obsolete]Đánh dấu thành phần đã lỗi thời (Deprecated)[Obsolete("Use NewMethod")]
[Serializable]Đánh dấu kiểu để tuần tự hóa nhị phân (Binary Serialization)[Serializable] class Config { }
[NonSerialized]Loại trừ trường khỏi tuần tự hóa (Serialization)[NonSerialized] private string _temp;
[Flags]Đánh dấu enum là trường bit (Bit Field)[Flags] enum Perms { Read=1, Write=2 }
[Conditional]Biên dịch có điều kiện các lệnh gọi phương thức[Conditional("DEBUG")]
[DllImport]Nhập hàm từ DLL không quản lý (Unmanaged DLL)[DllImport("user32.dll")]
// [Obsolete] — warns or errors on usage
[Obsolete("Use ProcessOrderAsync instead.", error: false)]
public void ProcessOrder(int orderId) { }

// [Flags] — enables bitwise operations and correct ToString()
[Flags]
public enum Permissions
{
None = 0,
Read = 1,
Write = 2,
Execute = 4,
All = Read | Write | Execute
}
var perms = Permissions.Read | Permissions.Write;
Console.WriteLine(perms); // "Read, Write"

// [Conditional] — calls compiled only when symbol is defined
[Conditional("DEBUG")]
public void LogDebug(string message) => Console.WriteLine(message);
// In Release builds, all calls to LogDebug are removed

// [DllImport] — P/Invoke
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
static extern int MessageBox(IntPtr hWnd, string text, string caption, int type);

Thuộc tính tùy chỉnh (Custom Attributes)

Tạo thuộc tính tùy chỉnh bằng cách kế thừa từ System.Attribute. Sử dụng [AttributeUsage] để kiểm soát nơi nó có thể được áp dụng:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class ValidateRangeAttribute : Attribute
{
public int Min { get; }
public int Max { get; }
public string? ErrorMessage { get; set; }

public ValidateRangeAttribute(int min, int max)
{
Min = min;
Max = max;
}
}

// Applying the custom attribute
public class Product
{
[ValidateRange(0, 9999, ErrorMessage = "Price must be 0-9999")]
public int Price { get; set; }

[ValidateRange(1, 10000)]
public int Quantity { get; set; }
}

Các tham số của [AttributeUsage]:

Tham sốKiểu (Type)Giá trị mặc địnhMô tả
ValidOnAttributeTargetsAllThành phần nào thuộc tính có thể nhắm tới
AllowMultipleboolfalseCó thể áp dụng thuộc tính nhiều lần hay không
InheritedbooltrueLớp kế thừa có được thừa hưởng thuộc tính hay không
Quy ước đặt tên (Naming Convention)

Theo quy ước, tên lớp thuộc tính (Attribute) kết thúc bằng Attribute. Khi áp dụng thuộc tính, hậu tố có thể được bỏ qua: [ValidateRange] là cách viết tắt của [ValidateRangeAttribute].

Truy vấn Thuộc tính bằng Phản chiếu (Querying Attributes with Reflection)

Sử dụng phản chiếu (Reflection) để đọc thuộc tính (Attribute) tại thời điểm chạy (Runtime):

using System.Reflection;

// Get a specific attribute
var prop = typeof(Product).GetProperty(nameof(Product.Price))!;
var attr = prop.GetCustomAttribute<ValidateRangeAttribute>();
if (attr is not null)
Console.WriteLine($"Range: {attr.Min} to {attr.Max}, Error: {attr.ErrorMessage}");

// Check if an attribute is defined
bool hasRange = prop.IsDefined(typeof(ValidateRangeAttribute));

// Get all attributes on a type
var typeAttrs = typeof(Product).GetCustomAttributes();

// Get all properties with a specific attribute
var validatedProps = typeof(Product)
.GetProperties()
.Where(p => p.IsDefined(typeof(ValidateRangeAttribute)))
.ToList();

Công cụ xác thực đơn giản sử dụng thuộc tính (Attribute) + phản chiếu (Reflection):

public static List<string> Validate(object obj)
{
var errors = new List<string>();
foreach (var prop in obj.GetType().GetProperties())
{
var attr = prop.GetCustomAttribute<ValidateRangeAttribute>();
if (attr is null) continue;

int value = (int)prop.GetValue(obj)!;
if (value < attr.Min || value > attr.Max)
errors.Add($"{prop.Name}: {attr.ErrorMessage ?? $"Must be {attr.Min}-{attr.Max}"}");
}
return errors;
}

var product = new Product { Price = -5, Quantity = 0 };
var errors = Validate(product);
// ["Price: Price must be 0-9999"]

Phản chiếu cơ bản (Reflection Basics)

Không gian tên System.Reflection cung cấp các kiểu để kiểm tra assembly, kiểu và thành phần tại thời điểm chạy (Runtime).

using System.Reflection;

// --- Getting a Type ---
Type type = typeof(Product); // From type name
Type type2 = product.GetType(); // From instance
Type? type3 = Type.GetType("MyApp.Product"); // From string (needs assembly-qualified name for external)

// --- Exploring a Type ---
PropertyInfo[] props = type.GetProperties();
MethodInfo[] methods = type.GetMethods();
FieldInfo[] fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);
ConstructorInfo[] ctors = type.GetConstructors();

// --- Getting and setting values ---
object? value = prop.GetValue(product); // Read
prop.SetValue(product, 42); // Write

// --- Invoking methods ---
MethodInfo method = type.GetMethod("ToString")!;
string? result = (string?)method.Invoke(product, null);

// --- Creating instances ---
object? instance = Activator.CreateInstance(type); // Parameterless
object? instance2 = Activator.CreateInstance(type, args); // With parameters

Cờ liên kết (Binding Flags) kiểm soát thành phần nào phản chiếu (Reflection) tìm thấy:

Cờ (Flag)Những gì nó bao gồm
BindingFlags.PublicThành phần công khai (Public Members)
BindingFlags.NonPublicThành phần private, protected, internal
BindingFlags.InstanceThành phần thực thể (Instance Members)
BindingFlags.StaticThành phần tĩnh (Static Members)
BindingFlags.FlattenHierarchyThành phần tĩnh lên chuỗi kế thừa (Inheritance Chain)

Bạn phải kết hợp Public/NonPublic với Instance/Static — chúng không phải là bộ lọc tùy chọn.

Liên kết muộn bằng Phản chiếu (Late Binding with Reflection)

Gọi phương thức và tạo thực thể khi kiểu không được biết tại thời điểm biên dịch (Compile Time):

// Plugin-style: load type by name
Type? type = Type.GetType("MyApp.Services.EmailService, MyApp");
if (type is null) throw new TypeLoadException("EmailService not found");

object? instance = Activator.CreateInstance(type);
MethodInfo? method = type.GetMethod("Send");
method?.Invoke(instance, new object[] { "Hello", "recipient@example.com" });

// Generic method invocation
MethodInfo? generic = type.GetMethod("Process")?.MakeGenericMethod(typeof(string));
generic?.Invoke(instance, new object[] { "data" });

// Loading an external assembly
Assembly assembly = Assembly.LoadFrom("plugins/MyPlugin.dll");
Type[] types = assembly.GetTypes();
foreach (Type t in types)
{
if (typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract)
{
IPlugin plugin = (IPlugin)Activator.CreateInstance(t)!;
plugin.Initialize();
}
}

Hiệu suất của Phản chiếu (Reflection Performance)

Phản chiếu (Reflection) chậm đáng kể so với lệnh gọi trực tiếp. Chi phí điển hình:

Thao tác (Operation)Chi phí tương đối (Relative Cost)
Lệnh gọi phương thức trực tiếp (Direct Method Call)1x (cơ sở)
MethodInfo.Invoke đã lưu vào bộ đệm (Cached)10-50x
Activator.CreateInstance10-20x
GetProperty + GetValue20-100x

Chiến lược giảm thiểu (Mitigation Strategies):

// 1. Cache reflection objects (never re-lookup in a loop)
private static readonly MethodInfo _method = typeof(MyClass).GetMethod("Process")!;

// 2. Compile expression trees for repeated invocation
var param = Expression.Parameter(typeof(MyClass), "obj");
var call = Expression.Call(param, _method);
var func = Expression.Lambda<Func<MyClass, string>>(call, param).Compile();

// Now func(instance) is nearly as fast as a direct call
string result = func(myInstance);

// 3. Use Delegate.CreateDelegate for method invocation
var del = Delegate.CreateDelegate(typeof(Func<MyClass, string>), null, _method);
var typedDel = (Func<MyClass, string>)del;
string result2 = typedDel(myInstance);
Tránh phản chiếu trong các đường dẫn nóng (Hot Paths)

Không bao giờ sử dụng phản chiếu (Reflection) trong các vòng lặp chặt hoặc mỗi yêu cầu trong các API có thông lượng cao. Hãy lưu vào bộ đệm các lượt tra cứu, biên dịch cây biểu thức (Expression Trees), hoặc sử dụng trình tạo mã nguồn (Source Generators) thay thế.

Trình tạo Mã nguồn như Giải pháp Hiện đại (Source Generators as Modern Alternative)

Trình tạo mã nguồn (Source Generators) (C# 9 / .NET 5+) là các trình tạo mã tại thời điểm biên dịch (Compile-Time) kiểm tra mã của bạn thông qua Roslyn và tạo ra các tệp mã nguồn bổ sung. Chúng loại bỏ nhiều trường hợp sử dụng phản chiếu (Reflection) tại thời điểm chạy (Runtime).

// System.Text.Json source generator
[JsonSerializable(typeof(Product))]
public partial class ProductContext : JsonSerializerContext { }

// Compile-time generated serialization — no reflection at runtime
string json = JsonSerializer.Serialize(product, ProductContext.Default.Product);

Ưu điểm so với phản chiếu (Reflection):

Khía cạnh (Aspect)Phản chiếu (Reflection)Trình tạo Mã nguồn (Source Generators)
Thời điểm tạo mã (When code is generated)Thời điểm chạy (Runtime)Thời điểm biên dịch (Compile Time)
Hiệu suất (Performance)Chậm (liên kết muộn)Nhanh (lệnh gọi trực tiếp)
Khả năng tương thích AOTThường không tương thíchHoàn toàn tương thích
An toàn kiểu (Type Safety)Lỗi tại thời điểm chạyLỗi tại thời điểm biên dịch
Hỗ trợ IDEKhông có (chuỗi)IntelliSense đầy đủ

Các trường hợp sử dụng trình tạo mã nguồn (Source Generator) phổ biến:

  • Tuần tự hóa System.Text.Json ([JsonSerializable])
  • Ghi nhật ký tại thời điểm biên dịch của Microsoft.Extensions.Logging
  • Ánh xạ ORM (mô hình biên dịch của EF Core)
  • Đăng ký tiêm phụ thuộc (Dependency Injection)
  • Tuần tự hóa bộ đệm giao thức (Protocol Buffer)

Xem Modern C# để biết về thuộc tính một phần (Partial Properties) và các mẫu trình tạo mã nguồn (Source Generator Patterns).

Khi nào sử dụng (When to Use)

Tình huống (Scenario)Sử dụng Thuộc tính (Attribute) + Phản chiếu (Reflection)?
Quy tắc xác thực khai báo (Declarative Validation Rules)Có (thuộc tính tùy chỉnh + công cụ xác thực)
Khám phá và tải plugin (Plugin Discovery)Có (Assembly.LoadFrom, phản chiếu)
Ánh xạ cột/bảng ORM (ORM Mapping)Có (hoặc trình tạo mã nguồn)
Cấu hình tuần tự hóa (Serialization Configuration)Có (hoặc trình tạo mã nguồn)
Khám phá phương thức của framework kiểm thử (Test Framework)Có (trình chạy kiểm thử sử dụng phản chiếu)
Đường dẫn nóng quan trọng hiệu suất (Performance-critical Hot Paths)Không — sử dụng trình tạo mã nguồn hoặc biểu thức đã biên dịch
Cấu hình đơn giản (Simple Configuration)Không — sử dụng appsettings.json hoặc hằng số thông thường

Lỗi thường gặp (Common Pitfalls)

  1. Chi phí hiệu suất (Performance Overhead) — Phản chiếu (Reflection) chậm. Luôn lưu vào bộ đệm các đối tượng Type, MethodInfo, và PropertyInfo. Cân nhắc biên dịch cây biểu thức (Expression Trees) cho truy cập lặp lại.

  2. Phá vỡ tái cấu trúc âm thầm (Breaking Refactoring Silently) — Đổi tên phương thức được tham chiếu bằng chuỗi trong GetMethod("Name") gây ra lỗi tại thời điểm chạy mà không có cảnh báo tại thời điểm biên dịch. Sử dụng nameof() khi có thể.

  3. Rủi ro bảo mật (Security Risks) — Phản chiếu có thể truy cập thành phần private. Hãy thận trọng trong các môi trường hộp cát (Sandboxed) hoặc nhạy cảm về bảo mật.

  4. Lạm dụng thuộc tính (Attribute Misuse) — Thuộc tính (Attribute) nên mã hóa siêu dữ liệu, không phải logic. Logic phụ thuộc vào thuộc tính nên nằm trong mã truy vấn chúng, không phải bên trong lớp thuộc tính.

  5. Thiếu kiểm tra null (Missing Null Checks)GetMethod, GetProperty, và Type.GetType trả về null nếu không tìm thấy thành phần. Luôn kiểm tra null trước khi sử dụng kết quả.

Điểm chính (Key Takeaways)

  1. Thuộc tính (Attribute) thêm siêu dữ liệu vào các phần tử mã; phản chiếu (Reflection) đọc siêu dữ liệu đó tại thời điểm chạy (Runtime).
  2. Sử dụng [AttributeUsage] để kiểm soát nơi thuộc tính tùy chỉnh có thể xuất hiện.
  3. Type, MethodInfo, PropertyInfo, và Assembly là các kiểu phản chiếu cốt lõi.
  4. Phản chiếu mạnh mẽ nhưng chậm — lưu vào bộ đệm các lượt tra cứu hoặc biên dịch trước với cây biểu thức (Expression Trees).
  5. Trình tạo mã nguồn (Source Generators) là giải pháp thay thế hiện đại tại thời điểm biên dịch giúp loại bỏ nhiều trường hợp sử dụng phản chiếu tại thời điểm chạy.
  6. Luôn xác thực kết quả phản chiếu với nullGetMethod, GetProperty, và Type.GetType trả về null khi thất bại, không phải ngoại lệ.

Câu hỏi Phỏng vấn (Interview Questions)

Q: Thuộc tính (Attribute) trong C# là gì?

Thuộc tính là các thẻ siêu dữ liệu (Metadata) khai báo được áp dụng lên các phần tử chương trình bằng dấu ngoặc vuông (ví dụ: [Obsolete]). Chúng không thay đổi logic chương trình trực tiếp nhưng có thể được truy vấn tại thời điểm chạy (Runtime) thông qua phản chiếu (Reflection) hoặc được xử lý bởi trình biên dịch và các công cụ.

Q: Phản chiếu (Reflection) là gì và khi nào nên sử dụng?

Phản chiếu là khả năng kiểm tra siêu dữ liệu kiểu (Type Metadata) tại thời điểm chạy (Runtime) sử dụng System.Reflection. Nó cho phép bạn khám phá các kiểu, gọi phương thức, đọc thuộc tính và truy vấn thuộc tính (Attribute) một cách động (Dynamically). Các trường hợp sử dụng phổ biến bao gồm hệ thống plugin, tuần tự hóa (Serialization), framework xác thực và ánh xạ ORM.

Q: Làm thế nào để tạo thuộc tính tùy chỉnh (Custom Attribute)?

Kế thừa một lớp từ System.Attribute, áp dụng [AttributeUsage] để chỉ định mục tiêu hợp lệ và hành vi, đồng thời xác định các tham số constructor và thuộc tính. Theo quy ước, tên lớp kết thúc bằng Attribute, nhưng hậu tố được bỏ qua khi áp dụng.

Q: Tác động hiệu suất của phản chiếu (Reflection) là gì?

Phản chiếu chậm hơn 10-100 lần so với lệnh gọi trực tiếp. Giảm thiểu bằng cách lưu vào bộ đệm các đối tượng Type/MethodInfo, biên dịch cây biểu thức (Expression Trees), sử dụng Delegate.CreateDelegate, hoặc thay thế phản chiếu tại thời điểm chạy bằng trình tạo mã nguồn (Source Generators).

Q: Trình tạo mã nguồn (Source Generators) là gì và liên hệ thế nào với phản chiếu (Reflection)?

Trình tạo mã nguồn là các trình tạo mã tại thời điểm biên dịch (Compile-Time) kiểm tra chương trình của bạn sử dụng API Roslyn và tạo ra các tệp mã nguồn bổ sung. Chúng loại bỏ nhiều trường hợp sử dụng phản chiếu tại thời điểm chạy (tuần tự hóa, ánh xạ, đăng ký) bằng cách tạo mã tại thời điểm biên dịch, cung cấp hiệu suất tốt hơn và khả năng tương thích AOT.

Tài liệu tham khảo (References)