Tìm hiểu về Reflection trong .NET

Trong lập trình .NET, Reflection là một khái niệm quan trọng mà mỗi developer cần hiểu để tận dụng hết khả năng của ngôn ngữ lập trình C#.NET. Reflection cung cấp khả năng động hóa trong quá trình thực thi ứng dụng, cho phép bạn xem và tương tác với thông tin metadata của các loại (types) trong thời gian chạy.

Tìm hiểu về Reflection trong .NET

 

Giới thiệu về Reflection trong C#.NET

Trong lập trình hướng đối tượng (OOP), chúng ta định nghĩa các đặc điểm và hành vi của đối tượng bằng cách sử dụng các class, struct,… Dữ liệu mô tả các models, definitions, và các thành phần ứng dụng tương tự thường được biết đến chung là siêu dữ liệu (metadata). Trong C#, chúng ta có thể xử lý siêu dữ liệu này vào thời gian chạy thông qua một cơ chế tích hợp mạnh mẽ được gọi là Reflection.

Ví Dụ Thực Tế

Reflection trong lập trình .NET có thể giải quyết nhiều bài toán thực tế khác nhau. Dưới đây là một số ví dụ về cách Reflection có thể được ứng dụng trong các tình huống thực tế:
Dependency Injection Động: Bạn muốn thực hiện Dependency Injection một cách động, chẳng hạn khi bạn có nhiều services và chỉ muốn chọn loại phù hợp theo điều kiện chạy. Reflection cho phép bạn đọc thông tin về các loại services và tự động ánh xạ chúng vào ứng dụng của bạn mà không cần phải thay đổi mã nguồn.
Serialization và Deserialization Tự Động: Bạn muốn tự động chuyển đổi giữa các đối tượng và dữ liệu dạng văn bản hoặc JSON. Reflection giúp động cơ quét các thuộc tính của đối tượng và tự động tạo và đọc dữ liệu từ đối tượng mà không cần phải viết mã cụ thể cho mỗi đối tượng.
 
Tạo Các Hệ Thống ORM (Object-Relational Mapping): Bạn muốn tạo một hệ thống ORM mà không cần phải định nghĩa mỗi lớp ánh xạ cho cơ sở dữ liệu. Reflection cho phép động cơ đọc thông tin về các lớp và thuộc tính, giúp tự động ánh xạ chúng với cơ sở dữ liệu mà không cần phải viết mã đặc biệt cho từng lớp.
Và còn nhiều hơn thế nữa…

Giới thiệu về C# Type Class

Trong .NET, Type là một lớp quan trọng trong System.Reflection namespace. Đối tượng của lớp Type đại diện cho thông tin về kiểu dữ liệu và cung cấp các phương thức để truy cập và thao tác thông tin này trong quá trình chạy ứng dụng. Bạn có thể sử dụng lớp Type để lấy thông tin về các thành phần của một kiểu như phương thức, thuộc tính, trường, sự kiện, và nhiều thông tin khác.
Dưới đây là một ví dụ cụ thể về cách sử dụng Type trong việc đọc thông tin về một kiểu:

 

class Example	
{	
    public int MyProperty { get; set; }	
    public void MyMethod() { }	
}	
	
class Program	
{	
    static void Main()	
    {	
        // Lấy Type của lớp Example	
        Type exampleType = typeof(Example);	
	
        // Hiển thị tên kiểu	
        Console.WriteLine($"Type Name: {exampleType.Name}");	
	
        // Lấy thông tin về thuộc tính	
        Console.WriteLine("Properties in Example class:");	
        foreach (var property in exampleType.GetProperties())	
        {	
            Console.WriteLine(property.Name);	
        }	
	
        // Lấy thông tin về phương thức        Console.WriteLine("Methods in Example class:");	
        foreach (var method in exampleType.GetMethods())	
        {	
            Console.WriteLine(method.Name);	
        }	
    }	
}
Trong ví dụ này:
  • typeof(Example) trả về một đối tượng Type đại diện cho lớp Example.
  • Chúng ta sử dụng đối tượng Type để truy cập thông tin như tên kiểu (Name).
  • Sử dụng các phương thức như GetProperties() và GetMethods() để lấy thông tin về thuộc tính và phương thức của kiểu.
