Lập trình hướng khía cạnh với Metalama trong .NET

Hầu hết ứng dụng .NET đều có những đoạn mã lặp lại vì các lý do không liên quan trực tiếp đến nghiệp vụ.

Một phương thức tính tổng đơn hàng nhưng đồng thời ghi log. Một phương thức khác lấy thông tin khách hàng nhưng cũng đo thời gian thực thi. Nhiều lớp cùng kiểm tra tham số, triển khai thông báo thay đổi thuộc tính hoặc áp dụng một bộ quy tắc giống nhau.

Từng phần mã đều hữu ích. Vấn đề xuất hiện khi cùng một cách triển khai bị sao chép trên hàng chục phương thức.

Khi quy ước ghi log thay đổi, bạn phải tìm kiếm trên toàn bộ solution. Một bước kiểm tra bị bỏ sót có thể tạo ra hành vi không nhất quán. Các phương thức nghiệp vụ cũng trở nên dài hơn, khiến mục đích thực sự của chúng khó nhận biết.

Trong một dự án trước đây, tôi cần đo thời gian thực thi của rất nhiều hàm có gọi API bên ngoài và cơ sở dữ liệu, trong khi các hàm này nằm trong một luồng nghiệp vụ phức tạp.

Tôi phải lặp lại thao tác khởi động và khởi động lại Stopwatch, sau đó ghi log sau mỗi thành phần. Đây là công việc tốn thời gian và khá nhàm chán.

Lập trình hướng khía cạnh, hay Aspect-Oriented Programming, giúp tổ chức những mối quan tâm lặp lại và áp dụng chúng một cách nhất quán. Metalama đưa cách tiếp cận này vào C# thông qua việc sinh mã tại thời điểm biên dịch, đồng thời cung cấp công cụ giúp chúng ta hiểu mã sau biến đổi.

Tại sao nên dùng lập trình hướng khía cạnh?

Ghi log, tracing, kiểm tra hợp lệ và những hành vi tương tự thường được gọi là mối quan tâm xuyên suốt, vì chúng ảnh hưởng đến nhiều phần của ứng dụng.

Hãy xem một service đơn giản:

public decimal CalculateTotal(decimal price, int quantity)<br>{<br>    Console.WriteLine("Starting calculation");<br><br>    try<br>    {<br>        return price * quantity;<br>    }<br>    finally<br>    {<br>        Console.WriteLine("Calculation completed");<br>    }<br>}

Thao tác nghiệp vụ chỉ là một biểu thức. Phần còn lại dùng để triển khai quy ước ghi log.

Nếu có hai mươi phương thức cần cùng hành vi, việc sao chép cấu trúc này sẽ tạo ra hai mươi vị trí cần bảo trì.

Với lập trình hướng khía cạnh, chúng ta định nghĩa hành vi trong một aspect, sau đó áp dụng aspect đó cho những khai báo được chọn:

[Log]<br>public decimal CalculateTotal(decimal price, int quantity)<br>    => price * quantity;

Aspect cung cấp hành vi bao quanh, còn phương thức vẫn tập trung vào trách nhiệm của chính nó.

Cách làm này bổ trợ cho những kỹ thuật quen thuộc như middleware và decorator. Middleware vẫn phù hợp ở ranh giới request, còn decorator hữu ích tại ranh giới service. Aspect cung cấp thêm một cách áp dụng hành vi lặp lại trực tiếp lên phương thức, thuộc tính, tham số và kiểu.

Một số trường hợp sử dụng khác gồm bảo mật, khả năng quan sát, theo dõi thay đổi, bổ sung dependency, xử lý ngoại lệ, cũng như các mẫu thiết kế như builder, memento và factory.

Giới thiệu Metalama

Metalama là bộ công cụ mã nguồn mở dành cho pattern và kiến trúc trong C#.

Metalama giúp lập trình viên định nghĩa các pattern có thể tái sử dụng, sinh mã triển khai lặp lại và kiểm tra các khai báo. Khả năng của nó không chỉ dừng ở việc bao quanh lời gọi phương thức. Bạn có thể xem tài liệu chính thức về Metalama để biết thêm chi tiết.

PostSharp Technologies là công ty đứng sau cả PostSharp và Metalama. Công ty đã mang kinh nghiệm về metaprogramming trong .NET vào các công cụ này và là một trong những đơn vị tiên phong đưa lập trình hướng khía cạnh đến thế giới .NET từ năm 2004 thông qua PostSharp.

PostSharp là một chủ đề riêng và sẽ được đề cập trong một bài viết khác.

