Kỹ thuật Reflection và Dependency Injection trong C#

1. Cơ chế Reflection (Phản chiếu)

1.1. Tổng quan về Reflection

Reflection là khả năng cho phép chương trình xem xét và thao tác trên siêu dữ liệu (metadata) của chính nó trong thời gian chạy (runtime). Thông qua cơ chế này, mã nguồn có thể truy cập vào thông tin về các assembly, module, kiểu dữ liệu và các thành viên bên trong chúng mà không cần biết trước cụ thể tại thời điểm biên dịch.

Với Reflection, lập trình viên có thể khởi tạo đối tượng động, liên kết kiểu dữ liệu với đối tượng tồn tại sẵn, hoặc trích xuất thông tin kiểu từ đối tượng đó để gọi phương thức và truy cập trường dữ liệu.

1.1.1. Ưu và nhược điểm

Lợi ích:

  • Tăng cường tính linh hoạt và khả năng mở rộng của ứng dụng.
  • Giảm sự phụ thuộc chặt chẽ (coupling) giữa các thành phần.
  • Cho phép tạo và điều khiển đối tượng mà không cần hard-code lớp cụ thể.

Hạn chế:

  • Hiệu suất: Reflection hoạt động dựa trên cơ chế giải thích (interpretation), do đó tốc độ truy cập trường và phương thức chậm hơn so gọi trực tiếp. Chỉ nên sử dụng trong các framework yêu cầu tính mở rộng cao.
  • Bảo trì: Logic chương trình trở nên khó theo dõi hơn vì Reflection bypass qua việc kiểm tra kiểu tĩnh của trình biên dịch, dễ gây lỗi runtime nếu không cẩn thận.

1.1.2. Trường hợp áp dụng

  • Serialization/Deserialization: Đọc ghi thuộc tính đối tượng ra định dạng JSON/XML.
  • Hệ thống Plugin: Tải động các assembly bên thứ ba và kích hoạt chức năng.
  • Testing Frameworks: Các công cụ như xUnit sử dụng Reflection để tìm và chạy các method đánh dấu là test.
  • Code Generation: Kết hợp với System.Reflection.Emit để sinh mã động.

Lưu ý: Cần cân nhắc vấn đề hiệu năng khi gọi lặp lại nhiều lần. Nên cache kết quả reflection. Ngoài ra, việc truy cập thành viên private có thể gây rủi ro bảo mật nếu không kiểm soát chặt chẽ.

1.2. Các thành phần cốt lõi

Để làm việc với Reflection, cần nắm vững các lớp chính trong namespace System.Reflection:

  • Type: Đại diện cho thông tin kiểu dữ liệu (class, interface, array...).
  • Assembly: Đại diện cho một đơn vị triển khai chứa nhiều kiểu dữ liệu.
  • MethodInfo, PropertyInfo, FieldInfo: Cung cấp thông tin chi tiết về phương thức, thuộc tính và trường dữ liệu.
using System;
using System.Reflection;

1.3. Thực hành với Reflection

1.3.1. Truy xuất thông tin kiểu

Có nhiều cách để lấy đối tượng Type:

// Cách 1: Sử dụng toán tử typeof
var dataType = typeof(Guid);

// Cách 2: Lấy từ instance đối tượng
var id = Guid.NewGuid();
var instanceType = id.GetType();

// Cách 3: Load từ Assembly
var assembly = Assembly.Load("System.Runtime");
var typeFromAssembly = assembly.GetType("System.Guid");

1.3.2. Đọc siêu dữ liệu (Metadata)

Sử dụng lớp Type để duyệt qua các thành phần của lớp:

var target = typeof(DateTime);

// Lấy tên lớp và namespace
Console.WriteLine($"Ten lop: {target.Name}");
Console.WriteLine($"Namespace: {target.Namespace}");

// Duyệt danh sách phương thức
var methods = target.GetMethods();
foreach (var m in methods)
{
    Console.WriteLine($"Phuong thuc: {m.Name}");
}

// Duyệt danh sách thuộc tính
var props = target.GetProperties();
foreach (var p in props)
{
    Console.WriteLine($"Thuoc tinh: {p.Name}");
}

1.3.3. Gọi phương thức động

Reflection cho phép gọi method mà không cần biết tên cụ thể lúc viết code:

var mathType = typeof(Math);
// Lấy thông tin phương thức Abs
var methodInfo = mathType.GetMethod("Abs", new[] { typeof(int) });

// Gọi phương thức static (tham số đầu là null)
var result = methodInfo.Invoke(null, new object[] { -10 });
Console.WriteLine($"Ket qua: {result}"); // Output: 10

Lưu ý: Tham số đầu tiên của Invoke là instance đối tượng nếu là method instance, hoặc null nếu là static method.

1.3.4. Khởi tạo đối tượng động

Sử dụng Activator để tạo instance từ kiểu dữ liệu:

var stringType = typeof(string);
// Tạo instance rỗng
var obj = Activator.CreateInstance(stringType); 

// Hoặc tạo với tham số constructor
var builderType = typeof(System.Text.StringBuilder);
var builder = Activator.CreateInstance(builderType, new object[] { "Start" });

1.3.5. Truy cập thành viên Private

Để đọc ghi trường private, cần chỉ định BindingFlags phù hợp:

public class Wallet
{
    private decimal balance = 1000;
}

var wallet = new Wallet();
var type = wallet.GetType();

// Lấy trường private
var field = type.GetField("balance", BindingFlags.NonPublic | BindingFlags.Instance);

// Đọc giá trị
var currentBalance = (decimal)field.GetValue(wallet);
Console.WriteLine($"So du: {currentBalance}");

1.4. Ví dụ tổng hợp

1.4.1. Thao tác với thuộc tính

public class Employee
{
    public string FullName { get; set; }
    public int Level { get; set; }
}

var staff = new Employee();
var info = staff.GetType();

// Set gia tri thuoc tinh
var nameProp = info.GetProperty("FullName");
nameProp.SetValue(staff, "Nguyen Van A");

var levelProp = info.GetProperty("Level");
levelProp.SetValue(staff, 5);

Console.WriteLine($"Nhan vien: {staff.FullName}, Cap bac: {staff.Level}");

1.4.2. Gọi method động

public class MathUtils
{
    public int Multiply(int a, int b) => a * b;
}

var utils = new MathUtils();
var typeInfo = utils.GetType();
var method = typeInfo.GetMethod("Multiply");

var res = method.Invoke(utils, new object[] { 4, 5 });
Console.WriteLine($"Tich: {res}"); // Output: 20

2. Dependency Injection (DI)

2.1. Khái niệm cơ bản

Dependency Injection (DI) là một mẫu thiết kế giúp thực hiện nguyên lý Đảo ngược điều khiển (IoC). Thay vì các đối tượng tự tạo ra phụ thuộc của mình, chúng sẽ nhận các phụ thuộc đó từ bên ngoài (thường là một container). Điều này giúp code dễ kiểm thử và bảo trì hơn.

Các thành phần chính:

  • Dependency: Đối tượng mà một class cần để hoạt động.
  • Injection: Quá trình truyền đối tượng dependency vào class.
  • IoC Container: Nơi quản lý vòng đời và việc khởi tạo các đối tượng.

2.2. Lợi ích của DI

Xét ví dụ không dùng DI:

public class ReportService
{
    private DatabaseProvider _db;
    public ReportService()
    {
        _db = new DatabaseProvider(); // Coupling chat
    }
}

Vấn đề: Class ReportService bị phụ thuộc cứng vào DatabaseProvider, khó thay thế khi test.

Áp dụng DI:

public class ReportService
{
    private readonly IDataProvider _data;
    public ReportService(IDataProvider data)
    {
        _data = data; // Inject qua constructor
    }
}