Lớp Type chủ yếu được sử dụng trong ngữ cảnh của Reflection để thăm dò và tương tác với các thành phần của kiểu trong thời gian chạy của ứng dụng.

Giới thiệu về System.Reflection

System.Reflection là một namespace trong .NET và nó chứa các class và interface. Những thành phần này cung cấp cách để có thể quản lý các trường (fields), phương thức (methods), và kiểu dữ liệu cũng như tùy chọn để chúng ta có thể gọi các kiểu đó một cách động. Một số class thường được sử dụng nhất là:

1. Assembly

Lớp này cho phép chúng ta load, khám phá, và tương tác với một assembly một cách động.
Nói một cách rõ ràng hơn thì trong .NET, khái niệm Assembly bao gồm cả tập tin DLL và tập tin thực thi (EXE) cũng như các định dạng khác như Windows Runtime Component (WinMD).
Nói một cách đơn giản, một Assembly là một đơn vị triển khai của ứng dụng .NET, chứa mã nguồn được biên dịch, metadata mô tả về kiểu và tài nguyên (nếu có). Nó cũng có thể chứa thông tin về phiên bản, chữ ký số, v.v.
Cùng khám phá Assembly với ví dụ sau:
class Program	
{	
    static void Main()	
    {	
        // Đường dẫn đến file assembly (cần thay thế bằng đường dẫn thực tế của bạn)	
        string assemblyPath = "C:\Path\To\Your\Assembly.dll";	
	
        try	
        {	
            // Tải assembly từ đường dẫn	
            Assembly loadedAssembly = Assembly.LoadFrom(assemblyPath);	
	
            // Hiển thị tên của assembly	
            Console.WriteLine($"Assembly Name: {loadedAssembly.FullName}");	
	
            // Lấy danh sách các kiểu trong assembly	
            Console.WriteLine("Types in the Assembly:");	
            foreach (Type type in loadedAssembly.GetTypes())	
            {	
                Console.WriteLine(type.FullName);	
            }	
	
            // Lấy thông tin về phiên bản của assembly	
            Version version = loadedAssembly.GetName().Version;	
            Console.WriteLine($"Assembly Version: {version}");	
        }	
        catch (Exception ex)	
        {	
            Console.WriteLine($"Error: {ex.Message}");	
        }	
    }	
}
Trong ví dụ này:
  • Chúng ta sử dụng phương thức Assembly.LoadFrom() để tải một assembly từ đường dẫn cụ thể. Bạn cần thay thế giá trị của assemblyPath bằng đường dẫn thực tế đến file assembly của bạn.
  • Sau đó, chúng ta sử dụng loadedAssembly.FullName để lấy tên đầy đủ của assembly và loadedAssembly.GetTypes() để lấy danh sách các kiểu trong assembly.
  • Cuối cùng, chúng ta sử dụng loadedAssembly.GetName().Version để lấy thông tin về phiên bản của assembly.

2.Module:

Module trong reflection cung cấp thông tin về một phần cụ thể của một Assembly, bao gồm danh sách các kiểu, phương thức, thuộc tính và sự kiện trong Module đó. Nó mở ra cơ hội để kiểm tra và tương tác với các thành phần cụ thể của một Assembly.
Dưới đây là một ví dụ đơn giản về việc sử dụng Module trong reflection:
// Load assembly từ tệp tin DLL	
Assembly assembly = Assembly.LoadFrom("Path\To\Your\Assembly.dll");	
	
// Lấy tất cả các Modules trong Assembly	
Module[] modules = assembly.GetModules();	
	
