Xin chào các bạn đồng nghiệp tương lai trên con đường chinh phục .NET! Chúng ta đã cùng nhau đi qua nhiều chặng đường trong Lộ trình học ASP.NET Core 2025, từ những kiến thức nền tảng về C#, Hệ Sinh Thái .NET, cho đến các chủ đề chuyên sâu hơn như cơ sở dữ liệu (SQL, EF Core, so sánh ORM), API (RESTful, GraphQL, gRPC), và các kiến trúc ứng dụng. Hôm nay, chúng ta sẽ khám phá một chủ đề mới nhưng cực kỳ hữu ích: Template Engine.
Trong nhiều ứng dụng, chúng ta cần tạo ra các nội dung động dựa trên dữ liệu. Đó có thể là nội dung email gửi cho người dùng, các báo cáo tùy chỉnh, các file cấu hình được tạo tự động, hoặc thậm chí là code. Thay vì viết code C# phức tạp để nối chuỗi hoặc sử dụng các StringBuilder lặp đi lặp lại, Template Engine cung cấp một cách tiếp cận sạch sẽ, dễ bảo trì và linh hoạt hơn nhiều.
Trong bài viết này, chúng ta sẽ tìm hiểu về hai thư viện Template Engine phổ biến trong hệ sinh thái .NET: Fluid và Scriban. Chúng ta sẽ khám phá cách chúng hoạt động, cú pháp cơ bản, cách tích hợp vào ứng dụng .NET, và so sánh để biết khi nào nên sử dụng công cụ nào.
Mục lục
Template Engine là gì và Tại sao chúng ta cần chúng?
Hãy tưởng tượng bạn cần tạo một email chào mừng tự động cho người dùng mới đăng ký. Nội dung email sẽ có cấu trúc cố định, nhưng các thông tin như tên người dùng, ngày đăng ký, hoặc liên kết kích hoạt tài khoản sẽ thay đổi tùy theo từng người dùng.
Cách tiếp cận “truyền thống” có thể là:
string template = "Xin chào [Tên Người Dùng],\nCảm ơn bạn đã đăng ký vào ngày [Ngày Đăng Ký]. Vui lòng click vào link sau để kích hoạt tài khoản: [Link Kích Hoạt].\nTrân trọng!";
string emailBody = template
.Replace("[Tên Người Dùng]", user.Name)
.Replace("[Ngày Đăng Ký]", user.RegistrationDate.ToString("dd/MM/yyyy"))
.Replace("[Link Kích Hoạt]", activationLink);
Cách này hoạt động cho các template đơn giản, nhưng sẽ nhanh chóng trở thành cơn ác mộng khi template trở nên phức tạp hơn với các điều kiện (ví dụ: “nếu người dùng là thành viên VIP thì thêm dòng này”), các vòng lặp (ví dụ: “liệt kê danh sách các sản phẩm đã mua”), hoặc cần định dạng dữ liệu.
Đây là lúc Template Engine tỏa sáng. Về cơ bản, Template Engine là một công cụ cho phép bạn viết các “template” (các văn bản mẫu có chứa các placeholder và logic trình bày đơn giản) và sau đó “render” (kết xuất) chúng bằng cách cung cấp dữ liệu. Engine sẽ điền dữ liệu vào các placeholder và thực hiện các logic nhỏ được định nghĩa trong template để tạo ra chuỗi kết quả cuối cùng.
Các trường hợp sử dụng phổ biến trong .NET:
- Tạo email động: Một trong những ứng dụng phổ biến nhất.
- Tạo nội dung HTML: Mặc dù ASP.NET Core MVC/Razor Pages/Blazor đã có Razor cho việc này, các template engine vẫn hữu ích cho các phần nhỏ, độc lập hoặc các hệ thống không dựa trên Razor.
- Tạo báo cáo văn bản hoặc HTML đơn giản: Xuất dữ liệu dưới dạng bảng, danh sách.
- Tạo file cấu hình động: Ví dụ: tạo file cấu hình cho các dịch vụ dựa trên môi trường triển khai.
- Code Generation: Tạo ra các đoạn code lặp đi lặp lại dựa trên cấu trúc dữ liệu (ví dụ: tạo các lớp DTO, boilerplate code).
- Xử lý các file template do người dùng cung cấp: Với các engine hỗ trợ sandbox, bạn có thể cho phép người dùng tùy chỉnh template mà không lo ngại về vấn đề bảo mật.
Khám phá Fluid Template Engine
Fluid là một Template Engine cho .NET được lấy cảm hứng mạnh mẽ từ Liquid, một ngôn ngữ template được phát triển bởi Shopify và rất phổ biến trong các nền tảng thương mại điện tử và hệ thống quản lý nội dung.
Fluid được viết hoàn toàn bằng C#, tận dụng tối đa các tính năng và hiệu suất của .NET. Nó được thiết kế để an toàn và có thể được sử dụng trong các môi trường mà template có thể đến từ các nguồn không đáng tin cậy (ví dụ: do người dùng chỉnh sửa) nhờ cơ chế “sandbox” tích hợp.
Cài đặt Fluid
Bạn có thể cài đặt Fluid thông qua NuGet Package Manager hoặc .NET CLI:
dotnet add package Fluid
Cú pháp Fluid cơ bản
Cú pháp của Fluid (và Liquid) khá đơn giản và dễ đọc, dựa trên hai loại thẻ chính:
- Objects (Đối tượng): Được bao quanh bởi
{{
và}}
. Chúng xuất dữ liệu từ các biến hoặc kết quả của các filter. Ví dụ:{{ user.name }}
- Tags (Thẻ): Được bao quanh bởi
{%
và%}
. Chúng thực hiện logic như vòng lặp, điều kiện, gán biến. Ví dụ:{% if user.is_vip %}
Ví dụ sử dụng Fluid
Đầu tiên, chúng ta cần tạo một FluidParser
và parse template string. Sau đó, tạo một TemplateContext
chứa dữ liệu và render template.
using Fluid;
using Fluid.Values;
// Giả định bạn có một đối tượng dữ liệu
var userData = new
{
Name = "Nguyễn Văn A",
IsVip = true,
Products = new[]
{
new { Name = "Laptop", Price = 1200.00 },
new { Name = "Mouse", Price = 25.50 }
}
};
// Định nghĩa template string
string templateString = @"
Xin chào {{ Name }},
Cảm ơn bạn đã mua sắm tại cửa hàng của chúng tôi.
{% if IsVip %}
Chúng tôi có ưu đãi đặc biệt dành riêng cho khách hàng VIP!
{% endif %}
Các sản phẩm bạn đã mua:
{% for product in Products %}
- {{ product.Name }}: {{ product.Price | currency }}
{% endfor %}
Tổng cộng: {{ Products | array.size }} sản phẩm.
Trân trọng,
Cửa hàng ABC.
";
// Tạo parser
var parser = new FluidParser();
// Parse template
if (parser.TryParse(templateString, out var template, out var error))
{
// Tạo context với dữ liệu.
// Fluid sử dụng FluidValue để bọc dữ liệu.
// TemplateContext.GlobalScope có thể được cấu hình để xử lý các loại dữ liệu phức tạp.
// Hoặc đơn giản hơn, sử dụng TemplateContext.CreateObjectScope
var context = new TemplateContext();
context.MemberAccessStrategy.Register(userData.GetType()); // Đăng ký kiểu dữ liệu để Fluid biết cách truy cập properties
context.SetValue("Name", userData.Name);
context.SetValue("IsVip", userData.IsVip);
context.SetValue("Products", userData.Products); // Fluid có thể xử lý IEnumerable
// Hoặc dùng cách ngắn gọn hơn với object scope (cần thư viện Fluid.Core)
// var context = new TemplateContext(userData, new MemberAccessStrategy());
// context.MemberAccessStrategy.Register(userData.GetType());
// Render template
string result = template.Render(context);
Console.WriteLine(result);
}
else
{
Console.WriteLine($"Lỗi khi parse template: {error}");
}
Giải thích:
- Chúng ta định nghĩa một đối tượng
userData
chứa các thông tin cần thiết. templateString
chứa cú pháp Fluid:{{ Name }}
,{{ product.Name }}
để hiển thị giá trị;{% if IsVip %}
để kiểm tra điều kiện;{% for product in Products %}
để lặp qua danh sách.| currency
là một “filter” được áp dụng cho giá trịproduct.Price
để định dạng nó thành tiền tệ. Fluid có sẵn nhiều filter và bạn có thể tạo filter tùy chỉnh.TemplateContext
là nơi chúng ta đặt dữ liệu mà template có thể truy cập. Quan trọng là cấu hìnhMemberAccessStrategy
để Fluid biết cách đọc các property từ các đối tượng .NET của bạn một cách an toàn (tránh truy cập các property không mong muốn).
Ưu điểm của Fluid:
- Sandbox Security: Đây là điểm mạnh lớn nhất của Fluid. Nó được thiết kế để ngăn chặn template truy cập vào các phần nhạy cảm của ứng dụng hoặc thực hiện các hành động độc hại. Điều này làm cho nó lý tưởng cho các tình huống mà template được cung cấp hoặc chỉnh sửa bởi người dùng không đáng tin cậy.
- Tương thích Liquid: Nếu bạn quen thuộc với Liquid hoặc cần xử lý các template Liquid hiện có, Fluid là lựa chọn tuyệt vời.
- Được phát triển cho .NET: Tận dụng hệ sinh thái và hiệu suất của .NET.
Nhược điểm của Fluid:
- Cú pháp hạn chế: So với một số engine khác, cú pháp Liquid/Fluid có thể hơi hạn chế ở các khía cạnh logic phức tạp hoặc truy cập các chức năng .NET nâng cao.
- Yêu cầu cấu hình MemberAccessStrategy: Cần thiết cho bảo mật nhưng đôi khi có thể hơi rườm rà khi bắt đầu.
Khám phá Scriban Template Engine
Scriban là một Template Engine khác được phát triển cho .NET, tập trung vào hiệu suất và tính linh hoạt. Scriban có cú pháp lấy cảm hứng từ Liquid, nhưng mở rộng thêm nhiều tính năng mạnh mẽ, đặc biệt là khả năng gọi các hàm và truy cập các thành viên của đối tượng C# một cách tự nhiên hơn (mặc dù vẫn có các cơ chế kiểm soát an toàn).
Cài đặt Scriban
Cài đặt Scriban cũng rất đơn giản qua NuGet:
dotnet add package Scriban
Cú pháp Scriban cơ bản
Cú pháp của Scriban tương tự Liquid/Fluid ở các điểm cơ bản ({{ }}
cho output, {% %}
cho control flow), nhưng có nhiều điểm khác biệt và mở rộng:
- Objects/Variables: Sử dụng
{{ variable }}
. Scriban hỗ trợ truy cập thành viên lồng nhau bằng dấu chấm:{{ user.profile.address }}
. - Control Flow Tags: Sử dụng
{% tag %}
. Các tag phổ biến nhưif
,for
(với nhiều tùy chọn hơn),while
,case
,include
, v.v. - Functions/Scripts: Scriban có một hệ thống hàm tích hợp phong phú và cho phép bạn định nghĩa hàm riêng. Cú pháp gọi hàm thường là
{{ function_name arg1 arg2 }}
hoặc sử dụng pipe{{ value | function_name arg1 }}
.
Ví dụ sử dụng Scriban
Với Scriban, chúng ta sử dụng lớp Template
:
using Scriban;
using Scriban.Runtime;
// Giả định bạn có cùng đối tượng dữ liệu
var userData = new
{
Name = "Nguyễn Văn A",
IsVip = true,
Products = new[]
{
new { Name = "Laptop", Price = 1200.00 },
new { Name = "Mouse", Price = 25.50 }
}
};
// Định nghĩa template string (sử dụng cú pháp Scriban)
// Lưu ý sự khác biệt nhỏ về cú pháp filter (gọi là script/function trong Scriban)
string templateString = @"
Xin chào {{ Name }},
Cảm ơn bạn đã mua sắm tại cửa hàng của chúng tôi.
{% if IsVip %}
Chúng tôi có ưu đãi đặc biệt dành riêng cho khách hàng VIP!
{% endendif %}
Các sản phẩm bạn đã mua:
{% for product in Products %}
- {{ product.Name }}: {{ product.Price | number.to_string 'C' }}
{% endfor %}
Tổng cộng: {{ Products | array.size }} sản phẩm.
Trân trọng,
Cửa hàng ABC.
";
// Parse template
// Template.Parse có thể ném ngoại lệ nếu có lỗi cú pháp
try
{
var template = Template.Parse(templateString);
// Tạo ScriptObject để truyền dữ liệu.
// Bạn có thể thêm các đối tượng hoặc giá trị vào đây.
// Scriban cho phép mapping trực tiếp từ .NET object nếu bạn muốn.
var scriptObject = new ScriptObject();
scriptObject.Add("Name", userData.Name);
scriptObject.Add("IsVip", userData.IsVip);
scriptObject.Add("Products", userData.Products);
// Cấu hình LiquidMemberAccessPolicy để kiểm soát quyền truy cập properties (tùy chọn nhưng nên làm)
// Nếu không cấu hình, Scriban mặc định sẽ truy cập được hầu hết public members.
// var context = new TemplateContext();
// context.PushGlobal(scriptObject);
// context.MemberRenamer = member => member.Name; // Hoặc tùy chỉnh cách đổi tên
// Render template với ScriptObject
string result = template.Render(scriptObject);
Console.WriteLine(result);
}
catch (ParseException ex)
{
Console.WriteLine($"Lỗi khi parse template:");
foreach (var message in ex.Messages)
{
Console.WriteLine(message);
}
}
catch (Exception ex)
{
Console.WriteLine($"Lỗi khi render template: {ex.Message}");
}
Giải thích:
- Scriban sử dụng
Template.Parse
để parse template. Nó có thể ném ngoại lệParseException
nếu cú pháp sai. - Dữ liệu được truyền thông qua
ScriptObject
. Bạn thêm các cặp key-value vào đây. Scriban cũng hỗ trợ truyền trực tiếp các đối tượng .NET và sử dụng reflection để truy cập các public member. - Cú pháp tương tự Fluid nhưng có khác biệt nhỏ, ví dụ
{% endif %}
thay vì{% endif %}
. - Scriban có các hàm tích hợp mạnh mẽ hơn, ví dụ
number.to_string 'C'
để định dạng số thành tiền tệ. - Scriban cũng có cơ chế kiểm soát truy cập thành viên, nhưng mặc định ít “sandbox” hơn Fluid trừ khi bạn cấu hình rõ ràng.
Ưu điểm của Scriban:
- Hiệu suất cao: Scriban thường được biết đến với hiệu suất rendering tốt.
- Tính năng phong phú: Hỗ trợ nhiều control flow tags hơn, hệ thống hàm tích hợp mạnh mẽ (string manipulation, toán học, collection, v.v.).
- Truy cập .NET dễ dàng: Cho phép truy cập và gọi các public member của đối tượng C# một cách tự nhiên hơn (có thể kiểm soát bằng Policy).
- Cú pháp linh hoạt: Gần gũi với các ngôn ngữ lập trình truyền thống hơn ở một số khía cạnh (như gọi hàm).
Nhược điểm của Scriban:
- Sandbox ít nghiêm ngặt mặc định: So với Fluid, Scriban yêu cầu cấu hình rõ ràng hơn nếu bạn cần một sandbox chặt chẽ cho template từ nguồn không đáng tin cậy.
- Cú pháp phức tạp hơn: Với các tính năng nâng cao, cú pháp có thể trở nên phức tạp hơn so với Fluid đơn giản.
Fluid vs. Scriban: Lựa chọn nào?
Cả Fluid và Scriban đều là những Template Engine tuyệt vời cho .NET, nhưng chúng có điểm mạnh và điểm yếu riêng. Bảng so sánh dưới đây có thể giúp bạn đưa ra quyết định:
Tính năng | Fluid | Scriban |
---|---|---|
Nguồn gốc cảm hứng | Shopify Liquid | Scripting language/Liquid-like |
Cú pháp | Liquid ({{ }} , {% %} ) |
Liquid-like, thêm nhiều cú pháp scripting (gọi hàm,…) |
Bảo mật (Sandbox) | Thiết kế mặc định theo hướng sandbox, yêu cầu đăng ký truy cập thành viên rõ ràng. Rất tốt cho template không đáng tin cậy. | Cho phép truy cập .NET members mặc định, có thể cấu hình policy để hạn chế. Ít nghiêm ngặt hơn trừ khi được cấu hình. |
Tính năng nâng cao (Hàm, Logic) | Ít phong phú hơn, chủ yếu dùng filters. Tập trung vào trình bày đơn giản. | Hệ thống hàm tích hợp mạnh mẽ, hỗ trợ nhiều control flow tags hơn. Linh hoạt hơn cho logic trong template. |
Hiệu năng | Tốt | Rất tốt, thường được đánh giá cao về hiệu năng. |
Khả năng mở rộng | Cho phép tạo custom filters và tags. | Cho phép tạo custom functions và objects. |
Trường hợp sử dụng điển hình | Email marketing, template do người dùng chỉnh sửa, các ứng dụng cần sandbox mạnh. | Code generation, tạo file cấu hình phức tạp, báo cáo nội bộ, các ứng dụng cần hiệu năng cao và logic template phức tạp hơn. |
Khi nào chọn Fluid?
- Khi yêu cầu bảo mật (sandbox) là ưu tiên hàng đầu, đặc biệt nếu bạn cho phép người dùng cuối tùy chỉnh template.
- Khi bạn cần tương thích với cú pháp Liquid hiện có.
- Khi bạn chỉ cần một ngôn ngữ template đơn giản chủ yếu để hiển thị dữ liệu và các logic trình bày cơ bản (điều kiện, lặp).
Khi nào chọn Scriban?
- Khi hiệu năng là yếu tố quan trọng.
- Khi bạn cần các tính năng scripting mạnh mẽ hơn bên trong template (gọi hàm phức tạp, logic phức tạp hơn một chút).
- Khi bạn cần truy cập dễ dàng hơn vào các public member của đối tượng C# từ template (với sự kiểm soát bằng policy).
- Khi template được tạo hoặc quản lý bởi các nguồn đáng tin cậy (do developer viết).
Trong nhiều trường hợp, cả hai đều có thể đáp ứng nhu cầu cơ bản. Lựa chọn cuối cùng có thể phụ thuộc vào sở thích cá nhân, yêu cầu cụ thể về bảo mật/hiệu năng, hoặc các tính năng nâng cao mà một trong hai thư viện cung cấp.
Một vài lưu ý và Best Practices
- Giữ logic trong template đơn giản: Template Engine không phải là nơi lý tưởng để viết business logic phức tạp. Hãy chuẩn bị dữ liệu cẩn thận trong code C# (sử dụng các DTO hoặc ViewModel phù hợp) trước khi đưa vào template. Template chỉ nên làm nhiệm vụ trình bày dữ liệu đó.
- Sử dụng đối tượng mạnh (Strongly-typed objects): Truyền các instance của class C# (đã đăng ký MemberAccessStrategy với Fluid hoặc sử dụng ScriptObject/mapping với Scriban) thay vì các Dictionary hoặc Anonymous Type sẽ giúp template dễ đọc và dễ bảo trì hơn.
- Xử lý lỗi: Template Engine có thể ném lỗi khi parse hoặc render. Luôn bao bọc các hoạt động này trong
try-catch
và ghi log lỗi cẩn thận (như chúng ta đã học với Serilog hoặc NLog) để dễ dàng debug. - Cache Template: Parse template tốn tài nguyên. Nếu bạn sử dụng cùng một template nhiều lần, hãy parse nó một lần và lưu trữ instance của template để render nhiều lần với các bộ dữ liệu khác nhau. Cả Fluid và Scriban đều hỗ trợ điều này.
- An toàn là trên hết (với template không đáng tin cậy): Nếu template đến từ người dùng, luôn sử dụng các cơ chế sandbox và kiểm soát truy cập chặt chẽ để ngăn chặn tấn công (ví dụ: truy cập file hệ thống, gọi các phương thức nguy hiểm). Fluid có lợi thế lớn ở điểm này với thiết kế mặc định an toàn.
Kết luận
Template Engine là một công cụ mạnh mẽ giúp tách biệt logic xử lý dữ liệu khỏi logic trình bày, làm cho code của bạn sạch sẽ, dễ đọc và dễ bảo trì hơn khi làm việc với nội dung động. Fluid và Scriban là hai lựa chọn hàng đầu trong .NET, mỗi loại có những ưu điểm riêng.
Fluid nổi bật với khả năng sandbox mạnh mẽ và cú pháp Liquid quen thuộc, là lựa chọn an toàn cho các template từ nguồn không đáng tin cậy hoặc khi cần tương thích Liquid. Scriban cung cấp hiệu năng vượt trội và tính năng scripting phong phú hơn, lý tưởng cho các template nội bộ phức tạp hơn hoặc khi hiệu năng là cực kỳ quan trọng.
Việc lựa chọn và sử dụng thành thạo ít nhất một trong hai thư viện này sẽ là một kỹ năng giá trị trên Lộ trình .NET của bạn, mở ra nhiều khả năng trong việc tạo ra các ứng dụng linh hoạt và có khả năng tùy chỉnh cao.
Hãy dành thời gian cài đặt và thử nghiệm với cả Fluid và Scriban. Tạo một vài template đơn giản, thử các vòng lặp, điều kiện, và filter/function. Thực hành là cách tốt nhất để nắm vững chúng.
Ở bài viết tiếp theo, chúng ta sẽ chuyển sang một chủ đề khác cũng không kém phần quan trọng. Hãy cùng chờ đón nhé!