diff --git a/.DS_Store b/.DS_Store index c02464c..d5fd322 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 054511c..0a69165 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -22,7 +22,9 @@ "Bash(curl:*)", "Bash(timeout 3 cmd:*)", "Bash(timeout:*)", - "Bash(tasklist:*)" + "Bash(tasklist:*)", + "Bash(dotnet build:*)", + "Bash(dotnet --list-sdks:*)" ], "deny": [], "ask": [] diff --git a/Svrnty.CQRS.Grpc.Generators/ProtoFileSourceGenerator.cs b/Svrnty.CQRS.Grpc.Generators/ProtoFileSourceGenerator.cs index 272fadf..d240d4a 100644 --- a/Svrnty.CQRS.Grpc.Generators/ProtoFileSourceGenerator.cs +++ b/Svrnty.CQRS.Grpc.Generators/ProtoFileSourceGenerator.cs @@ -41,9 +41,10 @@ public class ProtoFileSourceGenerator : IIncrementalGenerator try { - // Get build properties for configuration - var packageName = GetBuildProperty(spc, "RootNamespace") ?? "cqrs"; - var csharpNamespace = GetBuildProperty(spc, "RootNamespace") ?? "Generated.Grpc"; + // Get the root namespace from the compilation - this matches what GrpcGenerator does + var rootNamespace = compilation.AssemblyName ?? "Generated"; + var packageName = "cqrs"; + var csharpNamespace = $"{rootNamespace}.Grpc"; // Generate the proto file content var generator = new ProtoFileGenerator(compilation); diff --git a/Svrnty.CQRS.Grpc.Generators/WriteProtoFileTask.cs b/Svrnty.CQRS.Grpc.Generators/WriteProtoFileTask.cs new file mode 100644 index 0000000..aeb9b39 --- /dev/null +++ b/Svrnty.CQRS.Grpc.Generators/WriteProtoFileTask.cs @@ -0,0 +1,130 @@ +#pragma warning disable RS1035 // Do not use APIs banned for analyzers - This is an MSBuild task, not an analyzer + +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Svrnty.CQRS.Grpc.Generators; + +/// +/// MSBuild task that extracts the auto-generated proto file content from the source generator +/// output and writes it to disk so Grpc.Tools can process it +/// +public class WriteProtoFileTask : Task +{ + /// + /// The project directory where we should look for generated files + /// + [Required] + public string ProjectDirectory { get; set; } = string.Empty; + + /// + /// The intermediate output path (typically obj/Debug/net10.0) + /// + [Required] + public string IntermediateOutputPath { 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; + + public override bool Execute() + { + try + { + Log.LogMessage(MessageImportance.High, + "Svrnty.CQRS.Grpc: Extracting auto-generated proto file..."); + + // Look for the generated C# file containing the proto content + // Source generators output to obj/Generated, not IntermediateOutputPath/Generated + var generatedFilePath = Path.Combine( + ProjectDirectory, + "obj", + "Generated", + "Svrnty.CQRS.Grpc.Generators", + "Svrnty.CQRS.Grpc.Generators.ProtoFileSourceGenerator", + "GeneratedProtoFile.g.cs" + ); + + if (!File.Exists(generatedFilePath)) + { + Log.LogWarning( + $"Generated proto file not found at {generatedFilePath}. " + + "The proto file may not have been generated yet. This is normal on first build."); + return true; // Don't fail the build, just skip + } + + // Read the generated C# file + var csContent = File.ReadAllText(generatedFilePath); + + // Extract the proto content using a more robust approach + // Looking for: public const string Content = @"..."; + var startMarker = "public const string Content = @\""; + var startIndex = csContent.IndexOf(startMarker); + + if (startIndex < 0) + { + Log.LogError($"Could not find Content property in {generatedFilePath}"); + return false; + } + + startIndex += startMarker.Length; + + // Find the closing "; - We need the LAST occurrence because the content contains escaped quotes + // The pattern is: Content = @"...content..."; + // where content has "" for literal quotes + var endMarker = "\";"; + + // Find where the next field starts or class ends to limit our search + var nextFieldOrEnd = csContent.IndexOf("\n }", startIndex); // End of class + if (nextFieldOrEnd < 0) + { + nextFieldOrEnd = csContent.Length; + } + + var endIndex = csContent.LastIndexOf(endMarker, nextFieldOrEnd, nextFieldOrEnd - startIndex); + + if (endIndex < 0 || endIndex < startIndex) + { + Log.LogError($"Could not find end of Content property in {generatedFilePath}"); + return false; + } + + // Extract and unescape doubled quotes + var protoContent = csContent.Substring(startIndex, endIndex - startIndex); + protoContent = protoContent.Replace("\"\"", "\""); + + Log.LogMessage(MessageImportance.High, + $"Extracted proto content length: {protoContent.Length} characters"); + + // Ensure output directory exists + var fullOutputPath = 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; + } + } +} diff --git a/Svrnty.CQRS.Grpc.Generators/build/Svrnty.CQRS.Grpc.Generators.targets b/Svrnty.CQRS.Grpc.Generators/build/Svrnty.CQRS.Grpc.Generators.targets index 6f77d41..e31cc0b 100644 --- a/Svrnty.CQRS.Grpc.Generators/build/Svrnty.CQRS.Grpc.Generators.targets +++ b/Svrnty.CQRS.Grpc.Generators/build/Svrnty.CQRS.Grpc.Generators.targets @@ -6,17 +6,32 @@ cqrs_services.proto - - - + + + <_GeneratorsAssemblyPath Condition="Exists('$(MSBuildThisFileDirectory)\..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)\..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll + <_GeneratorsAssemblyPath Condition="'$(_GeneratorsAssemblyPath)' == '' AND Exists('$(MSBuildThisFileDirectory)\..\bin\Debug\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)\..\bin\Debug\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll + <_GeneratorsAssemblyPath Condition="'$(_GeneratorsAssemblyPath)' == '' AND Exists('$(MSBuildThisFileDirectory)\..\bin\Release\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)\..\bin\Release\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll + + + + - - - $(MSBuildProjectDirectory) - + + + + + + + diff --git a/Svrnty.CQRS.Grpc.Sample/Protos/cqrs_services.proto b/Svrnty.CQRS.Grpc.Sample/Protos/cqrs_services.proto index 1857eeb..4cab121 100644 --- a/Svrnty.CQRS.Grpc.Sample/Protos/cqrs_services.proto +++ b/Svrnty.CQRS.Grpc.Sample/Protos/cqrs_services.proto @@ -6,46 +6,48 @@ package cqrs; // Command service for CQRS operations service CommandService { - // Adds a new user and returns the user ID + // AddUserCommand operation rpc AddUser (AddUserCommandRequest) returns (AddUserCommandResponse); - // Removes a user + // RemoveUserCommand operation rpc RemoveUser (RemoveUserCommandRequest) returns (RemoveUserCommandResponse); + } // Query service for CQRS operations service QueryService { - // Fetches a user by ID + // FetchUserQuery operation rpc FetchUser (FetchUserQueryRequest) returns (FetchUserQueryResponse); + } -// Request message for adding a user +// Request message for AddUserCommand message AddUserCommandRequest { string name = 1; string email = 2; int32 age = 3; } -// Response message containing the added user ID +// Response message for AddUserCommand message AddUserCommandResponse { int32 result = 1; } -// Request message for removing a user +// Request message for RemoveUserCommand message RemoveUserCommandRequest { int32 user_id = 1; } -// Response message for remove user (empty) +// Response message for RemoveUserCommand message RemoveUserCommandResponse { } -// Request message for fetching a user +// Request message for FetchUserQuery message FetchUserQueryRequest { int32 user_id = 1; } -// Response message containing the user +// Response message for FetchUserQuery message FetchUserQueryResponse { User result = 1; } @@ -56,3 +58,4 @@ message User { string name = 2; string email = 3; } + diff --git a/Svrnty.CQRS.Grpc.Sample/Svrnty.CQRS.Grpc.Sample.csproj b/Svrnty.CQRS.Grpc.Sample/Svrnty.CQRS.Grpc.Sample.csproj index 50b1100..8b4beef 100644 --- a/Svrnty.CQRS.Grpc.Sample/Svrnty.CQRS.Grpc.Sample.csproj +++ b/Svrnty.CQRS.Grpc.Sample/Svrnty.CQRS.Grpc.Sample.csproj @@ -32,4 +32,7 @@ + + + diff --git a/Svrnty.CQRS.Grpc/.DS_Store b/Svrnty.CQRS.Grpc/.DS_Store new file mode 100644 index 0000000..0ca91ff Binary files /dev/null and b/Svrnty.CQRS.Grpc/.DS_Store differ