// Duyệt qua từng Module và hiển thị thông tin	
foreach (Module module in modules)	
{	
    Console.WriteLine($"Module Name: {module.Name}");	
    // Lấy tất cả các kiểu trong Module	
    	
    Type[] types = module.GetTypes();	
    Console.WriteLine("Types in the Module:");	
    foreach (Type type in types)	
    {	
        Console.WriteLine(type.FullName);	
    }	
    Console.WriteLine();	
}
 

3. MethodInfo:

MethodInfo là một lớp trong .NET Framework thuộc namespace System.Reflection, và nó cung cấp thông tin về một phương thức (method) của một kiểu (type) trong một Assembly. MethodInfo cho phép bạn kiểm tra và tương tác với các đặc điểm của một phương thức như tên, kiểu trả về, tham số, và các thuộc tính khác.
Dưới đây là một ví dụ đơn giản về việc sử dụng MethodInfo:
public class Example	
{	
    public void DisplayMessage(string message)	
    {	
        Console.WriteLine($"Message: {message}");	
    }	
}	
	
class Program	
{	
    static void Main()	
    {	
        // Tạo một đối tượng Example	
        Example example = new Example();	
	
        // Lấy kiểu của đối tượng	
        Type type = example.GetType();	
	
        // Lấy MethodInfo cho phương thức DisplayMessage	
        MethodInfo methodInfo = type.GetMethod("DisplayMessage");	
	
        // Kiểm tra xem phương thức có tồn tại hay không	
        if (methodInfo != null)	
        {	
            // Gọi phương thức và truyền tham số	
            object[] parameters = { "Hello, Reflection!" };	
            methodInfo.Invoke(example, parameters);	
        }	
    }	
}
Trong ví dụ này, chúng ta:
  • Tạo một đối tượng Example.
  • Lấy Type của đối tượng Example.
  • Sử dụng GetMethod để lấy MethodInfo cho phương thức “DisplayMessage”.
  • Kiểm tra xem phương thức có tồn tại hay không.
  • Sử dụng Invoke để gọi thực thi phương thức và truyền tham số.
Output của chương trình sẽ là:
Message: Hello, Reflection!

 

4. PropertyInfo:

PropertyInfo là một lớp trong .NET Framework thuộc namespace System.Reflection. Nó cung cấp thông tin về một thuộc tính (property) của một kiểu (type) trong một Assembly. PropertyInfo cho phép bạn kiểm tra và tương tác với các đặc điểm của một thuộc tính như tên, kiểu dữ liệu của thuộc tính, và các thuộc tính khác.
Dưới đây là một ví dụ đơn giản về việc sử dụng PropertyInfo:
public class Example	
{	
    public string MyProperty { get; set; }	
}	
	
class Program	
{	
    static void Main()	
    {	
        // Tạo một đối tượng Example	
        Example example = new Example();	
	
        // Lấy kiểu của đối tượng	
        Type type = example.GetType();	
	
        // Lấy PropertyInfo cho thuộc tính MyProperty	
        PropertyInfo propertyInfo = type.GetProperty("MyProperty");	
	
        // Kiểm tra xem thuộc tính có tồn tại hay không	
        if (propertyInfo != null)	
        {	
            // Gán giá trị mới cho thuộc tính	
            propertyInfo.SetValue(example, "New Value");	
	
            // Đọc giá trị của thuộc tính	
            string value = (string)propertyInfo.GetValue(example);	
	
            Console.WriteLine($"MyProperty value: {value}");	
        }	
    }	
}	

 

5. FieldInfo:

FieldInfo là một lớp trong .NET Framework thuộc namespace System.Reflection. Nó cung cấp thông tin về một trường (field) của một kiểu (type) trong một Assembly. FieldInfo cho phép bạn kiểm tra và tương tác với các đặc điểm của một trường như tên, kiểu dữ liệu của trường, và các thuộc tính khác.
Dưới đây là một ví dụ đơn giản về việc sử dụng FieldInfo:
public class Example	
{	
    public string MyField;	
}	
	