Metalama cung cấp những gì?

  • Ít lặp lại hơn: duy trì một triển khai lặp lại trong một aspect duy nhất.
  • Hành vi nhất quán: áp dụng cùng một pattern cho các khai báo được chọn.
  • Mã nghiệp vụ tập trung: giữ các chi tiết hạ tầng bên ngoài thân phương thức.
  • Sinh mã: tạo thêm thành viên thay vì tự duy trì mã khuôn mẫu.
  • Khả năng quan sát: kiểm tra biến đổi và gỡ lỗi phần triển khai được sinh ra.

Thông thường, Metalama biến đổi mã trong quá trình biên dịch mà không viết lại các tệp mã nguồn bạn đang duy trì. Live template là một tính năng riêng, chủ động thay đổi mã nguồn. Hãy xem hướng dẫn bắt đầu để biết thêm.

Metaprogramming nằm ở đâu?

Metaprogramming là việc viết mã có thể làm việc với mã khác: kiểm tra, sinh hoặc biến đổi mã đó.

Metalama sử dụng kỹ thuật này để triển khai aspect. Template của Metalama kết hợp các quyết định tại thời điểm biên dịch với chỉ dẫn sinh mã C# chạy ở thời điểm thực thi.

Ngôn ngữ template được gọi là T#. Nó dùng cú pháp tương thích với C#, nhưng template được thực thi trong lúc biên dịch để tạo ra mã ứng dụng chạy về sau. Bạn có thể đọc thêm về T# template trong tài liệu chính thức.

Aspect, reflection và hiệu năng thời gian chạy

Lập trình hướng khía cạnh và reflection mô tả hai khái niệm khác nhau.

Lập trình hướng khía cạnh tổ chức các hành vi xuyên suốt. Reflection cho phép một ứng dụng đang chạy kiểm tra metadata và khi cần thì truy cập thành viên một cách động. Một triển khai AOP có thể sử dụng reflection, nhưng reflection không bắt buộc.

Metalama kiểm tra code model trong thời gian biên dịch và sinh ra phần triển khai trước khi chương trình chạy. Một logging aspect tạo ra các câu lệnh ghi log thông thường bao quanh thân phương thức gốc. Lời gọi không cần đi qua một bộ điều phối aspect dựa trên reflection.

Thiết kế này giải thích hiệu năng thời gian chạy của Metalama: ứng dụng thực thi mã đã sinh thay vì diễn giải template aspect ở mỗi lần gọi.

Tuy nhiên, điều đó không có nghĩa hành vi bổ sung là miễn phí. Ghi log vẫn thực hiện I/O, các contract vẫn chạy bước kiểm tra, còn phần triển khai được sinh quá lớn có thể làm tăng kích thước ứng dụng và công việc của JIT.

Tài liệu Metalama khuyến nghị giữ mã được sinh ở mức ngắn gọn và chuyển phần logic dùng chung đáng kể vào các helper chạy ở thời gian thực thi.

Tại sao dùng Metalama khi AI đã có thể sinh mã khuôn mẫu?

Mọi mã nguồn, kể cả mã do AI sinh ra, đều cần được xem xét, tìm lỗi, hiểu và bảo trì. Metalama không sinh thêm mã nguồn cần duy trì theo cách thông thường. Vì vậy, đây là một hướng tiếp cận tốt để giảm lỗi, giảm độ phức tạp nhận thức của mã nguồn và thậm chí giảm số lượng token cần sử dụng.

Viết aspect tùy chỉnh đầu tiên

Với một ứng dụng console, hãy thêm package lõi:

dotnet add package Metalama.Framework

Sau đó tạo aspect ghi log:

using System;<br>using Metalama.Framework.Aspects;<br><br>public sealed class LogAttribute : OverrideMethodAspect<br>{<br>    public override dynamic? OverrideMethod()<br>    {<br>        Console.WriteLine($"[log] Enter {meta.Target.Method.Name}");<br><br>        try<br>        {<br>            return meta.Proceed();<br>        }<br>        catch (Exception exception)<br>        {<br>            Console.WriteLine($"[log] Failure: {exception.Message}");<br>            throw;<br>        }<br>        finally<br>        {<br>            Console.WriteLine($"[log] Leave {meta.Target.Method.Name}");<br>        }<br>    }<br>}

Áp dụng aspect cho một phương thức:

public sealed class OrderService<br>{<br>    [Log]<br>    public decimal CalculateTotal(decimal price, int quantity)<br>        => price * quantity;<br>}

