using System; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Svrnty.CQRS.Grpc.Generators; /// /// Incremental source generator that generates .proto files from C# commands and queries /// [Generator] public class ProtoFileSourceGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { // Register a post-initialization output to generate the proto file context.RegisterPostInitializationOutput(ctx => { // Generate a placeholder - the actual proto will be generated in the source output }); // Collect all command and query types var commandsAndQueries = context.SyntaxProvider .CreateSyntaxProvider( predicate: static (s, _) => IsCommandOrQuery(s), transform: static (ctx, _) => GetTypeSymbol(ctx)) .Where(static m => m is not null) .Collect(); // Combine with compilation to have access to it var compilationAndTypes = context.CompilationProvider.Combine(commandsAndQueries); // Generate proto file when commands/queries change context.RegisterSourceOutput(compilationAndTypes, (spc, source) => { var (compilation, types) = source; if (types.IsDefaultOrEmpty) return; try { // Get build properties for configuration var packageName = GetBuildProperty(spc, "RootNamespace") ?? "cqrs"; var csharpNamespace = GetBuildProperty(spc, "RootNamespace") ?? "Generated.Grpc"; // Generate the proto file content var generator = new ProtoFileGenerator(compilation); var protoContent = generator.Generate(packageName, csharpNamespace); // Output as an embedded resource that can be extracted var protoFileName = "cqrs_services.proto"; // Generate a C# class that contains the proto content // This allows build tools to extract it if needed var csContent = $$""" // #nullable enable namespace Svrnty.CQRS.Grpc.Generated { /// /// Contains the auto-generated Protocol Buffer definition /// internal static class GeneratedProtoFile { public const string FileName = "{{protoFileName}}"; public const string Content = @"{{protoContent.Replace("\"", "\"\"")}}"; } } """; spc.AddSource("GeneratedProtoFile.g.cs", csContent); // Report that we generated the proto content var descriptor = new DiagnosticDescriptor( "CQRSGRPC002", "Proto file generated", "Generated proto file content in GeneratedProtoFile class", "Svrnty.CQRS.Grpc", DiagnosticSeverity.Info, isEnabledByDefault: true); spc.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None)); } catch (Exception ex) { // Report diagnostic if generation fails var descriptor = new DiagnosticDescriptor( "CQRSGRPC001", "Proto file generation failed", "Failed to generate proto file: {0}", "Svrnty.CQRS.Grpc", DiagnosticSeverity.Warning, isEnabledByDefault: true); spc.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None, ex.Message)); } }); } private static bool IsCommandOrQuery(SyntaxNode node) { if (node is not TypeDeclarationSyntax typeDecl) return false; var name = typeDecl.Identifier.Text; return name.EndsWith("Command") || name.EndsWith("Query"); } private static INamedTypeSymbol? GetTypeSymbol(GeneratorSyntaxContext context) { var typeDecl = (TypeDeclarationSyntax)context.Node; var symbol = context.SemanticModel.GetDeclaredSymbol(typeDecl) as INamedTypeSymbol; // Skip if it has GrpcIgnore attribute if (symbol?.GetAttributes().Any(a => a.AttributeClass?.Name == "GrpcIgnoreAttribute") == true) return null; return symbol; } private static string? GetBuildProperty(SourceProductionContext context, string propertyName) { // Try to get build properties from the compilation options // This is a simplified approach - in practice, you might need analyzer config return null; // Will use defaults } }