class Program	
{	
    static void Main()	
    {	
        // Tạo một đối tượng Example	
        Example example = new Example();	
	
        // Lấy kiểu của đối tượng	
        Type type = example.GetType();	
	
        // Lấy FieldInfo cho trường MyField	
        FieldInfo fieldInfo = type.GetField("MyField");	
	
        // Kiểm tra xem trường có tồn tại hay không	
        if (fieldInfo != null)	
        {	
            // Gán giá trị mới cho trường	
            fieldInfo.SetValue(example, "New Value");	
	
            // Đọc giá trị của trường	
            string value = (string)fieldInfo.GetValue(example);	
	
            Console.WriteLine($"MyField value: {value}");	
        }	
    }	
}	

 

6. EventInfo:

EventInfo là một lớp trong .NET Framework thuộc namespace System.Reflection. Nó cung cấp thông tin về một sự kiện (event) của một kiểu (type) trong một Assembly. EventInfo cho phép bạn kiểm tra và tương tác với các đặc điểm của một sự kiện như tên, kiểu của delegate được sử dụng để xử lý sự kiện, và các thuộc tính khác.
Dưới đây là một ví dụ đơn giản về việc sử dụng EventInfo:
public class Example	
{	
    public event EventHandler MyEvent;	
	
    public void RaiseEvent()	
    {	
        MyEvent?.Invoke(this, EventArgs.Empty);	
    }	
}	
	
class Program	
{	
    static void Main()	
    {	
        // Tạo một đối tượng Example	
        Example example = new Example();	
	
        // Lấy kiểu của đối tượng	
        Type type = example.GetType();	
	
        // Lấy EventInfo cho sự kiện MyEvent	
        EventInfo eventInfo = type.GetEvent("MyEvent");	
	
        // Kiểm tra xem sự kiện có tồn tại hay không	
        if (eventInfo != null)	
        {	
            // Thêm một phương thức xử lý sự kiện	
            EventHandler handler = (sender, args) =>	
            {	
                Console.WriteLine("Event handled!");	
            };	
            eventInfo.AddEventHandler(example, handler);	
	
            // Kích hoạt sự kiện	
            example.RaiseEvent();	
	
            // Xóa phương thức xử lý sự kiện	
            eventInfo.RemoveEventHandler(example, handler);	
        }	
    }	
}

 

7. ConstructorInfo:

ConstructorInfo là một lớp trong .NET thuộc namespace System.Reflection. Nó cung cấp thông tin về một constructor (phương thức khởi tạo) của một kiểu (type) trong một Assembly. ConstructorInfo cho phép bạn kiểm tra và tương tác với các đặc điểm của một constructor như danh sách tham số, kiểu trả về (nếu có), và các thuộc tính khác.
Dưới đây là một ví dụ đơn giản về việc sử dụng ConstructorInfo:
public class Example	
{	
    public Example(int value)	
    {	
        Console.WriteLine($"Constructor called with value: {value}");	
    }	
}	
	
class Program	
{	
    static void Main()	
    {	
        // Lấy kiểu của đối tượng Example	
        Type type = typeof(Example);	
	
        // Lấy ConstructorInfo cho constructor có tham số int	
        ConstructorInfo constructorInfo = type.GetConstructor(new Type[] { typeof(int) });	
	
        // Kiểm tra xem constructor có tồn tại hay không	
        if (constructorInfo != null)	
        {	
            // Tạo một đối tượng mới từ constructor	
            object[] parameters = { 42 };	
            Example example = (Example)constructorInfo.Invoke(parameters);	
        }	
    }	
}
Trong ví dụ này:
  • Chúng ta lấy kiểu của đối tượng Example bằng cách sử dụng typeof.
  • Sử dụng GetConstructor để lấy ConstructorInfo cho constructor có tham số là int.
  • Kiểm tra xem constructor có tồn tại hay không.
  • Sử dụng Invoke để tạo một đối tượng mới từ constructor và truyền giá trị 42 cho tham số.