Hai biểu thức sau giải thích cơ chế hoạt động:

  • meta.Target.Method.Name đọc metadata của khai báo trong thời gian biên dịch.
  • meta.Proceed() đại diện cho việc thực thi phần triển khai gốc.

Phương thức sau biến đổi vẫn giữ phép tính, bao quanh nó bằng thao tác ghi log và duy trì việc truyền ngoại lệ.

Ghi log ra console giúp ví dụ dễ đọc. Trong ứng dụng thực tế, hãy điều chỉnh pattern cho phù hợp với hạ tầng ghi log của bạn và tránh đưa các giá trị tham số nhạy cảm vào log.

Contract tùy chỉnh

Contract aspect có thể kiểm tra giá trị đầu vào trước khi ứng dụng sử dụng chúng.

Ví dụ sau tạo một contract dành cho các khai báo kiểu chuỗi:

using System;<br>using Metalama.Framework.Aspects;<br><br>public sealed class RequiredTextAttribute : ContractAspect<br>{<br>    public override void Validate(dynamic? value)<br>    {<br>        if (string.IsNullOrWhiteSpace(value))<br>        {<br>            throw new ArgumentException(<br>                "Text must not be empty or whitespace.",<br>                nameof(value));<br>        }<br>    }<br>}

Sử dụng contract trên tham số hoặc thuộc tính:

public sealed class Customer<br>{<br>    [RequiredText]<br>    public string Name { get; set; } = "Unknown";<br><br>    public string Welcome([RequiredText] string name)<br>        => $"Welcome, {name}.";<br>}

Tham số chỉ chứa khoảng trắng sẽ bị từ chối trước khi Welcome được thực thi. Việc gán một giá trị không hợp lệ cho thuộc tính cũng bị từ chối trước khi giá trị mới thay thế giá trị hiện tại.

Khi sinh mã kiểm tra tham số, Metalama thay nameof(value) bằng tên của tham số đích. Contract cũng có thể chuẩn hóa giá trị và kiểm tra giá trị trả về.

Giới thiệu thành viên mới

Aspect có thể sinh thành viên mới cũng như thay đổi hành vi hiện có:

using System;<br>using Metalama.Framework.Aspects;<br><br>public sealed class StampAttribute : TypeAspect<br>{<br>    [Introduce]<br>    public Guid TrackingId { get; } = Guid.NewGuid();<br><br>    [Introduce]<br>    public string DescribeStamp()<br>        => $"{meta.Target.Type.Name} / {TrackingId}";<br>}

Áp dụng aspect:

[Stamp]<br>public sealed partial class Shipment<br>{<br>    public string Destination { get; set; } = "London";<br>}

Các thành viên được sinh có thể được truy cập bằng C# thông thường:

var shipment = new Shipment();<br><br>Console.WriteLine(shipment.TrackingId);<br>Console.WriteLine(shipment.DescribeStamp());

Từ khóa partial rất quan trọng. Khi mã nguồn tham chiếu đến thành viên được giới thiệu, Metalama yêu cầu kiểu đích phải là partial để các thành viên đó hiển thị trong quá trình phân tích lúc thiết kế. Nếu thiếu từ khóa này, biên dịch có thể vẫn thành công nhưng IntelliSense lại báo lỗi thiếu thành viên, chẳng hạn CS1061.

Khai thác Visual Studio cho Metalama

Tiện ích mở rộng Visual Studio của Metalama giúp việc kiểm tra và tìm hiểu aspect dễ dàng hơn.

Để cài đặt:

  1. Mở Extensions → Manage Extensions.
  2. Tìm Metalama + PostSharp.
  3. Chọn tiện ích và nhấn Download.
  4. Hoàn tất quá trình cài đặt, sau đó mở lại Visual Studio và xác nhận tiện ích đã được cài.

Xem aspect nào đang tác động lên mã

CodeLens hiển thị các aspect đã áp dụng và nguồn gốc của chúng. Tính năng này đặc biệt hữu ích khi một fabric áp dụng aspect nhưng trên phương thức không xuất hiện attribute.

Metalama Diff so sánh mã gốc với mã sau biến đổi. Bạn có thể mở tính năng này thông qua Preview Transformed Code trong CodeLens hoặc lệnh Metalama Diff trên menu ngữ cảnh của tài liệu.

Aspect Explorer hoạt động theo hướng ngược lại: chọn một aspect rồi kiểm tra các khai báo chịu ảnh hưởng. Mở công cụ này qua Extensions → Metalama + PostSharp → Aspect Explorer.

