如何用Roslyn替代T4生成多文件输出制品并集成至项目?
Great question! The good news is you absolutely can replicate your T4 workflow with Roslyn while generating visible SQL/JSON files and integrating them as embedded resources—all without breaking your legacy system's existing integrations. Here's a practical, step-by-step implementation:
The core challenge here is that Roslyn source generators default to in-memory code only, but combining them with custom MSBuild tasks lets us mirror your T4 workflow exactly: analyze code with Roslyn, generate visible output files, and auto-mark them as embedded resources.
Core Approach
- Roslyn Analyzer: Replace T4's reflection/code parsing to identify entities inheriting
BaseEntitywith theRequiresAuditattribute. - MSBuild Task: Use the analyzer's output to generate your 3 SQL trigger files and
docs.jsondirectly into your project directory (visible in VS). - MSBuild Configuration: Automatically mark generated files as embedded resources while preserving the legacy system's expected file paths/format.
Step 1: Build the Roslyn Analyzer (Code Logic)
Create a .NET Standard 2.0 Roslyn Analyzer project to extract eligible entity data:
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.IO; [Generator] public class AuditEntityAnalyzer : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) { context.RegisterForSyntaxNotifications(() => new AuditEntityReceiver()); } public void Execute(GeneratorExecutionContext context) { if (context.SyntaxReceiver is not AuditEntityReceiver receiver) return; // Extract valid entity details var auditEntities = receiver.CandidateEntities .Select(syntaxNode => { if (context.SemanticModel.GetDeclaredSymbol(syntaxNode, context.CancellationToken) is not INamedTypeSymbol symbol) return null; // Get table name (fallback to entity name if no [Table] attribute) var tableName = symbol.GetAttributeData("Table")?.ConstructorArguments.First().Value.ToString() ?? symbol.Name; // Get key properties (adjust logic to match your schema) var keyProperties = symbol.GetMembers().OfType<IPropertySymbol>() .Where(p => p.GetAttributeData("Key") != null || p.Name.EndsWith("Id")) .Select(p => p.Name) .ToList(); return new AuditEntityInfo(symbol.Name, tableName, keyProperties); }) .Where(e => e != null) .ToList(); // Write analysis results to a temp file for MSBuild to read if (context.AnalyzerConfigOptions.GlobalOptions.TryGetValue("build_property.ProjectDir", out var projectDir)) { var tempPath = Path.Combine(projectDir, "obj", "AuditEntities.json"); Directory.CreateDirectory(Path.GetDirectoryName(tempPath)); File.WriteAllText(tempPath, JsonSerializer.Serialize(auditEntities)); } } } // Syntax receiver to collect candidate classes public class AuditEntityReceiver : ISyntaxReceiver { public List<ClassDeclarationSyntax> CandidateEntities { get; } = new(); public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { if (syntaxNode is ClassDeclarationSyntax classDecl && classDecl.BaseList?.Types.Any(t => t.Type.ToString() == "BaseEntity") == true && classDecl.AttributeLists.Any(al => al.Attributes.Any(a => a.Name.ToString() == "RequiresAudit"))) { CandidateEntities.Add(classDecl); } } } // Helper record to store entity metadata public record AuditEntityInfo(string EntityName, string TableName, List<string> KeyProperties); // Extension method to simplify attribute lookup public static class SymbolExtensions { public static AttributeData GetAttributeData(this ISymbol symbol, string attributeName) { return symbol.GetAttributes().FirstOrDefault(a => a.AttributeClass?.Name == attributeName); } }
Step 2: Build the MSBuild Task (File Generation)
Create a .NET Standard 2.0 MSBuild Task project to generate your output files:
using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using System.Text; using System.Text.Json; using System.IO; public class GenerateAuditFilesTask : Task { [Required] public string AuditEntitiesPath { get; set; } [Required] public string OutputDir { get; set; } public override bool Execute() { if (!File.Exists(AuditEntitiesPath)) { Log.LogWarning("No audit-eligible entities found; skipping file generation."); return true; } var auditEntities = JsonSerializer.Deserialize<List<AuditEntityInfo>>(File.ReadAllText(AuditEntitiesPath)); if (auditEntities == null || !auditEntities.Any()) return true; // Ensure output directory exists Directory.CreateDirectory(OutputDir); foreach (var entity in auditEntities) { GenerateTriggerSql(entity, "Insert"); GenerateTriggerSql(entity, "Update"); GenerateTriggerSql(entity, "Delete"); } GenerateDocsJson(auditEntities); return true; } private void GenerateTriggerSql(AuditEntityInfo entity, string actionType) { var sqlContent = $@"CREATE TRIGGER [dbo].[trg_{entity.TableName}_{actionType}] ON [dbo].[{entity.TableName}] AFTER {actionType} AS BEGIN SET NOCOUNT ON; -- Legacy tool-compatible audit logic (adjust to match your existing T4 output) INSERT INTO [dbo].[AuditLog] (EntityName, ActionType, AuditData, AuditTimestamp) VALUES ( '{entity.EntityName}', '{actionType}', (SELECT {string.Join(", ", entity.KeyProperties)} FROM inserted FOR JSON AUTO), GETUTCDATE() ); END"; var filePath = Path.Combine(OutputDir, $"{entity.EntityName}.{actionType}.sql"); File.WriteAllText(filePath, sqlContent, Encoding.UTF8); } private void GenerateDocsJson(List<AuditEntityInfo> entities) { var docContent = new { GeneratedAt = DateTime.UtcNow.ToString("o"), AuditEntities = entities.Select(e => new { e.EntityName, e.TableName, SupportedActions = new[] { "Insert", "Update", "Delete" }, KeyProperties = e.KeyProperties }) }; var filePath = Path.Combine(OutputDir, "docs.json"); File.WriteAllText(filePath, JsonSerializer.Serialize(docContent, new JsonSerializerOptions { WriteIndented = true }), Encoding.UTF8); } }
Step 3: Integrate with Your .NET 5 Project
Add this configuration to your project's .csproj file to wire up the analyzer, task, and resource embedding:
<Project Sdk="Microsoft.NET.Sdk"> <!-- Existing project configuration --> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <!-- Reference the analyzer and task projects --> <ItemGroup> <ProjectReference Include="..\AuditEntityAnalyzer\AuditEntityAnalyzer.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> <ProjectReference Include="..\GenerateAuditFilesTask\GenerateAuditFilesTask.csproj" /> </ItemGroup> <!-- Custom MSBuild target to run before compilation --> <Target Name="GenerateAuditArtifacts" BeforeTargets="CoreCompile"> <!-- Run Roslyn analyzer to get entity data --> <MSBuild Projects="$(MSBuildProjectFile)" Targets="CoreCompile" Properties="BuildProjectReferences=false" /> <!-- Generate SQL/JSON files --> <GenerateAuditFilesTask AuditEntitiesPath="$(ProjectDir)obj\AuditEntities.json" OutputDir="$(ProjectDir)GeneratedAuditFiles\" /> <!-- Match your legacy path here --> <!-- Add files to project and mark as embedded resources --> <ItemGroup> <EmbeddedResource Include="GeneratedAuditFiles\**\*.sql" /> <EmbeddedResource Include="GeneratedAuditFiles\docs.json" /> <!-- Make files visible in Visual Studio --> <None Include="GeneratedAuditFiles\**\*.sql" /> <None Include="GeneratedAuditFiles\docs.json" /> </ItemGroup> </Target> </Project>
Step 4: Maintain Legacy System Compatibility
- File Path Alignment: Set
OutputDirto the exact directory your legacy tools expect—no changes needed to their file-reading logic. - Embedded Resource Access: If your legacy system reads resources from the assembly, use this code (matching existing T4-based workflows):
using System.Reflection; using System.IO; var assembly = Assembly.GetExecutingAssembly(); // Example: Read docs.json using var resourceStream = assembly.GetManifestResourceStream("YourProjectNamespace.GeneratedAuditFiles.docs.json"); using var reader = new StreamReader(resourceStream); var docContent = reader.ReadToEnd();
Key Benefits
- Roslyn-Native: Avoids T4's lifecycle concerns and VS 2019 .NET 5 compatibility gaps.
- Visible Files: Generated SQL/JSON appears in your project just like T4 output, making debugging easy.
- Auto-Integration: Runs on every build without manual intervention.
- Legacy-Friendly: No changes required to your existing database tools or integration points.
内容的提问来源于stack exchange,提问作者Paul Stringer