Output của chương trình sẽ là:
Constructor called with value: 42

 

8. CustomAttributeData:

Lớp này được sử dụng để lấy thông tin về các custom attributes hoặc kiểm tra các thuộc tính mà không cần phải tạo các instant của chúng.

Thực chiến với C# Reflection

1. Inspecting Class Properties

Bây giờ chúng ta thực chiến với ví dụ đầu tiên của C# reflection:
public class ExampleClass	
{	
    public string Name { get; set; }	
    public int Age { get; set; }	
}	
	
// Get Type of ExampleClass	
Type classType = typeof(ExampleClass);	
	
// Iterate through properties and get names & types	
foreach (PropertyInfo property in classType.GetProperties())	
{	
    Console.WriteLine($"{property.Name} ({property.PropertyType.Name})");	
}

Như ví dụ trên, chúng ta có một hàm đơn giản ExampleClass với hai thuộc tính: Name và Age. Chúng ta lấy Type, sau đó duyệt qua các thuộc tính của nó, hiển thị tên thuộc tính và loại thuộc tính.

2. Invoking Methods Dynamically

Bây giờ, chúng ta cùng khám phá ví dụ mà bạn có thể gọi một method một cách dynamic khi sử dụng reflection:
public class Calculator	
{	
    public int Add(int a, int b)	
    {	
        return a + b;	
    }	
}	
	
// Get Type of Calculator class	
Type calculatorType = typeof(Calculator);	
	
// Create an instance of Calculator	
object calculatorInstance = Activator.CreateInstance(calculatorType);	
	
// Get the Add method	
MethodInfo addMethod = calculatorType.GetMethod("Add");	
	
// Invoke the Add method dynamically	
object result = addMethod.Invoke(calculatorInstance, new object[] { 10, 20 });	
Console.WriteLine($"10 + 20 = {result}"); // 10 + 20 = 30

Trong ví dụ này, chúng ta có một class Calculator với một phương thức Add. Chúng ta tạo một instance của Calculator bằng cách sử dụng Activator.CreateInstance(). Sau đó, chúng ta nhận được phương thức Add bằng cách sử dụng GetMethod(“Add”), và cuối cùng, chúng ta gọi phương thức Add đó một cách linh hoạt, truyền các tham số cần thiết và hiển thị kết quả trên console.

3. Add Property to Dynamic Object

Trong ví dụ này, mình sẽ trình bày cách thêm một property mới vào dynamic object. Chúng ta sẽ tạo một dynamic object bằng cách sử dụng ExpandoObject và thêm các thuộc tính bằng từ khóa dynamic. Để sử dụng reflection , chúng ta lấy ExpandoObject type trước tiên, kiểm tra xem thuộc tính “Age” có tồn tại hay không và thêm nó vào dynamic object nếu nó chưa tồn tại. Cuối cùng, hiển thị các thuộc tính trên console.
using System.Dynamic;	
	
// Create a dynamic object	
dynamic expando = new ExpandoObject();	
	
// Add properties using dynamic keyword	
expando.FirstName = "John";	
expando.LastName = "Doe";	
	
// Use PropertyInfo to add properties by reflection	
Type expandoType = expando.GetType();	
PropertyInfo ageProperty = expandoType.GetProperty("Age");	
if (ageProperty == null)	
{	
    IDictionary<string, object> expandoDict = (IDictionary<string, object>)expando;	
    expandoDict["Age"] = 30;	
}	
	
Console.WriteLine($"{expando.FirstName} {expando.LastName}, {expando.Age} years old");

4. Custom Attributes and Metadata