Tính năng tô màu T# cũng giúp phân biệt biểu thức chạy ở thời điểm biên dịch với hành vi thời gian chạy được sinh ra. Kết hợp với nhau, các công cụ này giúp bạn kiểm tra quá trình biến đổi trong lúc phát triển.

Gỡ lỗi mã sau biến đổi

Gỡ lỗi hành vi được sinh là một quy trình thực tế trong Visual Studio. Với Metalama, bạn chỉ cần cấu hình IDE ở mức tối thiểu.

Để xem phần triển khai sau biến đổi:

  1. Mở Build → Configuration Manager.
  2. Tạo một cấu hình có tên LamaDebug.
  3. Sao chép thiết lập từ cấu hình Debug.
  4. Chọn cấu hình mới và build.
  5. Bắt đầu gỡ lỗi rồi dùng F11 để bước vào phần thực thi.

Trong cấu hình này, trình gỡ lỗi hiển thị mã sau biến đổi.

Trong Solution Explorer, bật Show All Files và tìm các tệp được sinh bên dưới thư mục kết quả trung gian, thường là:

obj/LamaDebug/net10.0/metalama

Bạn có thể đặt breakpoint trong các tệp sau biến đổi để kiểm tra thao tác ghi log hoặc validation được sinh.

Có một chi tiết quan trọng: trong cấu hình LamaDebug, breakpoint đặt trong tệp mã nguồn gốc sẽ không được kích hoạt vì quá trình gỡ lỗi ánh xạ đến các tệp sau biến đổi.

Bạn cũng có thể tạm thời dùng Debugger.Break() để vào trình gỡ lỗi. Cảnh báo định dạng LAMA0066 là điều bình thường trong cấu hình này.

Cách làm này cho phép bạn điều tra chính xác mã mà ứng dụng thực thi, bao gồm cả hành vi được aspect bổ sung.

Khám phá aspect có sẵn trên Marketplace

Bạn không cần tự triển khai mọi pattern.

Tài liệu Metalama hướng dẫn lập trình viên tìm các aspect và extension mã nguồn mở trên Metalama Marketplace.

  1. Mở Metalama Marketplace.
  2. Tìm pattern cần thiết, chẳng hạn contract, observability hoặc caching.
  3. Kiểm tra danh mục, tác giả, chất lượng và thông tin giấy phép.
  4. Mở tài liệu hoặc kho mã của mục đó.
  5. Làm theo hướng dẫn cài đặt và ví dụ sử dụng.

Ví dụ, hãy cài thư viện contract:

dotnet add package Metalama.Patterns.Contracts

Sau đó sử dụng các attribute có sẵn:

using Metalama.Patterns.Contracts;<br><br>public sealed class DeliveryService<br>{<br>    public string CreateLabel([NotNull] string recipient)<br>        => $"Deliver to: {recipient}";<br>}

Thư viện cung cấp bước kiểm tra null. Các attribute contract khác hỗ trợ chuỗi, khoảng số, collection và nhiều trường hợp khác.

Contract cũng hỗ trợ áp dụng quy tắc kế thừa và các quy ước sinh mã có thể cấu hình, vì vậy chúng hữu ích hơn những câu lệnh kiểm tra riêng lẻ.

Một số aspect đáng chú ý trên Marketplace:

  • Metalama.Extensions.DependencyInjection
  • Metalama.Patterns.Observability
  • Metalama.Patterns.Memoization
  • Metalama.Patterns.Caching

Bạn có thể tìm thêm aspect trên Metalama Marketplace.

Áp dụng aspect hàng loạt bằng fabric

Attribute phù hợp khi mỗi nơi áp dụng cần được thể hiện rõ ràng. Với các quy ước diện rộng, việc thêm cùng một attribute lặp đi lặp lại lại trở thành một công việc bảo trì khác.

Fabric cho phép chọn các khai báo bằng lập trình và áp dụng aspect thông qua những truy vấn theo phong cách LINQ.

Với LogAttribute ở trên, một project fabric có thể chọn các phương thức public trong những kiểu public:

using Metalama.Framework.Aspects;<br>using Metalama.Framework.Code;<br>using Metalama.Framework.Fabrics;<br><br>internal sealed class LoggingFabric : NamespaceFabric<br>{<br>    public override void AmendNamespace(INamespaceAmender amender)<br>    {<br>        amender.SelectMany(ns => ns.Types)<br>            .Where(type => type.Accessibility == Accessibility.Public)<br>            .SelectMany(type => type.Methods)<br>            .Where(method => method.Accessibility == Accessibility.Public)<br>            .AddAspectIfEligible();<br>    }<br>}

