#pragma warning disable RS1035 // Do not use APIs banned for analyzers - This is an MSBuild task, not an analyzer using System; using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; namespace Svrnty.CQRS.Grpc.Generators; /// /// MSBuild task that generates .proto files by creating its own Roslyn compilation. /// This runs BEFORE CoreCompile to solve the source generator timing issue. /// public class GenerateProtoFileTask : Task { /// /// The project directory /// [Required] public string ProjectDirectory { get; set; } = string.Empty; /// /// The output directory where the proto file should be written (typically Protos/) /// [Required] public string OutputDirectory { get; set; } = string.Empty; /// /// The name of the proto file to generate (typically cqrs_services.proto) /// [Required] public string ProtoFileName { get; set; } = string.Empty; /// /// The C# source files to compile (from @(Compile) ItemGroup) /// [Required] public ITaskItem[] SourceFiles { get; set; } = Array.Empty(); /// /// The assembly references (from @(ReferencePath) ItemGroup) /// [Required] public ITaskItem[] References { get; set; } = Array.Empty(); /// /// The root namespace of the project /// public string RootNamespace { get; set; } = string.Empty; /// /// The assembly name of the project /// public string AssemblyName { get; set; } = string.Empty; public override bool Execute() { try { Log.LogMessage(MessageImportance.High, "Svrnty.CQRS.Grpc: Generating proto file via MSBuild task..."); // Determine the namespace for the proto file var projectNamespace = !string.IsNullOrEmpty(RootNamespace) ? RootNamespace : !string.IsNullOrEmpty(AssemblyName) ? AssemblyName : "Generated"; var grpcNamespace = $"{projectNamespace}.Grpc"; var packageName = "cqrs"; // Create the compilation var compilation = CreateCompilation(); if (compilation == null) { Log.LogWarning("Svrnty.CQRS.Grpc: Could not create compilation. Writing placeholder proto file."); WritePlaceholderProto(grpcNamespace); return true; } // Check for compilation errors that would prevent proper analysis var diagnostics = compilation.GetDiagnostics() .Where(d => d.Severity == DiagnosticSeverity.Error) .ToList(); if (diagnostics.Count > 0) { Log.LogMessage(MessageImportance.Normal, $"Svrnty.CQRS.Grpc: Compilation has {diagnostics.Count} errors. Attempting to generate proto anyway..."); // Log first few errors for debugging foreach (var diag in diagnostics.Take(5)) { Log.LogMessage(MessageImportance.Low, $" {diag.GetMessage()}"); } } // Use the ProtoFileGenerator to generate content var generator = new ProtoFileGenerator(compilation); var protoContent = generator.Generate(packageName, grpcNamespace); // Check if we got meaningful content if (string.IsNullOrWhiteSpace(protoContent) || !protoContent.Contains("rpc ")) { Log.LogMessage(MessageImportance.High, "Svrnty.CQRS.Grpc: No commands/queries/notifications found. Writing minimal proto file."); WritePlaceholderProto(grpcNamespace); return true; } // Ensure output directory exists var fullOutputPath = Path.IsPathRooted(OutputDirectory) ? OutputDirectory : Path.Combine(ProjectDirectory, OutputDirectory); Directory.CreateDirectory(fullOutputPath); // Write the proto file var protoFilePath = Path.Combine(fullOutputPath, ProtoFileName); File.WriteAllText(protoFilePath, protoContent); Log.LogMessage(MessageImportance.High, $"Svrnty.CQRS.Grpc: Successfully generated proto file at {protoFilePath}"); return true; } catch (Exception ex) { Log.LogErrorFromException(ex, true); return false; } } private CSharpCompilation? CreateCompilation() { try { // Parse all source files into syntax trees var syntaxTrees = new List(); foreach (var sourceFile in SourceFiles) { var filePath = sourceFile.ItemSpec; if (!Path.IsPathRooted(filePath)) { filePath = Path.Combine(ProjectDirectory, filePath); } if (!File.Exists(filePath)) { Log.LogMessage(MessageImportance.Low, $"Source file not found: {filePath}"); continue; } try { var sourceText = File.ReadAllText(filePath); var syntaxTree = CSharpSyntaxTree.ParseText( sourceText, CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest), path: filePath); syntaxTrees.Add(syntaxTree); } catch (Exception ex) { Log.LogMessage(MessageImportance.Low, $"Failed to parse {filePath}: {ex.Message}"); } } if (syntaxTrees.Count == 0) { Log.LogWarning("Svrnty.CQRS.Grpc: No source files could be parsed."); return null; } Log.LogMessage(MessageImportance.Normal, $"Svrnty.CQRS.Grpc: Parsed {syntaxTrees.Count} source files"); // Create metadata references from the References var metadataReferences = new List(); foreach (var reference in References) { var refPath = reference.ItemSpec; if (!File.Exists(refPath)) { Log.LogMessage(MessageImportance.Low, $"Reference not found: {refPath}"); continue; } try { var metadataRef = MetadataReference.CreateFromFile(refPath); metadataReferences.Add(metadataRef); } catch (Exception ex) { Log.LogMessage(MessageImportance.Low, $"Failed to load reference {refPath}: {ex.Message}"); } } Log.LogMessage(MessageImportance.Normal, $"Svrnty.CQRS.Grpc: Loaded {metadataReferences.Count} references"); // Create the compilation var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) .WithNullableContextOptions(NullableContextOptions.Enable); var assemblyName = !string.IsNullOrEmpty(AssemblyName) ? AssemblyName : "TempCompilation"; var compilation = CSharpCompilation.Create( assemblyName, syntaxTrees, metadataReferences, compilationOptions); return compilation; } catch (Exception ex) { Log.LogMessage(MessageImportance.High, $"Svrnty.CQRS.Grpc: Failed to create compilation: {ex.Message}"); return null; } } private void WritePlaceholderProto(string grpcNamespace) { var placeholderProto = $@"syntax = ""proto3""; option csharp_namespace = ""{grpcNamespace}""; package cqrs; // Placeholder proto file - will be regenerated when commands/queries are available // Using namespace: {grpcNamespace} // Empty service definitions so Grpc.Tools generates base classes service CommandService {{ }} service QueryService {{ }} service DynamicQueryService {{ }} "; var fullOutputPath = Path.IsPathRooted(OutputDirectory) ? OutputDirectory : Path.Combine(ProjectDirectory, OutputDirectory); Directory.CreateDirectory(fullOutputPath); var protoFilePath = Path.Combine(fullOutputPath, ProtoFileName); File.WriteAllText(protoFilePath, placeholderProto); Log.LogMessage(MessageImportance.High, $"Svrnty.CQRS.Grpc: Wrote placeholder proto file at {protoFilePath}"); } }