Khi bạn làm việc với attribute thì khó tránh khỏi việc làm việc với reflection. Attributes cho phép bạn thêm lượng lớn thông tin bổ sung vào các types, members, assemblies mà sau này có thể được truy vấn, xác thực hoặc thao tác thông qua reflection.
Dưới đây là một ví dụ:
// Custom attribute definition	
public class MyCustomAttribute : Attribute	
{	
    public string Description { get; }	
	
    public MyCustomAttribute(string description)	
    {	
        Description = description;	
    }	
}	
	
[MyCustomAttribute("This is a custom attribute applied to a class.")]	
public class ExampleClass	
{	
    // ...	
}	
	
// Get the custom attribute from ExampleClass	
Type exampleType = typeof(ExampleClass);	
object[] attributes = exampleType.GetCustomAttributes(typeof(MyCustomAttribute), false);	
if (attributes.Length > 0)	
{	
    MyCustomAttribute customAttribute = (MyCustomAttribute)attributes[0];	
    Console.WriteLine($"Custom Attribute: {customAttribute.Description}");	
}

Trong ví dụ trên, chúng ta tạo một class MyCustomAttribute kế thừa từ Attribute. Sau đó, apply custom attribute này cho ExampleClass và truy vấn sau đó bằng cách sử dụng GetCustomAttributes(). Sau khi được truy xuất, chuyển thuộc tính thành loại cụ thể và truy cập các thuộc tính của nó.

5. Generate Code

Trong ví dụ sau đây, bạn lại sẽ thấy sự vi diệu của reflection hơn, khi mà reflection đóng vai trò quan trọng trong việc xử lý generate code. Bạn có thể kiểm tra types, objects, or assemblies dựa trên metadata, tạo ra các đoạn code hay class trong runtime.
public class Customer	
{	
    public string FirstName { get; set; }	
    public int BirthYear { get; set; }	
}	
	
public static void GenerateObjectInitializer(Type targetType)	
{	
    StringBuilder builder = new StringBuilder($"new {targetType.Name}n{{n");	
    PropertyInfo[] properties = targetType.GetProperties();	
	
    foreach (PropertyInfo property in properties)	
    {	
        builder.AppendLine($"{property.Name} = default({property.PropertyType.Name}),");	
    }	
	
    builder.AppendLine("};");	
    string generatedCode = builder.ToString();	
    Console.WriteLine(generatedCode);	
}	
	
// Call the GenerateObjectInitializer method	
GenerateObjectInitializer(typeof(Customer));	

 

 Output:
 new Customer
 {
     FirstName = default(String),
     BirthYear = default(Int32),
};

Phương thức GenerateObjectInitializer() lấy một tham số Type và tạo ra đoạn code khởi tạo đối tượng tương ứng cho loại đó. Sau đó, chúng ta gọi phương thức này, truyền typeof(Customer) vào và xem code được tạo được in trên console.

 

6. Dynamic Method Dispatching

Dynamic dispatching có nghĩa là đề cập đến quá trình xác định method nào sẽ thực thi trong thời gian chạy, thay vì tại thời gian biên dịch.
Sử dụng dynamic objects (ExpandoObject, DynamicObject, hoặc dynamic keyword) với reflection sẽ giải quyết được bài toán trên:
using System.Dynamic;	
	
dynamic dynamicInstance = new ExpandoObject();	
	
// Add properties to dynamic instance	
dynamicInstance.FirstName = "John";	
dynamicInstance.LastName = "Doe";	
	
// Get FullName dynamically	
Func<dynamic, string> getFullName = d => $"{d.FirstName} {d.LastName}";	
Console.WriteLine(getFullName(dynamicInstance)); // John Doe

Lời kết

Các góc cạnh khi làm việc với reflection rất nhiều, ở trong bài viết này đã đủ bao quát lên các định nghĩa và các ví dụ để áp dụng trong dự án của bạn. Song, sẽ có phần 2 cho đề tài này về những khái niệm và ví dụ nâng cao hơn.
Mong bài viết hữu ích với bạn.
Hieu Ho.

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *