using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; namespace Svrnty.CQRS.Grpc.Generators; /// /// Generates Protocol Buffer (.proto) files from C# Command and Query types /// internal class ProtoFileGenerator { private readonly Compilation _compilation; private readonly HashSet _requiredImports = new HashSet(); private readonly HashSet _generatedMessages = new HashSet(); private readonly StringBuilder _messagesBuilder = new StringBuilder(); public ProtoFileGenerator(Compilation compilation) { _compilation = compilation; } public string Generate(string packageName, string csharpNamespace) { var commands = DiscoverCommands(); var queries = DiscoverQueries(); var dynamicQueries = DiscoverDynamicQueries(); var sb = new StringBuilder(); // Header sb.AppendLine("syntax = \"proto3\";"); sb.AppendLine(); sb.AppendLine($"option csharp_namespace = \"{csharpNamespace}\";"); sb.AppendLine(); sb.AppendLine($"package {packageName};"); sb.AppendLine(); // Imports (will be added later if needed) var importsPlaceholder = sb.Length; // Command Service if (commands.Any()) { sb.AppendLine("// Command service for CQRS operations"); sb.AppendLine("service CommandService {"); foreach (var command in commands) { var methodName = command.Name.Replace("Command", ""); var requestType = $"{command.Name}Request"; var responseType = $"{command.Name}Response"; sb.AppendLine($" // {GetXmlDocSummary(command)}"); sb.AppendLine($" rpc {methodName} ({requestType}) returns ({responseType});"); sb.AppendLine(); } sb.AppendLine("}"); sb.AppendLine(); } // Query Service if (queries.Any()) { sb.AppendLine("// Query service for CQRS operations"); sb.AppendLine("service QueryService {"); foreach (var query in queries) { var methodName = query.Name.Replace("Query", ""); var requestType = $"{query.Name}Request"; var responseType = $"{query.Name}Response"; sb.AppendLine($" // {GetXmlDocSummary(query)}"); sb.AppendLine($" rpc {methodName} ({requestType}) returns ({responseType});"); sb.AppendLine(); } sb.AppendLine("}"); sb.AppendLine(); } // DynamicQuery Service if (dynamicQueries.Any()) { sb.AppendLine("// DynamicQuery service for CQRS operations"); sb.AppendLine("service DynamicQueryService {"); foreach (var dq in dynamicQueries) { var entityName = dq.Name; var pluralName = Pluralize(entityName); var methodName = $"Query{pluralName}"; var requestType = $"DynamicQuery{pluralName}Request"; var responseType = $"DynamicQuery{pluralName}Response"; sb.AppendLine($" // Dynamic query for {entityName}"); sb.AppendLine($" rpc {methodName} ({requestType}) returns ({responseType});"); sb.AppendLine(); } sb.AppendLine("}"); sb.AppendLine(); } // Generate messages for commands foreach (var command in commands) { GenerateRequestMessage(command); GenerateResponseMessage(command); } // Generate messages for queries foreach (var query in queries) { GenerateRequestMessage(query); GenerateResponseMessage(query); } // Generate messages for dynamic queries foreach (var dq in dynamicQueries) { GenerateDynamicQueryMessages(dq); } // Append all generated messages sb.Append(_messagesBuilder); // Insert imports if any were needed if (_requiredImports.Any()) { var imports = new StringBuilder(); foreach (var import in _requiredImports.OrderBy(i => i)) { imports.AppendLine($"import \"{import}\";"); } imports.AppendLine(); sb.Insert(importsPlaceholder, imports.ToString()); } return sb.ToString(); } private List DiscoverCommands() { return _compilation.GetSymbolsWithName( name => name.EndsWith("Command"), SymbolFilter.Type) .OfType() .Where(t => !HasGrpcIgnoreAttribute(t)) .Where(t => t.TypeKind == TypeKind.Class || t.TypeKind == TypeKind.Struct) .ToList(); } private List DiscoverQueries() { return _compilation.GetSymbolsWithName( name => name.EndsWith("Query"), SymbolFilter.Type) .OfType() .Where(t => !HasGrpcIgnoreAttribute(t)) .Where(t => t.TypeKind == TypeKind.Class || t.TypeKind == TypeKind.Struct) .ToList(); } private bool HasGrpcIgnoreAttribute(INamedTypeSymbol type) { return type.GetAttributes().Any(attr => attr.AttributeClass?.Name == "GrpcIgnoreAttribute"); } private void GenerateRequestMessage(INamedTypeSymbol type) { var messageName = $"{type.Name}Request"; if (_generatedMessages.Contains(messageName)) return; _generatedMessages.Add(messageName); _messagesBuilder.AppendLine($"// Request message for {type.Name}"); _messagesBuilder.AppendLine($"message {messageName} {{"); var properties = type.GetMembers() .OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public) .ToList(); int fieldNumber = 1; foreach (var prop in properties) { if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type)) { // Skip unsupported types and add a comment _messagesBuilder.AppendLine($" // Skipped: {prop.Name} - unsupported type {prop.Type.Name}"); continue; } var protoType = ProtoFileTypeMapper.MapType(prop.Type, out var needsImport, out var importPath); if (needsImport && importPath != null) { _requiredImports.Add(importPath); } var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name); _messagesBuilder.AppendLine($" {protoType} {fieldName} = {fieldNumber};"); // If this is a complex type, generate its message too if (IsComplexType(prop.Type)) { GenerateComplexTypeMessage(prop.Type as INamedTypeSymbol); } fieldNumber++; } _messagesBuilder.AppendLine("}"); _messagesBuilder.AppendLine(); } private void GenerateResponseMessage(INamedTypeSymbol type) { var messageName = $"{type.Name}Response"; if (_generatedMessages.Contains(messageName)) return; _generatedMessages.Add(messageName); _messagesBuilder.AppendLine($"// Response message for {type.Name}"); _messagesBuilder.AppendLine($"message {messageName} {{"); // Determine the result type from ICommandHandler or IQueryHandler var resultType = GetResultType(type); if (resultType != null) { var protoType = ProtoFileTypeMapper.MapType(resultType, out var needsImport, out var importPath); if (needsImport && importPath != null) { _requiredImports.Add(importPath); } _messagesBuilder.AppendLine($" {protoType} result = 1;"); } // If no result type, leave message empty (void return) _messagesBuilder.AppendLine("}"); _messagesBuilder.AppendLine(); // Generate complex type message after closing the response message if (resultType != null && IsComplexType(resultType)) { GenerateComplexTypeMessage(resultType as INamedTypeSymbol); } } private void GenerateComplexTypeMessage(INamedTypeSymbol? type) { if (type == null || _generatedMessages.Contains(type.Name)) return; // Don't generate messages for system types or primitives if (type.ContainingNamespace?.ToString().StartsWith("System") == true) return; _generatedMessages.Add(type.Name); _messagesBuilder.AppendLine($"// {type.Name} entity"); _messagesBuilder.AppendLine($"message {type.Name} {{"); var properties = type.GetMembers() .OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public) .ToList(); int fieldNumber = 1; foreach (var prop in properties) { if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type)) { _messagesBuilder.AppendLine($" // Skipped: {prop.Name} - unsupported type {prop.Type.Name}"); continue; } var protoType = ProtoFileTypeMapper.MapType(prop.Type, out var needsImport, out var importPath); if (needsImport && importPath != null) { _requiredImports.Add(importPath); } var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name); _messagesBuilder.AppendLine($" {protoType} {fieldName} = {fieldNumber};"); // Recursively generate nested complex types if (IsComplexType(prop.Type)) { GenerateComplexTypeMessage(prop.Type as INamedTypeSymbol); } fieldNumber++; } _messagesBuilder.AppendLine("}"); _messagesBuilder.AppendLine(); } private ITypeSymbol? GetResultType(INamedTypeSymbol commandOrQueryType) { // Scan for handler classes that implement ICommandHandler or IQueryHandler var handlerInterfaceName = commandOrQueryType.Name.EndsWith("Command") ? "ICommandHandler" : "IQueryHandler"; // Find all types in the compilation var allTypes = _compilation.GetSymbolsWithName(_ => true, SymbolFilter.Type) .OfType(); foreach (var type in allTypes) { // Check if this type implements the handler interface foreach (var @interface in type.AllInterfaces) { if (@interface.Name == handlerInterfaceName && @interface.TypeArguments.Length >= 1) { // Check if the first type argument matches our command/query var firstArg = @interface.TypeArguments[0]; if (SymbolEqualityComparer.Default.Equals(firstArg, commandOrQueryType)) { // Found the handler! Return the result type (second type argument) if it exists if (@interface.TypeArguments.Length == 2) { return @interface.TypeArguments[1]; } // If only one type argument, it's a void command (ICommandHandler) return null; } } } } return null; // No handler found } private bool IsComplexType(ITypeSymbol type) { // Check if it's a user-defined class/struct (not a primitive or system type) if (type.TypeKind != TypeKind.Class && type.TypeKind != TypeKind.Struct) return false; var fullName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); return !fullName.Contains("System."); } private string GetXmlDocSummary(INamedTypeSymbol type) { var xml = type.GetDocumentationCommentXml(); if (string.IsNullOrEmpty(xml)) return $"{type.Name} operation"; // Simple extraction - could be enhanced // xml is guaranteed non-null after IsNullOrEmpty check above var summaryStart = xml!.IndexOf(""); var summaryEnd = xml.IndexOf(""); if (summaryStart >= 0 && summaryEnd > summaryStart) { var summary = xml.Substring(summaryStart + 9, summaryEnd - summaryStart - 9).Trim(); return summary; } return $"{type.Name} operation"; } private List DiscoverDynamicQueries() { // Find IQueryableProvider implementations var queryableProviderInterface = _compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IQueryableProvider`1"); if (queryableProviderInterface == null) return new List(); var dynamicQueryTypes = new List(); var allTypes = _compilation.GetSymbolsWithName(_ => true, SymbolFilter.Type) .OfType(); foreach (var type in allTypes) { if (type.IsAbstract || type.IsStatic) continue; foreach (var iface in type.AllInterfaces) { if (iface.IsGenericType && SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryableProviderInterface)) { // Extract the entity type from IQueryableProvider var entityType = iface.TypeArguments[0] as INamedTypeSymbol; if (entityType != null && !dynamicQueryTypes.Contains(entityType, SymbolEqualityComparer.Default)) { dynamicQueryTypes.Add(entityType); } } } } return dynamicQueryTypes; } private void GenerateDynamicQueryMessages(INamedTypeSymbol entityType) { var pluralName = Pluralize(entityType.Name); var requestMessageName = $"DynamicQuery{pluralName}Request"; var responseMessageName = $"DynamicQuery{pluralName}Response"; // Common filter/sort/group/aggregate types (only generate once) if (!_generatedMessages.Contains("DynamicQueryFilter")) { _generatedMessages.Add("DynamicQueryFilter"); _messagesBuilder.AppendLine("// Dynamic query filter with AND/OR support"); _messagesBuilder.AppendLine("message DynamicQueryFilter {"); _messagesBuilder.AppendLine(" string path = 1;"); _messagesBuilder.AppendLine(" int32 type = 2; // PoweredSoft.DynamicQuery.Core.FilterType"); _messagesBuilder.AppendLine(" string value = 3;"); _messagesBuilder.AppendLine(" repeated DynamicQueryFilter and = 4;"); _messagesBuilder.AppendLine(" repeated DynamicQueryFilter or = 5;"); _messagesBuilder.AppendLine("}"); _messagesBuilder.AppendLine(); _messagesBuilder.AppendLine("// Dynamic query sort"); _messagesBuilder.AppendLine("message DynamicQuerySort {"); _messagesBuilder.AppendLine(" string path = 1;"); _messagesBuilder.AppendLine(" bool ascending = 2;"); _messagesBuilder.AppendLine("}"); _messagesBuilder.AppendLine(); _messagesBuilder.AppendLine("// Dynamic query group"); _messagesBuilder.AppendLine("message DynamicQueryGroup {"); _messagesBuilder.AppendLine(" string path = 1;"); _messagesBuilder.AppendLine("}"); _messagesBuilder.AppendLine(); _messagesBuilder.AppendLine("// Dynamic query aggregate"); _messagesBuilder.AppendLine("message DynamicQueryAggregate {"); _messagesBuilder.AppendLine(" string path = 1;"); _messagesBuilder.AppendLine(" int32 type = 2; // PoweredSoft.DynamicQuery.Core.AggregateType"); _messagesBuilder.AppendLine("}"); _messagesBuilder.AppendLine(); } // Request message _messagesBuilder.AppendLine($"// Dynamic query request for {entityType.Name}"); _messagesBuilder.AppendLine($"message {requestMessageName} {{"); _messagesBuilder.AppendLine(" int32 page = 1;"); _messagesBuilder.AppendLine(" int32 page_size = 2;"); _messagesBuilder.AppendLine(" repeated DynamicQueryFilter filters = 3;"); _messagesBuilder.AppendLine(" repeated DynamicQuerySort sorts = 4;"); _messagesBuilder.AppendLine(" repeated DynamicQueryGroup groups = 5;"); _messagesBuilder.AppendLine(" repeated DynamicQueryAggregate aggregates = 6;"); _messagesBuilder.AppendLine("}"); _messagesBuilder.AppendLine(); // Response message _messagesBuilder.AppendLine($"// Dynamic query response for {entityType.Name}"); _messagesBuilder.AppendLine($"message {responseMessageName} {{"); _messagesBuilder.AppendLine($" repeated {entityType.Name} data = 1;"); _messagesBuilder.AppendLine(" int64 total_records = 2;"); _messagesBuilder.AppendLine(" int32 number_of_pages = 3;"); _messagesBuilder.AppendLine("}"); _messagesBuilder.AppendLine(); // Generate entity message if not already generated GenerateComplexTypeMessage(entityType); } private static string Pluralize(string word) { if (word.EndsWith("y") && word.Length > 1 && !"aeiou".Contains(word[word.Length - 2].ToString())) return word.Substring(0, word.Length - 1) + "ies"; if (word.EndsWith("s") || word.EndsWith("x") || word.EndsWith("z") || word.EndsWith("ch") || word.EndsWith("sh")) return word + "es"; return word + "s"; } }