Hướng dẫn Tích hợp AOP với Unity Container trong .NET

Tại sao cần sử dụng AOP

Nhu cầu phát triển phần mềm thường xuyên biến đổi, đòi hỏi việc bổ sung các chức năng như ghi log, xử lý ngoại lệ, kiểm soát quyền hạn, cơ chế lưu đệm hoặc quản lý giao dịch vào nhiều phương thức khác nhau. Trong mô hình lập trình hướng đối tượng (OOP) thuần túy, điều này thường buộc chúng ta phải sửa đổi trực tiếp mã nguồn của các lớp nghiệp vụ.

Mặc dù các mẫu thiết kế (Design Patterns) có thể giúp thay thế toàn bộ đối tượng, nhưng chúng lại thiếu khả năng động để biến đổi một lớp đang chạy mà không xâm phạm vào cốt lõi của nó. Chính vì vậy, tư duy lập trình Hướng cắt ngang (AOP) ra đời như một sự bổ trợ cần thiết cho OOP, cho phép xây dựng hệ thống linh hoạt đáp ứng các yêu cầu ngày càng phức tạp.

APO giúp tách biệt các logic ngang (cross-cutting concerns) ra khỏi quy trình nghiệp vụ chính. Các tính năng phổ thông như nhật ký, bảo mật hay hiệu suất được đóng gói riêng biệt và áp dụng vào điểm thực thi mong muốn thông qua cơ chế chặn (interception), giúp mã nguồn gọn gàng hơn.

Cấu hình và Triển khai AOP

Bước 1: Thêm thư viện tham chiếu

Bạn cần đảm bảo dự án đã tham chiếu các assembly cần thiết của Unity, bao gồm cả phần mở rộng tương tác (Interception).

Bước 2: Thiết lập cấu hình (App.config / Web.config)

Khai báo section và định nghĩa các container cùng hành vi tương tác bên trong node unity. Thứ tự đăng ký các behavior rất quan trọng, đặc biệt là xử lý lỗi nên được đặt đầu tiên để bắt được mọi ngoại lệ.

<configSections>
    <!-- Khai báo section cho Unity -->
    <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Unity.Configuration" />
</configSections>

<unity>
    <sectionExtension type="Microsoft.Practices.Unity.InterceptionExtension.Configuration.InterceptionConfigurationExtension, Unity.Interception.Configuration" />
    <containers>
        <container name="coreAppInterception">
            <extension type="Interception" />
            <register type="Services.IRepository, Services" mapTo="Infrastructure.RepositoryImpl, Infrastructure">
                <!-- Sử dụng InterfaceInterceptor để chặn các phương thức của interface -->
                <interceptor type="InterfaceInterceptor"/>
                
                <!-- Xử lý lỗi cần đứng trước để bao trùm các hành vi khác -->
                <interceptionBehavior type="MyApp.CrossCutting.ErrorHandler, MyApp.CrossCutting"/>
                <!-- Theo dõi hiệu suất -->
                <interceptionBehavior type="MyApp.CrossCutting.PerformanceLogger, MyApp.CrossCutting"/>
                <!-- Xác thực đầu vào -->
                <interceptionBehavior type="MyApp.CrossCutting.InputValidator, MyApp.CrossCutting"/>
                <!-- Quản lý dữ liệu đệm -->
                <interceptionBehavior type="MyApp.CrossCutting.DataCache, MyApp.CrossCutting"/>
            </register>
        </container>
    </containers>
</unity>

Bước 3: Khởi tạo và Gọi chương trình

3.1 Khởi tạo container

// Tạo phiên bản giải quyết độc lập
var resolver = new UnityContainer();

// Đọc phần cấu hình từ file
UnityConfigurationSection section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");

// Áp dụng cấu hình vào container với tên miền đã định nghĩa
section.Configure(resolver, "coreAppInterception");

// Lấy instance từ container, lúc này các phương thức đã được bọc bởi interceptor
var accountService = resolver.Resolve<IRepository>();

// Gọi phương thức nghiệp vụ (logic chặn sẽ tự động chạy)
var profile = accountService.RetrieveProfile(50);

if (profile != null)
{
    Console.WriteLine($"Tên khách hàng: {profile.FullName}");
}

3.2 Các lớp Hành vi Tương tác (Interceptor Behaviors)

Mỗi behavior cần triển khai interface IInterceptionBehavior. Dưới đây là cách viết lại các logic trên với tên lớp và biến khác.

/// 
/// Bộ lọc tìm kiếm trong Cache
/// 
public class DataCache : IInterceptionBehavior
{
    public bool WillExecute => true;

    public IEnumerable<Type> GetRequiredInterfaces()
    {
        return Type.EmptyTypes;
    }

    public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
    {
        // Giả lập trả về dữ liệu đệm nếu có (ở đây demo luôn giá trị cứng)
        var cachedResult = new AccountModel() 
        { 
            Id = 999, 
            FullName = "Khách hàng từ Cache" 
        };
        
        return input.CreateMethodReturn(cachedResult);
    }
}
/// 
/// Middleware xử lý lỗi an toàn
/// 
public class ErrorHandler : IInterceptionBehavior
{
    public bool WillExecute => true;

    public IEnumerable<Type> GetRequiredInterfaces()
    {
        return Type.EmptyTypes;
    }

    public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
    {
        IMethodReturn result = getNext()(input, getNext);
        
        if (result.Exception != null)
        {
            // Ghi log chi tiết lỗi
            Trace.WriteLine($"Lỗi xảy ra: {result.Exception.Message}");
            
            // Ngăn exception rò rỉ ra ngoài caller nếu cần
            result.Exception = null; 
        }
        else
        {
            Trace.WriteLine("Hành động hoàn tất không lỗi.");
        }
        return result;
    }
}
/// 
/// Theo dõi thời gian thực thi
/// 
public class PerformanceLogger : IInterceptionBehavior
{
    public bool WillExecute => true;

    public IEnumerable<Type> GetRequiredInterfaces()
    {
        return Type.EmptyTypes;
    }

    public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
    {
        var timer = System.Diagnostics.Stopwatch.StartNew();
        IMethodReturn output = getNext()(input, getNext);
        timer.Stop();

        Console.WriteLine($"Phương thức {input.MethodBase.Name} tiêu tốn: {timer.ElapsedMilliseconds}ms");
        return output;
    }
}
/// 
/// Kiểm tra tính hợp lệ của tham số đầu vào
/// 
public class InputValidator : IInterceptionBehavior
{
    public bool WillExecute => true;

    public IEnumerable<Type> GetRequiredInterfaces()
    {
        return Type.EmptyTypes;
    }

    public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
    {
        Console.WriteLine("Đang thẩm định các tham số truyền vào...");
        
        foreach (var arg in input.Inputs)
        {
            Console.WriteLine(arg.ToString());
        }

        // Giả lập logic validate ID lớn hơn giới hạn
        int targetId = Convert.ToInt32(input.Inputs[0]);
        if (targetId > 100)
        {
            // Tạo kết quả trả về dạng lỗi ngay tại đây
            return input.CreateExceptionMethodReturn(new ArgumentException("ID vượt quá ngưỡng cho phép"));
        }

        return getNext()(input, getNext);
    }
}

3.3 Phương thức Nghiệp vụ

Interface và Class chỉ chứa logic thuần túy, không biết gì về các logic phụ trợ.

public interface IRepository
{
    AccountModel RetrieveProfile(int recordId);
}

public class RepositoryImpl : IRepository
{
    public AccountModel RetrieveProfile(int recordId)
    {
        Console.WriteLine("Truy vấn dữ liệu từ CSDL...");
        // Giả lập độ trễ truy xuất
        Thread.Sleep(2000); 
        
        return new AccountModel()
        {
            Id = recordId,
            FullName = "Nguyễn Văn A"
        };
    }
}

Hạn chế với mức độ chi tiết

Hiện tại, cơ cấu cấu hình này chỉ cho phép chặn toàn bộ các phương thức thuộc một interface. Nếu muốn áp dụng interceptor chỉ cho một vài phương thức cụ thể trong cùng interface, bạn sẽ cần kết hợp thêm Custom Attribute để đánh dấu phương thức đó, sau đó viết logic tùy chọn trong Behavior để kiểm tra xem phương thức hiện hành có chứa attribute đó hay không.

Thẻ: unity-container AOP dotnet-framework dependency-injection interception

Đăng vào ngày 22 tháng 5 lúc 17:43