Các thư viện phổ biến như CommunityToolkit.Mvvm cung cấp một trải nghiệm lập trình mượt mà bằng cách vừa cung cấp các API để gọi trực tiếp, vừa tự động sinh mã nguồn thông qua Source Generator. Thực tế, cơ chế này không nằm trong một dự án duy nhất mà là sự kết hợp của hai thành phần riêng biệt được đóng gói chung vào một tệp NuGet.
Cấu trúc giải pháp
Để xây dựng một thư viện "lai" như vậy, bạn cần tổ chức ít nhất ba dự án trong Solution:
- Source Generator Project: Chứa logic phân tích cú pháp và tạo mã tự động.
- Main Library Project: Chứa các logic runtime và là dự án chính dùng để đóng gói NuGet.
- Consumer Project: Dự án ứng dụng để kiểm tra tính năng.
1. Xây dựng Source Generator
Giả sử chúng ta muốn tạo một Generator đọc một tệp văn bản đính kèm (Additional File) có tên blueprint.txt. Với mỗi dòng văn bản trong tệp, nó sẽ sinh ra một lớp C# tương ứng.
Cấu hình dự án GeneratorCore.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" PrivateAssets="all" />
</ItemGroup>
</Project>
Mã nguồn xử lý logic sinh mã:
using Microsoft.CodeAnalysis;
using System.IO;
using System.Linq;
namespace GeneratorCore
{
[Generator]
public class ClassBlueprintGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var blueprints = context.AdditionalTextsProvider
.Where(file => Path.GetFileName(file.Path).Equals("blueprint.txt", System.StringComparison.OrdinalIgnoreCase))
.Collect();
context.RegisterSourceOutput(blueprints, (spc, files) =>
{
foreach (var file in files)
{
var content = file.GetText()?.ToString();
if (string.IsNullOrWhiteSpace(content)) continue;
string className = content.Trim();
string source = $@"
namespace GeneratedNamespace
{{
public partial class {className}
{{
public void DisplayInfo() => System.Console.WriteLine(""Generated class: {className}"");
}}
}}";
spc.AddSource($"{className}.g.cs", source);
}
});
}
}
}
2. Cấu hình Runtime Library để tích hợp Generator
Dự án chính (ví dụ: SharedLib.csproj) đóng vai trò là "vỏ bọc" NuGet. Ngoài các class thông thường, chúng ta cần cấu hình để trình đóng gói nhúng tệp DLL của Generator vào đúng thư mục kỹ thuật của NuGet.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>
<ItemGroup>
<!-- Đường dẫn đến DLL của project Generator -->
<None Include="..\GeneratorCore\bin\$(Configuration)\netstandard2.0\GeneratorCore.dll"
PackagePath="analyzers\dotnet\roslyn4.0\cs"
Pack="true"
Visible="false" />
</ItemGroup>
</Project>
Lưu ý quan trọng: Thuộc tính PackagePath="analyzers\dotnet\roslyn4.0\cs" chỉ định cho NuGet biết đây là một Analyzer/Generator. Khi người dùng cài đặt gói này, Roslyn sẽ tự động nhận diện và kích hoạt trình tạo mã mà không cần cấu hình thêm.
3. Sử dụng và kiểm tra
Trong dự án ứng dụng (Console App), sau khi cài đặt gói NuGet hoặc tham chiếu trực tiếp, bạn cần khai báo tệp blueprint.txt như một AdditionalFiles trong dự án:
<ItemGroup>
<AdditionalFiles Include="blueprint.txt" />
</ItemGroup>
Giả sử nội dung của blueprint.txt là MyDynamicService. Trình biên dịch sẽ tự động tạo ra class và bạn có thể gọi trực tiếp trong Program.cs:
using GeneratedNamespace;
var service = new MyDynamicService();
service.DisplayInfo(); // Output: Generated class: MyDynamicService
Bằng cách này, gói NuGet của bạn không chỉ cung cấp các thư viện dùng chung mà còn có khả năng can thiệp vào quá trình biên dịch để tối ưu hóa mã nguồn cho người dùng cuối.