You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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:

Solution: Roslyn Analyzer + MSBuild Tasks for File Generation & Resource Integration

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

  1. Roslyn Analyzer: Replace T4's reflection/code parsing to identify entities inheriting BaseEntity with the RequiresAudit attribute.
  2. MSBuild Task: Use the analyzer's output to generate your 3 SQL trigger files and docs.json directly into your project directory (visible in VS).
  3. 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 OutputDir to 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 15:47:28