Việc chọn mục tiêu dựa trên code model của dự án, vì vậy các phương thức mới phù hợp cũng tự động nhận aspect.

Đây là hoạt động chọn trong thời gian biên dịch, không phải gắn aspect trong thời gian chạy. ProjectFabric hoạt động trong phạm vi project, còn NamespaceFabric phù hợp với một quy ước có phạm vi namespace hẹp hơn.

Fabric thích hợp với các chính sách diện rộng như tracing. Với những hành vi chọn lọc hơn, cần thận trọng. Chẳng hạn, cache mọi phương thức có tên bắt đầu bằng Get có thể tạo ra hành vi sai. Hãy chọn mục tiêu dựa trên ngữ nghĩa và kiểm tra kết quả bằng CodeLens cùng Metalama Diff.

Metalama và AI agent

Một AI agent viết aspect cần có kiến thức chính xác về template, API thời gian biên dịch và code model.

Plugin Metalama cung cấp cho OpenAI Codex và Claude Code tài liệu khái niệm, tài liệu API, ví dụ và hướng dẫn về thư viện pattern.

Với OpenAI Codex, hãy thêm marketplace:

codex plugin marketplace add metalama/Metalama.AI.Skills

Sau đó mở Plugins trong ứng dụng Codex và cài đặt metalama.

Với Claude Code, chạy:

/plugin marketplace add https://github.com/metalama/Metalama.AI.Skills<br><br>/plugin install metalama

Nếu agent không sử dụng hướng dẫn chuyên biệt, hãy nêu rõ yêu cầu dùng Metalama. Luôn xem xét phần triển khai được sinh và kiểm tra hành vi như đối với bất kỳ thay đổi mã nào khác.

Bắt đầu khám phá Metalama hôm nay

Nếu mã hạ tầng lặp lại đang khiến service khó đọc hơn, Metalama là công cụ đáng để thử. Hãy bắt đầu với một aspect nhỏ, kiểm tra quá trình biến đổi và quyết định xem abstraction đó có giúp codebase dễ bảo trì hơn hay không.

Thử Metalama: làm theo hướng dẫn bắt đầu chính thức, xây dựng một logging aspect và so sánh phương thức gốc với phần triển khai được sinh.

Xin cảm ơn PostSharp Technologies vì những nỗ lực đáng kể trong việc đưa các công cụ này đến cộng đồng .NET, cũng như sự hợp tác trong bài viết.

Suy nghĩ cuối cùng

Lập trình hướng khía cạnh biến hành vi lặp lại thành một triển khai có thể tái sử dụng. Metalama hiện thực hóa cách tiếp cận đó thông qua metaprogramming tại thời điểm biên dịch, aspect tùy chỉnh, thư viện có sẵn và fabric.

Khuyến nghị của tôi là chỉ đưa Metalama vào khi một pattern thực sự lặp lại và các quy tắc của nó đã rõ ràng. Quy ước ghi log, contract và pattern thông báo thay đổi thuộc tính là những điểm khởi đầu phù hợp.

Nhìn lại phần mã Stopwatch và ghi log bị lặp lại trong dự án trước đây, aspect dành cho phương thức là tính năng tôi đặc biệt yêu thích ở Metalama.

Giảm mã khuôn mẫu cũng có nghĩa là giảm số vị trí cần bảo trì. Đây là lúc Metalama trở nên đặc biệt hữu ích: giữ cho các mối quan tâm kỹ thuật lặp lại luôn nhất quán, đồng thời giúp mã nghiệp vụ dễ đọc hơn. Đây là cách tiếp cận mà tôi luôn mong muốn có.

Tuy nhiên, Metalama có thể tạo ra độ phức tạp không cần thiết đối với một ứng dụng nhỏ, ít lặp lại. Những quyết định nghiệp vụ cần được thể hiện rõ ràng, việc phát hiện plugin trong thời gian chạy, hoặc hành vi đã được middleware hay decorator xử lý tốt có thể phù hợp hơn với các hướng tiếp cận khác.

Cách áp dụng tốt nhất là tiến từng bước: chọn một pattern, kiểm tra mã được sinh, kiểm thử hành vi và chỉ mở rộng khi kết quả giúp cả đội dễ hiểu hơn.

Tài liệu tham khảo

Phần thêm

Bạn có thể vừa đọc, vừa thưởng thức tuyển tập những bản Nocturne nổi tiếng của Chopin trong một video AI Story Art do Classical Oasis thực hiện.

Chúc bạn có trải nghiệm thú vị khi tìm hiểu, học tập và lập trình cùng Metalama.