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