Refactor proto generation from source generator to MSBuild task
All checks were successful
Publish NuGets / build (release) Successful in 40s
All checks were successful
Publish NuGets / build (release) Successful in 40s
Replace ProtoFileSourceGenerator and WriteProtoFileTask with a new GenerateProtoFileTask that creates its own Roslyn compilation. This solves the timing issue where source generators run too late for Grpc.Tools to process the generated proto files. The new task runs after ResolveAssemblyReferences but before _gRPC_GetProtoc and CoreCompile, ensuring the proto file exists when Grpc.Tools needs it. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
03041721ca
commit
433b852a43
256
Svrnty.CQRS.Grpc.Generators/GenerateProtoFileTask.cs
Normal file
256
Svrnty.CQRS.Grpc.Generators/GenerateProtoFileTask.cs
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
#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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MSBuild task that generates .proto files by creating its own Roslyn compilation.
|
||||||
|
/// This runs BEFORE CoreCompile to solve the source generator timing issue.
|
||||||
|
/// </summary>
|
||||||
|
public class GenerateProtoFileTask : Task
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The project directory
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string ProjectDirectory { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The output directory where the proto file should be written (typically Protos/)
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string OutputDirectory { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the proto file to generate (typically cqrs_services.proto)
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string ProtoFileName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The C# source files to compile (from @(Compile) ItemGroup)
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public ITaskItem[] SourceFiles { get; set; } = Array.Empty<ITaskItem>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The assembly references (from @(ReferencePath) ItemGroup)
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public ITaskItem[] References { get; set; } = Array.Empty<ITaskItem>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The root namespace of the project
|
||||||
|
/// </summary>
|
||||||
|
public string RootNamespace { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The assembly name of the project
|
||||||
|
/// </summary>
|
||||||
|
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<SyntaxTree>();
|
||||||
|
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<MetadataReference>();
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,124 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.CodeAnalysis;
|
|
||||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
||||||
|
|
||||||
namespace Svrnty.CQRS.Grpc.Generators;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Incremental source generator that generates .proto files from C# commands and queries
|
|
||||||
/// </summary>
|
|
||||||
[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 type declarations to trigger generation
|
|
||||||
// We use any type declaration as a trigger since ProtoFileGenerator scans all assemblies
|
|
||||||
var typeDeclarations = context.SyntaxProvider
|
|
||||||
.CreateSyntaxProvider(
|
|
||||||
predicate: static (s, _) => s is TypeDeclarationSyntax,
|
|
||||||
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(typeDeclarations);
|
|
||||||
|
|
||||||
// Generate proto file when commands/queries change
|
|
||||||
context.RegisterSourceOutput(compilationAndTypes, (spc, source) =>
|
|
||||||
{
|
|
||||||
var (compilation, types) = source;
|
|
||||||
|
|
||||||
// Note: We no longer bail out early since ProtoFileGenerator now scans all referenced assemblies
|
|
||||||
// The types from source are just a trigger - the generator will find types from all assemblies
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 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);
|
|
||||||
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 = $$"""
|
|
||||||
// <auto-generated />
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace Svrnty.CQRS.Grpc.Generated
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Contains the auto-generated Protocol Buffer definition
|
|
||||||
/// </summary>
|
|
||||||
public 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 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,187 +0,0 @@
|
|||||||
#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;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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
|
|
||||||
/// </summary>
|
|
||||||
public class WriteProtoFileTask : Task
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The project directory where we should look for generated files
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public string ProjectDirectory { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The intermediate output path (typically obj/Debug/net10.0)
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public string IntermediateOutputPath { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The output directory where the proto file should be written (typically Protos/)
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public string OutputDirectory { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The name of the proto file to generate (typically cqrs_services.proto)
|
|
||||||
/// </summary>
|
|
||||||
[Required]
|
|
||||||
public string ProtoFileName { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The root namespace of the project (optional, falls back to AssemblyName)
|
|
||||||
/// </summary>
|
|
||||||
public string RootNamespace { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The assembly name of the project (used for proto namespace if RootNamespace not set)
|
|
||||||
/// </summary>
|
|
||||||
public string AssemblyName { 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"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if proto file already exists (committed to repo or from previous build)
|
|
||||||
var existingProtoPath = Path.Combine(ProjectDirectory, OutputDirectory, ProtoFileName);
|
|
||||||
if (File.Exists(existingProtoPath) && !File.Exists(generatedFilePath))
|
|
||||||
{
|
|
||||||
Log.LogMessage(MessageImportance.High,
|
|
||||||
$"Svrnty.CQRS.Grpc: Using existing proto file at {existingProtoPath}. " +
|
|
||||||
"To regenerate, delete the file and build twice.");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.");
|
|
||||||
|
|
||||||
// Write a minimal placeholder proto file so Grpc.Tools doesn't fail
|
|
||||||
// The real content will be generated on the next build
|
|
||||||
// Use project's namespace so GrpcGenerator can find the service base types
|
|
||||||
var projectNamespace = !string.IsNullOrEmpty(RootNamespace) ? RootNamespace
|
|
||||||
: !string.IsNullOrEmpty(AssemblyName) ? AssemblyName
|
|
||||||
: "Generated";
|
|
||||||
var grpcNamespace = $"{projectNamespace}.Grpc";
|
|
||||||
|
|
||||||
var placeholderProto = $@"syntax = ""proto3"";
|
|
||||||
|
|
||||||
option csharp_namespace = ""{grpcNamespace}"";
|
|
||||||
|
|
||||||
package cqrs;
|
|
||||||
|
|
||||||
// Placeholder proto file - will be regenerated on next build with actual services
|
|
||||||
// Using namespace: {grpcNamespace}
|
|
||||||
|
|
||||||
// Empty service definitions so Grpc.Tools generates base classes
|
|
||||||
service CommandService {{
|
|
||||||
}}
|
|
||||||
|
|
||||||
service QueryService {{
|
|
||||||
}}
|
|
||||||
|
|
||||||
service DynamicQueryService {{
|
|
||||||
}}
|
|
||||||
";
|
|
||||||
var placeholderOutputPath = Path.Combine(ProjectDirectory, OutputDirectory);
|
|
||||||
Directory.CreateDirectory(placeholderOutputPath);
|
|
||||||
var placeholderProtoFilePath = Path.Combine(placeholderOutputPath, ProtoFileName);
|
|
||||||
File.WriteAllText(placeholderProtoFilePath, placeholderProto);
|
|
||||||
|
|
||||||
Log.LogMessage(MessageImportance.High,
|
|
||||||
$"Svrnty.CQRS.Grpc: Wrote placeholder proto file at {placeholderProtoFilePath}. " +
|
|
||||||
"Run build again to generate the actual proto content.");
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -9,30 +9,47 @@
|
|||||||
<!-- Determine the assembly path (different for NuGet package vs project reference) -->
|
<!-- Determine the assembly path (different for NuGet package vs project reference) -->
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_GeneratorsAssemblyPath Condition="Exists('$(MSBuildThisFileDirectory)\..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)\..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll</_GeneratorsAssemblyPath>
|
<_GeneratorsAssemblyPath Condition="Exists('$(MSBuildThisFileDirectory)\..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)\..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll</_GeneratorsAssemblyPath>
|
||||||
|
<_GeneratorsAssemblyPath Condition="'$(_GeneratorsAssemblyPath)' == '' AND Exists('$(MSBuildThisFileDirectory)Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)Svrnty.CQRS.Grpc.Generators.dll</_GeneratorsAssemblyPath>
|
||||||
<_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>
|
<_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>
|
||||||
<_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</_GeneratorsAssemblyPath>
|
<_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</_GeneratorsAssemblyPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Load the WriteProtoFileTask from the generator assembly -->
|
<!-- Load the GenerateProtoFileTask from the generator assembly -->
|
||||||
<UsingTask TaskName="Svrnty.CQRS.Grpc.Generators.WriteProtoFileTask"
|
<UsingTask TaskName="Svrnty.CQRS.Grpc.Generators.GenerateProtoFileTask"
|
||||||
AssemblyFile="$(_GeneratorsAssemblyPath)"
|
AssemblyFile="$(_GeneratorsAssemblyPath)"
|
||||||
Condition="'$(_GeneratorsAssemblyPath)' != ''" />
|
Condition="'$(_GeneratorsAssemblyPath)' != ''" />
|
||||||
|
|
||||||
<!-- This target ensures the Protos directory exists before the generator runs -->
|
<!-- This target ensures the Protos directory exists -->
|
||||||
<Target Name="EnsureProtosDirectory" BeforeTargets="CoreCompile">
|
<Target Name="EnsureProtosDirectory" BeforeTargets="SvrntyGenerateProtoFile">
|
||||||
<MakeDir Directories="$(ProtoOutputDirectory)" Condition="!Exists('$(ProtoOutputDirectory)')" />
|
<MakeDir Directories="$(ProtoOutputDirectory)" Condition="!Exists('$(ProtoOutputDirectory)')" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
<!-- Extract the proto file from the source generator output BEFORE Grpc.Tools processes protos -->
|
<!--
|
||||||
<!-- Runs before CoreCompile, after source generators have been executed -->
|
Generate the proto file BEFORE Grpc.Tools processes protos and BEFORE CoreCompile.
|
||||||
<Target Name="SvrntyExtractProtoFile" BeforeTargets="CoreCompile" AfterTargets="ResolveProjectReferences" DependsOnTargets="EnsureProtosDirectory" Condition="'$(GenerateProtoFile)' == 'true'">
|
This runs AFTER ResolveAssemblyReferences so we have access to @(ReferencePath).
|
||||||
<Message Text="Svrnty.CQRS.Grpc: Extracting auto-generated proto file to $(ProtoOutputDirectory)\$(GeneratedProtoFileName)" Importance="high" />
|
|
||||||
|
|
||||||
<WriteProtoFileTask
|
Key timing:
|
||||||
|
- AfterTargets="ResolveAssemblyReferences" ensures we have all references resolved
|
||||||
|
- BeforeTargets="_gRPC_GetProtoc;CoreCompile" ensures proto is generated before:
|
||||||
|
1. Grpc.Tools compiles the proto into C# (_gRPC_GetProtoc is Grpc.Tools' entry point)
|
||||||
|
2. CoreCompile compiles the project
|
||||||
|
-->
|
||||||
|
<Target Name="SvrntyGenerateProtoFile"
|
||||||
|
BeforeTargets="_gRPC_GetProtoc;CoreCompile"
|
||||||
|
AfterTargets="ResolveAssemblyReferences"
|
||||||
|
DependsOnTargets="EnsureProtosDirectory"
|
||||||
|
Condition="'$(GenerateProtoFile)' == 'true' AND '$(_GeneratorsAssemblyPath)' != ''">
|
||||||
|
|
||||||
|
<Message Text="Svrnty.CQRS.Grpc: Generating proto file from $(MSBuildProjectName)..." Importance="high" />
|
||||||
|
<Message Text="Svrnty.CQRS.Grpc: Source files count: @(Compile->Count())" Importance="normal" />
|
||||||
|
<Message Text="Svrnty.CQRS.Grpc: References count: @(ReferencePath->Count())" Importance="normal" />
|
||||||
|
|
||||||
|
<GenerateProtoFileTask
|
||||||
ProjectDirectory="$(MSBuildProjectDirectory)"
|
ProjectDirectory="$(MSBuildProjectDirectory)"
|
||||||
IntermediateOutputPath="$(IntermediateOutputPath)"
|
|
||||||
OutputDirectory="$(ProtoOutputDirectory)"
|
OutputDirectory="$(ProtoOutputDirectory)"
|
||||||
ProtoFileName="$(GeneratedProtoFileName)"
|
ProtoFileName="$(GeneratedProtoFileName)"
|
||||||
|
SourceFiles="@(Compile)"
|
||||||
|
References="@(ReferencePath)"
|
||||||
RootNamespace="$(RootNamespace)"
|
RootNamespace="$(RootNamespace)"
|
||||||
AssemblyName="$(AssemblyName)" />
|
AssemblyName="$(AssemblyName)" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|||||||
@ -21,13 +21,6 @@ service QueryService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DynamicQuery service for CQRS operations
|
|
||||||
service DynamicQueryService {
|
|
||||||
// Dynamic query for User
|
|
||||||
rpc QueryUsers (DynamicQueryUsersRequest) returns (DynamicQueryUsersResponse);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request message for AddUserCommand
|
// Request message for AddUserCommand
|
||||||
message AddUserCommandRequest {
|
message AddUserCommandRequest {
|
||||||
string name = 1;
|
string name = 1;
|
||||||
@ -66,46 +59,3 @@ message User {
|
|||||||
string email = 3;
|
string email = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dynamic query filter with AND/OR support
|
|
||||||
message DynamicQueryFilter {
|
|
||||||
string path = 1;
|
|
||||||
int32 type = 2; // PoweredSoft.DynamicQuery.Core.FilterType
|
|
||||||
string value = 3;
|
|
||||||
repeated DynamicQueryFilter and = 4;
|
|
||||||
repeated DynamicQueryFilter or = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic query sort
|
|
||||||
message DynamicQuerySort {
|
|
||||||
string path = 1;
|
|
||||||
bool ascending = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic query group
|
|
||||||
message DynamicQueryGroup {
|
|
||||||
string path = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic query aggregate
|
|
||||||
message DynamicQueryAggregate {
|
|
||||||
string path = 1;
|
|
||||||
int32 type = 2; // PoweredSoft.DynamicQuery.Core.AggregateType
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic query request for User
|
|
||||||
message DynamicQueryUsersRequest {
|
|
||||||
int32 page = 1;
|
|
||||||
int32 page_size = 2;
|
|
||||||
repeated DynamicQueryFilter filters = 3;
|
|
||||||
repeated DynamicQuerySort sorts = 4;
|
|
||||||
repeated DynamicQueryGroup groups = 5;
|
|
||||||
repeated DynamicQueryAggregate aggregates = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic query response for User
|
|
||||||
message DynamicQueryUsersResponse {
|
|
||||||
repeated User data = 1;
|
|
||||||
int64 total_records = 2;
|
|
||||||
int32 number_of_pages = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user