Ưu điểm: Dễ dàng swap implementation, hỗ trợ unit test với mock object.

2.3. Microsoft.Extensions.DependencyInjection

Đây là container DI mặc định trong .NET Core và .NET 5+. Nó nhẹ và đủ dùng cho hầu hết các scenario cơ bản.

2.3.1. Cài đặt

dotnet add package Microsoft.Extensions.DependencyInjection

2.3.2. Cấu hình cơ bản

using Microsoft.Extensions.DependencyInjection;

public interface IDataProvider
{
    void Connect();
}

public class SqlProvider : IDataProvider
{
    public void Connect() => Console.WriteLine("Connected to SQL");
}

public class ReportService
{
    private readonly IDataProvider _provider;
    public ReportService(IDataProvider provider) => _provider = provider;
    public void Generate() => _provider.Connect();
}

class Program
{
    static void Main()
    {
        var services = new ServiceCollection();
        services.AddTransient();
        services.AddTransient<ReportService>();

        var provider = services.BuildServiceProvider();
        var report = provider.GetService<ReportService>();
        report.Generate();
    }
}

2.3.3. Vòng đời dịch vụ (Lifetime)

  • AddTransient: Tạo instance mới mỗi lần yêu cầu.
  • AddScoped: Tạo instance mới mỗi scope (ví dụ: mỗi request HTTP).
  • AddSingleton: Tạo một instance duy nhất cho toàn bộ ứng dụng.
services.AddTransient<IService, Service>();
services.AddScoped<IService, Service>();
services.AddSingleton<IService, Service>();

2.4. Sử dụng Autofac

2.4.1. Giới thiệu

Autofac là một container DI mạnh mẽ hơn so với mặc định của Microsoft, hỗ trợ nhiều tính năng nâng cao như modularization, property injection, và quản lý lifecycle phức tạp. Phù hợp cho các enterprise application.

2.4.2. Cài đặt

dotnet add package Autofac
dotnet add package Autofac.Extensions.DependencyInjection

2.4.3. Đăng ký và Resolve

using Autofac;

public interface INotifier
{
    void Send(string msg);
}

public class SmsNotifier : INotifier
{
    public void Send(string msg) => Console.WriteLine($"SMS: {msg}");
}

class Program
{
    static void Main()
    {
        var builder = new ContainerBuilder();
        builder.RegisterType<SmsNotifier>().As<INotifier>();
        
        var container = builder.Build();
        var notifier = container.Resolve<INotifier>();
        notifier.Send("Hello");
    }
}

2.4.4. Quản lý Lifecycle

// Instance per dependency (tương đương Transient)
builder.RegisterType<SmsNotifier>().As<INotifier>().InstancePerDependency();

// Single instance (tương đương Singleton)
builder.RegisterType<SmsNotifier>().As<INotifier>().SingleInstance();

2.4.5. Các tính năng nâng cao

Đăng ký qua Lambda:

builder.Register(c => new SmsNotifier()).As<INotifier>();

Đăng ký instance có sẵn:

var config = new { Conn = "SQL" };
builder.RegisterInstance(config).AsSelf();

Điều kiện đăng ký:

builder.RegisterType<EmailNotifier>()
       .As<INotifier>()
       .IfNotRegistered(typeof(INotifier));

Module hóa:

public class AppModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<SmsNotifier>().As<INotifier>();
    }
}
// Su dung
builder.RegisterModule<AppModule>();

Property Injection:

builder.RegisterType<Consumer>().PropertiesAutowired();

Truyền tham số constructor:

builder.RegisterType<SmsNotifier>()
       .As<INotifier>()
       .WithParameter("gatewayUrl", "https://api.sms.com");

Thẻ: c-sharp Reflection dependency-injection Autofac dotnet-core

Đăng vào ngày 30 tháng 9 lúc 19:00