using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Svrnty.CQRS.Grpc.Generators.Helpers; using Svrnty.CQRS.Grpc.Generators.Models; using System.Collections.Generic; using System.Linq; using System.Text; namespace Svrnty.CQRS.Grpc.Generators { [Generator] public class GrpcGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { // Find all types that might be commands or queries var typeDeclarations = context.SyntaxProvider .CreateSyntaxProvider( predicate: static (node, _) => node is TypeDeclarationSyntax, transform: static (ctx, _) => GetTypeSymbol(ctx)) .Where(static symbol => symbol is not null); // Combine with compilation var compilationAndTypes = context.CompilationProvider.Combine(typeDeclarations.Collect()); // Register source output context.RegisterSourceOutput(compilationAndTypes, static (spc, source) => Execute(source.Left, source.Right!, spc)); } private static INamedTypeSymbol? GetTypeSymbol(GeneratorSyntaxContext context) { var typeDeclaration = (TypeDeclarationSyntax)context.Node; var symbol = context.SemanticModel.GetDeclaredSymbol(typeDeclaration); return symbol as INamedTypeSymbol; } private static void Execute(Compilation compilation, IEnumerable types, SourceProductionContext context) { var grpcIgnoreAttribute = compilation.GetTypeByMetadataName("Svrnty.CQRS.Grpc.Abstractions.Attributes.GrpcIgnoreAttribute"); var commandHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`1"); var commandHandlerWithResultInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`2"); var queryHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.IQueryHandler`2"); var dynamicQueryInterface2 = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IDynamicQuery`2"); var dynamicQueryInterface3 = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IDynamicQuery`3"); var queryableProviderInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IQueryableProvider`1"); if (commandHandlerInterface == null || queryHandlerInterface == null) { return; // Handler interfaces not found } var commandMap = new Dictionary(SymbolEqualityComparer.Default); // Command -> Result type (null if no result) var queryMap = new Dictionary(SymbolEqualityComparer.Default); // Query -> Result type var dynamicQueryMap = new List<(INamedTypeSymbol SourceType, INamedTypeSymbol DestinationType, INamedTypeSymbol? ParamsType)>(); // List of (Source, Destination, Params?) // Find all command and query types by looking at handler implementations foreach (var typeSymbol in types) { if (typeSymbol == null || typeSymbol.IsAbstract || typeSymbol.IsStatic) continue; // Check if this type implements ICommandHandler or ICommandHandler foreach (var iface in typeSymbol.AllInterfaces) { if (iface.IsGenericType) { // Check for ICommandHandler if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerInterface) && iface.TypeArguments.Length == 1) { var commandType = iface.TypeArguments[0] as INamedTypeSymbol; if (commandType != null && !commandMap.ContainsKey(commandType)) commandMap[commandType] = null; // No result type } // Check for ICommandHandler else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerWithResultInterface) && iface.TypeArguments.Length == 2) { var commandType = iface.TypeArguments[0] as INamedTypeSymbol; var resultType = iface.TypeArguments[1] as INamedTypeSymbol; if (commandType != null && resultType != null) commandMap[commandType] = resultType; } // Check for IQueryHandler else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryHandlerInterface) && iface.TypeArguments.Length == 2) { var queryType = iface.TypeArguments[0] as INamedTypeSymbol; var resultType = iface.TypeArguments[1] as INamedTypeSymbol; if (queryType != null && resultType != null) { // Check if this is a dynamic query handler if (queryType.IsGenericType && (SymbolEqualityComparer.Default.Equals(queryType.OriginalDefinition, dynamicQueryInterface2) || SymbolEqualityComparer.Default.Equals(queryType.OriginalDefinition, dynamicQueryInterface3))) { // Extract source, destination, and optional params types var sourceType = queryType.TypeArguments[0] as INamedTypeSymbol; var destinationType = queryType.TypeArguments[1] as INamedTypeSymbol; INamedTypeSymbol? paramsType = null; if (queryType.TypeArguments.Length == 3) { paramsType = queryType.TypeArguments[2] as INamedTypeSymbol; } if (sourceType != null && destinationType != null) { // Check if already added (avoid duplicates) var exists = dynamicQueryMap.Any(dq => SymbolEqualityComparer.Default.Equals(dq.SourceType, sourceType) && SymbolEqualityComparer.Default.Equals(dq.DestinationType, destinationType) && (dq.ParamsType == null && paramsType == null || dq.ParamsType != null && paramsType != null && SymbolEqualityComparer.Default.Equals(dq.ParamsType, paramsType))); if (!exists) { dynamicQueryMap.Add((sourceType, destinationType, paramsType)); } } } else { queryMap[queryType] = resultType; } } } } } // Check if this type implements IQueryableProvider - this indicates a dynamic query if (queryableProviderInterface != null) { foreach (var iface in typeSymbol.AllInterfaces) { if (iface.IsGenericType && SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryableProviderInterface)) { // Extract source type from IQueryableProvider var sourceType = iface.TypeArguments[0] as INamedTypeSymbol; if (sourceType != null) { // For IQueryableProvider, we assume TSource = TDestination (no params) var exists = dynamicQueryMap.Any(dq => SymbolEqualityComparer.Default.Equals(dq.SourceType, sourceType) && SymbolEqualityComparer.Default.Equals(dq.DestinationType, sourceType) && dq.ParamsType == null); if (!exists) { dynamicQueryMap.Add((sourceType, sourceType, null)); } } } } } } var commands = new List(); var queries = new List(); // Process discovered command types foreach (var kvp in commandMap) { var commandType = kvp.Key; var resultType = kvp.Value; // Skip if marked with [GrpcIgnore] if (grpcIgnoreAttribute != null && HasAttribute(commandType, grpcIgnoreAttribute)) continue; var commandInfo = ExtractCommandInfo(commandType, resultType); if (commandInfo != null) commands.Add(commandInfo); } // Process discovered query types foreach (var kvp in queryMap) { var queryType = kvp.Key; var resultType = kvp.Value; // Skip if marked with [GrpcIgnore] if (grpcIgnoreAttribute != null && HasAttribute(queryType, grpcIgnoreAttribute)) continue; var queryInfo = ExtractQueryInfo(queryType, resultType); if (queryInfo != null) queries.Add(queryInfo); } // Process discovered dynamic query types var dynamicQueries = new List(); foreach (var (sourceType, destinationType, paramsType) in dynamicQueryMap) { var dynamicQueryInfo = ExtractDynamicQueryInfo(sourceType, destinationType, paramsType); if (dynamicQueryInfo != null) dynamicQueries.Add(dynamicQueryInfo); } // Generate services if we found any commands, queries, or dynamic queries if (commands.Any() || queries.Any() || dynamicQueries.Any()) { GenerateProtoAndServices(context, commands, queries, dynamicQueries, compilation); } } private static bool HasAttribute(INamedTypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol) { return typeSymbol.GetAttributes().Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeSymbol)); } private static bool ImplementsInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? interfaceSymbol) { if (interfaceSymbol == null) return false; return typeSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, interfaceSymbol)); } private static bool ImplementsGenericInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? genericInterfaceSymbol) { if (genericInterfaceSymbol == null) return false; return typeSymbol.AllInterfaces.Any(i => i.IsGenericType && SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, genericInterfaceSymbol)); } private static CommandInfo? ExtractCommandInfo(INamedTypeSymbol commandType, INamedTypeSymbol? resultType) { var commandInfo = new CommandInfo { Name = commandType.Name, FullyQualifiedName = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), Namespace = commandType.ContainingNamespace.ToDisplayString(), Properties = new List() }; // Set result type if provided if (resultType != null) { commandInfo.ResultType = resultType.Name; commandInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); // Use fully qualified names to avoid ambiguity with proto-generated types var commandTypeFullyQualified = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var resultTypeFullyQualified = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandTypeFullyQualified}, {resultTypeFullyQualified}>"; } else { var commandTypeFullyQualified = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandTypeFullyQualified}>"; } // Extract properties var properties = commandType.GetMembers().OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) .ToList(); int fieldNumber = 1; foreach (var property in properties) { var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional); commandInfo.Properties.Add(new PropertyInfo { Name = property.Name, Type = propertyType, ProtoType = protoType, FieldNumber = fieldNumber++ }); } return commandInfo; } private static QueryInfo? ExtractQueryInfo(INamedTypeSymbol queryType, INamedTypeSymbol resultType) { var queryInfo = new QueryInfo { Name = queryType.Name, FullyQualifiedName = queryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), Namespace = queryType.ContainingNamespace.ToDisplayString(), Properties = new List() }; // Set result type queryInfo.ResultType = resultType.Name; queryInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); // Use fully qualified names to avoid ambiguity with proto-generated types var queryTypeFullyQualified = queryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var resultTypeFullyQualified = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); queryInfo.HandlerInterfaceName = $"IQueryHandler<{queryTypeFullyQualified}, {resultTypeFullyQualified}>"; // Check if result type is primitive var resultTypeString = resultType.ToDisplayString(); queryInfo.IsResultPrimitiveType = IsPrimitiveType(resultTypeString); // Extract result type properties if it's a complex type if (!queryInfo.IsResultPrimitiveType) { var resultProperties = resultType.GetMembers().OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) .ToList(); foreach (var property in resultProperties) { queryInfo.ResultProperties.Add(new PropertyInfo { Name = property.Name, Type = property.Type.ToDisplayString(), FullyQualifiedType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), ProtoType = string.Empty, // Not needed for result mapping FieldNumber = 0 // Not needed for result mapping }); } } // Extract properties var properties = queryType.GetMembers().OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) .ToList(); int fieldNumber = 1; foreach (var property in properties) { var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional); queryInfo.Properties.Add(new PropertyInfo { Name = property.Name, Type = propertyType, ProtoType = protoType, FieldNumber = fieldNumber++ }); } return queryInfo; } private static DynamicQueryInfo? ExtractDynamicQueryInfo(INamedTypeSymbol sourceType, INamedTypeSymbol destinationType, INamedTypeSymbol? paramsType) { var dynamicQueryInfo = new DynamicQueryInfo { SourceType = sourceType.Name, SourceTypeFullyQualified = sourceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), DestinationType = destinationType.Name, DestinationTypeFullyQualified = destinationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), HasParams = paramsType != null }; if (paramsType != null) { dynamicQueryInfo.ParamsType = paramsType.Name; dynamicQueryInfo.ParamsTypeFullyQualified = paramsType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); } // Pluralize destination type for naming (e.g., User -> Users) dynamicQueryInfo.Name = Pluralize(destinationType.Name); // Build interface names if (paramsType != null) { dynamicQueryInfo.QueryInterfaceName = $"IDynamicQuery<{dynamicQueryInfo.SourceTypeFullyQualified}, {dynamicQueryInfo.DestinationTypeFullyQualified}, {dynamicQueryInfo.ParamsTypeFullyQualified}>"; dynamicQueryInfo.HandlerInterfaceName = $"IQueryHandler<{dynamicQueryInfo.QueryInterfaceName}, IQueryExecutionResult<{dynamicQueryInfo.DestinationTypeFullyQualified}>>"; } else { dynamicQueryInfo.QueryInterfaceName = $"IDynamicQuery<{dynamicQueryInfo.SourceTypeFullyQualified}, {dynamicQueryInfo.DestinationTypeFullyQualified}>"; dynamicQueryInfo.HandlerInterfaceName = $"IQueryHandler<{dynamicQueryInfo.QueryInterfaceName}, IQueryExecutionResult<{dynamicQueryInfo.DestinationTypeFullyQualified}>>"; } return dynamicQueryInfo; } private static string Pluralize(string word) { // Simple pluralization logic - can be enhanced with Pluralize.NET if needed if (word.EndsWith("y") && word.Length > 1 && !"aeiou".Contains(word[word.Length - 2])) 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"; } private static void GenerateProtoAndServices(SourceProductionContext context, List commands, List queries, List dynamicQueries, Compilation compilation) { // Get root namespace from compilation var rootNamespace = compilation.AssemblyName ?? "Application"; // Generate service implementations for commands if (commands.Any()) { var commandService = GenerateCommandServiceImpl(commands, rootNamespace); context.AddSource("CommandServiceImpl.g.cs", commandService); } // Generate service implementations for queries if (queries.Any()) { var queryService = GenerateQueryServiceImpl(queries, rootNamespace); context.AddSource("QueryServiceImpl.g.cs", queryService); } // Generate service implementations for dynamic queries if (dynamicQueries.Any()) { var dynamicQueryService = GenerateDynamicQueryServiceImpl(dynamicQueries, rootNamespace); context.AddSource("DynamicQueryServiceImpl.g.cs", dynamicQueryService); } // Generate registration extensions var registrationExtensions = GenerateRegistrationExtensions(commands.Any(), queries.Any(), dynamicQueries.Any(), rootNamespace); context.AddSource("GrpcServiceRegistration.g.cs", registrationExtensions); } private static string GenerateCommandMessages(List commands, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using System.Runtime.Serialization;"); sb.AppendLine("using ProtoBuf;"); sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages"); sb.AppendLine("{"); foreach (var command in commands) { // Generate command DTO sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine($" public sealed class {command.Name}Dto"); sb.AppendLine(" {"); foreach (var prop in command.Properties) { sb.AppendLine($" [ProtoMember({prop.FieldNumber})]"); sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]"); sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine(); // Generate result DTO if command has a result if (command.HasResult) { sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine($" public sealed class {command.Name}ResultDto"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine($" public {command.ResultFullyQualifiedName} Result {{ get; set; }}"); sb.AppendLine(" }"); sb.AppendLine(); } } sb.AppendLine("}"); return sb.ToString(); } private static string GenerateQueryMessages(List queries, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using System.Runtime.Serialization;"); sb.AppendLine("using ProtoBuf;"); sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages"); sb.AppendLine("{"); foreach (var query in queries) { // Generate query DTO sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine($" public sealed class {query.Name}Dto"); sb.AppendLine(" {"); foreach (var prop in query.Properties) { sb.AppendLine($" [ProtoMember({prop.FieldNumber})]"); sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]"); sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine(); // Generate result DTO sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine($" public sealed class {query.Name}ResultDto"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine($" public {query.ResultFullyQualifiedName} Result {{ get; set; }}"); sb.AppendLine(" }"); sb.AppendLine(); } sb.AppendLine("}"); return sb.ToString(); } private static string GenerateCommandService(List commands, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using System.ServiceModel;"); sb.AppendLine("using System.Threading;"); sb.AppendLine("using System.Threading.Tasks;"); sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); sb.AppendLine($"using {rootNamespace}.Grpc.Messages;"); sb.AppendLine("using Svrnty.CQRS.Abstractions;"); sb.AppendLine("using ProtoBuf.Grpc;"); sb.AppendLine(); // Generate service interface sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); sb.AppendLine("{"); sb.AppendLine(" [ServiceContract]"); sb.AppendLine(" public interface ICommandService"); sb.AppendLine(" {"); foreach (var command in commands) { if (command.HasResult) { sb.AppendLine($" [OperationContract]"); sb.AppendLine($" Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);"); } else { sb.AppendLine($" [OperationContract]"); sb.AppendLine($" Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);"); } sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine(); // Generate service implementation sb.AppendLine(" public sealed class CommandService : ICommandService"); sb.AppendLine(" {"); sb.AppendLine(" private readonly IServiceProvider _serviceProvider;"); sb.AppendLine(); sb.AppendLine(" public CommandService(IServiceProvider serviceProvider)"); sb.AppendLine(" {"); sb.AppendLine(" _serviceProvider = serviceProvider;"); sb.AppendLine(" }"); sb.AppendLine(); foreach (var command in commands) { if (command.HasResult) { sb.AppendLine($" public async Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)"); sb.AppendLine(" {"); sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();"); sb.AppendLine($" var command = new {command.FullyQualifiedName}"); sb.AppendLine(" {"); foreach (var prop in command.Properties) { sb.AppendLine($" {prop.Name} = request.{prop.Name}!,"); } sb.AppendLine(" };"); sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);"); sb.AppendLine($" return new {command.Name}ResultDto {{ Result = result }};"); sb.AppendLine(" }"); } else { sb.AppendLine($" public async Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)"); sb.AppendLine(" {"); sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();"); sb.AppendLine($" var command = new {command.FullyQualifiedName}"); sb.AppendLine(" {"); foreach (var prop in command.Properties) { sb.AppendLine($" {prop.Name} = request.{prop.Name}!,"); } sb.AppendLine(" };"); sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);"); sb.AppendLine(" }"); } sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static string GenerateQueryService(List queries, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using System.ServiceModel;"); sb.AppendLine("using System.Threading;"); sb.AppendLine("using System.Threading.Tasks;"); sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); sb.AppendLine($"using {rootNamespace}.Grpc.Messages;"); sb.AppendLine("using Svrnty.CQRS.Abstractions;"); sb.AppendLine("using ProtoBuf.Grpc;"); sb.AppendLine(); // Generate service interface sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); sb.AppendLine("{"); sb.AppendLine(" [ServiceContract]"); sb.AppendLine(" public interface IQueryService"); sb.AppendLine(" {"); foreach (var query in queries) { sb.AppendLine($" [OperationContract]"); sb.AppendLine($" Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default);"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine(); // Generate service implementation sb.AppendLine(" public sealed class QueryService : IQueryService"); sb.AppendLine(" {"); sb.AppendLine(" private readonly IServiceProvider _serviceProvider;"); sb.AppendLine(); sb.AppendLine(" public QueryService(IServiceProvider serviceProvider)"); sb.AppendLine(" {"); sb.AppendLine(" _serviceProvider = serviceProvider;"); sb.AppendLine(" }"); sb.AppendLine(); foreach (var query in queries) { sb.AppendLine($" public async Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default)"); sb.AppendLine(" {"); sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();"); sb.AppendLine($" var query = new {query.FullyQualifiedName}"); sb.AppendLine(" {"); foreach (var prop in query.Properties) { sb.AppendLine($" {prop.Name} = request.{prop.Name}!,"); } sb.AppendLine(" };"); sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); sb.AppendLine($" return new {query.Name}ResultDto {{ Result = result }};"); sb.AppendLine(" }"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static string GenerateRegistrationExtensions(bool hasCommands, bool hasQueries, bool hasDynamicQueries, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("using Microsoft.AspNetCore.Builder;"); sb.AppendLine("using Microsoft.AspNetCore.Routing;"); sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); sb.AppendLine($"using {rootNamespace}.Grpc.Services;"); sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.Extensions"); sb.AppendLine("{"); sb.AppendLine(" /// "); sb.AppendLine(" /// Auto-generated extension methods for registering and mapping gRPC services"); sb.AppendLine(" /// "); sb.AppendLine(" public static class GrpcServiceRegistrationExtensions"); sb.AppendLine(" {"); if (hasCommands) { sb.AppendLine(" /// "); sb.AppendLine(" /// Registers the auto-generated Command gRPC service"); sb.AppendLine(" /// "); sb.AppendLine(" public static IServiceCollection AddGrpcCommandService(this IServiceCollection services)"); sb.AppendLine(" {"); sb.AppendLine(" services.AddGrpc();"); sb.AppendLine(" services.AddSingleton();"); sb.AppendLine(" return services;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Maps the auto-generated Command gRPC service endpoints"); sb.AppendLine(" /// "); sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommands(this IEndpointRouteBuilder endpoints)"); sb.AppendLine(" {"); sb.AppendLine(" endpoints.MapGrpcService();"); sb.AppendLine(" return endpoints;"); sb.AppendLine(" }"); sb.AppendLine(); } if (hasQueries) { sb.AppendLine(" /// "); sb.AppendLine(" /// Registers the auto-generated Query gRPC service"); sb.AppendLine(" /// "); sb.AppendLine(" public static IServiceCollection AddGrpcQueryService(this IServiceCollection services)"); sb.AppendLine(" {"); sb.AppendLine(" services.AddGrpc();"); sb.AppendLine(" services.AddSingleton();"); sb.AppendLine(" return services;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Maps the auto-generated Query gRPC service endpoints"); sb.AppendLine(" /// "); sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcQueries(this IEndpointRouteBuilder endpoints)"); sb.AppendLine(" {"); sb.AppendLine(" endpoints.MapGrpcService();"); sb.AppendLine(" return endpoints;"); sb.AppendLine(" }"); sb.AppendLine(); } if (hasDynamicQueries) { sb.AppendLine(" /// "); sb.AppendLine(" /// Registers the auto-generated DynamicQuery gRPC service"); sb.AppendLine(" /// "); sb.AppendLine(" public static IServiceCollection AddGrpcDynamicQueryService(this IServiceCollection services)"); sb.AppendLine(" {"); sb.AppendLine(" services.AddGrpc();"); sb.AppendLine(" services.AddSingleton();"); sb.AppendLine(" return services;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Maps the auto-generated DynamicQuery gRPC service endpoints"); sb.AppendLine(" /// "); sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcDynamicQueries(this IEndpointRouteBuilder endpoints)"); sb.AppendLine(" {"); sb.AppendLine(" endpoints.MapGrpcService();"); sb.AppendLine(" return endpoints;"); sb.AppendLine(" }"); sb.AppendLine(); } if (hasCommands || hasQueries || hasDynamicQueries) { sb.AppendLine(" /// "); sb.AppendLine(" /// Registers all auto-generated gRPC services (Commands, Queries, and DynamicQueries)"); sb.AppendLine(" /// "); sb.AppendLine(" public static IServiceCollection AddGrpcCommandsAndQueries(this IServiceCollection services)"); sb.AppendLine(" {"); sb.AppendLine(" services.AddGrpc();"); sb.AppendLine(" services.AddGrpcReflection();"); if (hasCommands) sb.AppendLine(" services.AddSingleton();"); if (hasQueries) sb.AppendLine(" services.AddSingleton();"); if (hasDynamicQueries) sb.AppendLine(" services.AddSingleton();"); sb.AppendLine(" return services;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Maps all auto-generated gRPC service endpoints (Commands, Queries, and DynamicQueries)"); sb.AppendLine(" /// "); sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommandsAndQueries(this IEndpointRouteBuilder endpoints)"); sb.AppendLine(" {"); if (hasCommands) sb.AppendLine(" endpoints.MapGrpcService();"); if (hasQueries) sb.AppendLine(" endpoints.MapGrpcService();"); if (hasDynamicQueries) sb.AppendLine(" endpoints.MapGrpcService();"); sb.AppendLine(" return endpoints;"); sb.AppendLine(" }"); sb.AppendLine(); // Add configuration-based methods sb.AppendLine(" /// "); sb.AppendLine(" /// Registers gRPC services based on configuration"); sb.AppendLine(" /// "); sb.AppendLine(" public static IServiceCollection AddGrpcFromConfiguration(this IServiceCollection services)"); sb.AppendLine(" {"); sb.AppendLine(" var config = services.BuildServiceProvider().GetService();"); sb.AppendLine(" var grpcOptions = config?.GetConfiguration();"); sb.AppendLine(" if (grpcOptions != null)"); sb.AppendLine(" {"); sb.AppendLine(" services.AddGrpc();"); sb.AppendLine(" if (grpcOptions.ShouldEnableReflection)"); sb.AppendLine(" services.AddGrpcReflection();"); sb.AppendLine(); if (hasCommands) { sb.AppendLine(" if (grpcOptions.GetShouldMapCommands())"); sb.AppendLine(" services.AddSingleton();"); } if (hasQueries) { sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); sb.AppendLine(" services.AddSingleton();"); } if (hasDynamicQueries) { sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); sb.AppendLine(" services.AddSingleton();"); } sb.AppendLine(" }"); sb.AppendLine(" return services;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Maps gRPC service endpoints based on configuration"); sb.AppendLine(" /// "); sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcFromConfiguration(this IEndpointRouteBuilder endpoints)"); sb.AppendLine(" {"); sb.AppendLine(" var config = endpoints.ServiceProvider.GetService();"); sb.AppendLine(" var grpcOptions = config?.GetConfiguration();"); sb.AppendLine(" if (grpcOptions != null)"); sb.AppendLine(" {"); if (hasCommands) { sb.AppendLine(" if (grpcOptions.GetShouldMapCommands())"); sb.AppendLine(" endpoints.MapGrpcService();"); } if (hasQueries) { sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); sb.AppendLine(" endpoints.MapGrpcService();"); } if (hasDynamicQueries) { sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); sb.AppendLine(" endpoints.MapGrpcService();"); } sb.AppendLine(); sb.AppendLine(" if (grpcOptions.ShouldEnableReflection)"); sb.AppendLine(" endpoints.MapGrpcReflectionService();"); sb.AppendLine(" }"); sb.AppendLine(" return endpoints;"); sb.AppendLine(" }"); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static string ToCamelCase(string str) { if (string.IsNullOrEmpty(str) || char.IsLower(str[0])) return str; return char.ToLowerInvariant(str[0]) + str.Substring(1); } // New methods for standard gRPC generation private static string GenerateCommandsProto(List commands, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("syntax = \"proto3\";"); sb.AppendLine(); sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";"); sb.AppendLine(); sb.AppendLine("package cqrs;"); sb.AppendLine(); sb.AppendLine("// Command service for CQRS operations"); sb.AppendLine("service CommandService {"); foreach (var command in commands) { var methodName = command.Name.Replace("Command", ""); sb.AppendLine($" // {command.Name}"); sb.AppendLine($" rpc {methodName} ({command.Name}Request) returns ({command.Name}Response);"); } sb.AppendLine("}"); sb.AppendLine(); // Generate message types foreach (var command in commands) { // Request message sb.AppendLine($"message {command.Name}Request {{"); foreach (var prop in command.Properties) { sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};"); } sb.AppendLine("}"); sb.AppendLine(); // Response message sb.AppendLine($"message {command.Name}Response {{"); if (command.HasResult) { sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(command.ResultFullyQualifiedName!, out _, out _)} result = 1;"); } sb.AppendLine("}"); sb.AppendLine(); } return sb.ToString(); } private static string GenerateQueriesProto(List queries, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("syntax = \"proto3\";"); sb.AppendLine(); sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";"); sb.AppendLine(); sb.AppendLine("package cqrs;"); sb.AppendLine(); sb.AppendLine("// Query service for CQRS operations"); sb.AppendLine("service QueryService {"); foreach (var query in queries) { var methodName = query.Name.Replace("Query", ""); sb.AppendLine($" // {query.Name}"); sb.AppendLine($" rpc {methodName} ({query.Name}Request) returns ({query.Name}Response);"); } sb.AppendLine("}"); sb.AppendLine(); // Generate message types foreach (var query in queries) { // Request message sb.AppendLine($"message {query.Name}Request {{"); foreach (var prop in query.Properties) { sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};"); } sb.AppendLine("}"); sb.AppendLine(); // Response message sb.AppendLine($"message {query.Name}Response {{"); sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(query.ResultFullyQualifiedName, out _, out _)} result = 1;"); sb.AppendLine("}"); sb.AppendLine(); } return sb.ToString(); } private static string GenerateCommandServiceImpl(List commands, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using Grpc.Core;"); sb.AppendLine("using System.Threading.Tasks;"); sb.AppendLine("using System.Linq;"); sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); sb.AppendLine("using FluentValidation;"); sb.AppendLine("using Google.Rpc;"); sb.AppendLine("using Google.Protobuf.WellKnownTypes;"); sb.AppendLine($"using {rootNamespace}.Grpc;"); sb.AppendLine("using Svrnty.CQRS.Abstractions;"); sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); sb.AppendLine("{"); sb.AppendLine(" /// "); sb.AppendLine(" /// Auto-generated gRPC service implementation for Commands"); sb.AppendLine(" /// "); sb.AppendLine(" public sealed class CommandServiceImpl : CommandService.CommandServiceBase"); sb.AppendLine(" {"); sb.AppendLine(" private readonly IServiceProvider _serviceProvider;"); sb.AppendLine(); sb.AppendLine(" public CommandServiceImpl(IServiceProvider serviceProvider)"); sb.AppendLine(" {"); sb.AppendLine(" _serviceProvider = serviceProvider;"); sb.AppendLine(" }"); sb.AppendLine(); foreach (var command in commands) { var methodName = command.Name.Replace("Command", ""); var requestType = $"{command.Name}Request"; var responseType = $"{command.Name}Response"; sb.AppendLine($" public override async Task<{responseType}> {methodName}("); sb.AppendLine($" {requestType} request,"); sb.AppendLine(" ServerCallContext context)"); sb.AppendLine(" {"); sb.AppendLine($" var command = new {command.FullyQualifiedName}"); sb.AppendLine(" {"); foreach (var prop in command.Properties) { sb.AppendLine($" {prop.Name} = request.{char.ToUpper(prop.Name[0]) + prop.Name.Substring(1)},"); } sb.AppendLine(" };"); sb.AppendLine(); sb.AppendLine(" // Validate command if validator is registered"); sb.AppendLine($" var validator = _serviceProvider.GetService>();"); sb.AppendLine(" if (validator != null)"); sb.AppendLine(" {"); sb.AppendLine(" var validationResult = await validator.ValidateAsync(command, context.CancellationToken);"); sb.AppendLine(" if (!validationResult.IsValid)"); sb.AppendLine(" {"); sb.AppendLine(" // Create Rich Error Model with structured field violations"); sb.AppendLine(" var badRequest = new BadRequest();"); sb.AppendLine(" foreach (var error in validationResult.Errors)"); sb.AppendLine(" {"); sb.AppendLine(" badRequest.FieldViolations.Add(new BadRequest.Types.FieldViolation"); sb.AppendLine(" {"); sb.AppendLine(" Field = error.PropertyName,"); sb.AppendLine(" Description = error.ErrorMessage"); sb.AppendLine(" });"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" var status = new Google.Rpc.Status"); sb.AppendLine(" {"); sb.AppendLine(" Code = (int)Code.InvalidArgument,"); sb.AppendLine(" Message = \"Validation failed\","); sb.AppendLine(" Details = { Any.Pack(badRequest) }"); sb.AppendLine(" };"); sb.AppendLine(); sb.AppendLine(" throw status.ToRpcException();"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();"); if (command.HasResult) { sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);"); sb.AppendLine($" return new {responseType} {{ Result = result }};"); } else { sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);"); sb.AppendLine($" return new {responseType}();"); } sb.AppendLine(" }"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static string GenerateQueryServiceImpl(List queries, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using Grpc.Core;"); sb.AppendLine("using System.Threading.Tasks;"); sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); sb.AppendLine($"using {rootNamespace}.Grpc;"); sb.AppendLine("using Svrnty.CQRS.Abstractions;"); sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); sb.AppendLine("{"); sb.AppendLine(" /// "); sb.AppendLine(" /// Auto-generated gRPC service implementation for Queries"); sb.AppendLine(" /// "); sb.AppendLine(" public sealed class QueryServiceImpl : QueryService.QueryServiceBase"); sb.AppendLine(" {"); sb.AppendLine(" private readonly IServiceProvider _serviceProvider;"); sb.AppendLine(); sb.AppendLine(" public QueryServiceImpl(IServiceProvider serviceProvider)"); sb.AppendLine(" {"); sb.AppendLine(" _serviceProvider = serviceProvider;"); sb.AppendLine(" }"); sb.AppendLine(); foreach (var query in queries) { var methodName = query.Name.Replace("Query", ""); var requestType = $"{query.Name}Request"; var responseType = $"{query.Name}Response"; sb.AppendLine($" public override async Task<{responseType}> {methodName}("); sb.AppendLine($" {requestType} request,"); sb.AppendLine(" ServerCallContext context)"); sb.AppendLine(" {"); sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();"); sb.AppendLine($" var query = new {query.FullyQualifiedName}"); sb.AppendLine(" {"); foreach (var prop in query.Properties) { sb.AppendLine($" {prop.Name} = request.{char.ToUpper(prop.Name[0]) + prop.Name.Substring(1)},"); } sb.AppendLine(" };"); sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); // Generate response with mapping if complex type if (query.IsResultPrimitiveType) { sb.AppendLine($" return new {responseType} {{ Result = result }};"); } else { // Complex type - need to map from C# type to proto type sb.AppendLine($" return new {responseType}"); sb.AppendLine(" {"); sb.AppendLine($" Result = new {query.ResultType}"); sb.AppendLine(" {"); foreach (var prop in query.ResultProperties) { sb.AppendLine($" {prop.Name} = result.{prop.Name},"); } sb.AppendLine(" }"); sb.AppendLine(" };"); } sb.AppendLine(" }"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static bool IsPrimitiveType(string typeName) { // Check for common primitive and built-in types var primitiveTypes = new[] { "int", "System.Int32", "long", "System.Int64", "short", "System.Int16", "byte", "System.Byte", "bool", "System.Boolean", "float", "System.Single", "double", "System.Double", "decimal", "System.Decimal", "string", "System.String", "System.DateTime", "System.DateTimeOffset", "System.TimeSpan", "System.Guid" }; return primitiveTypes.Contains(typeName) || typeName.StartsWith("System.Nullable<") || typeName.EndsWith("?"); } private static string GenerateDynamicQueryMessages(List dynamicQueries, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using System.Collections.Generic;"); sb.AppendLine("using System.ServiceModel;"); sb.AppendLine("using System.Runtime.Serialization;"); sb.AppendLine("using ProtoBuf;"); sb.AppendLine("using ProtoBuf.Grpc;"); sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.DynamicQuery"); sb.AppendLine("{"); // Common message types sb.AppendLine(" /// "); sb.AppendLine(" /// Dynamic query filter with support for nested AND/OR logic"); sb.AppendLine(" /// "); sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine(" public sealed class DynamicQueryFilter"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine(" public string Path { get; set; } = string.Empty;"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(2)]"); sb.AppendLine(" [DataMember(Order = 2)]"); sb.AppendLine(" public int Type { get; set; } // Maps to PoweredSoft.DynamicQuery.Core.FilterType"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(3)]"); sb.AppendLine(" [DataMember(Order = 3)]"); sb.AppendLine(" public string Value { get; set; } = string.Empty;"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(4)]"); sb.AppendLine(" [DataMember(Order = 4)]"); sb.AppendLine(" public List? And { get; set; }"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(5)]"); sb.AppendLine(" [DataMember(Order = 5)]"); sb.AppendLine(" public List? Or { get; set; }"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Dynamic query sort"); sb.AppendLine(" /// "); sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine(" public sealed class DynamicQuerySort"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine(" public string Path { get; set; } = string.Empty;"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(2)]"); sb.AppendLine(" [DataMember(Order = 2)]"); sb.AppendLine(" public bool Ascending { get; set; } = true;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Dynamic query group"); sb.AppendLine(" /// "); sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine(" public sealed class DynamicQueryGroup"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine(" public string Path { get; set; } = string.Empty;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Dynamic query aggregate"); sb.AppendLine(" /// "); sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine(" public sealed class DynamicQueryAggregate"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine(" public string Path { get; set; } = string.Empty;"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(2)]"); sb.AppendLine(" [DataMember(Order = 2)]"); sb.AppendLine(" public int Type { get; set; } // Maps to PoweredSoft.DynamicQuery.Core.AggregateType"); sb.AppendLine(" }"); sb.AppendLine(); // Generate request/response messages for each dynamic query foreach (var dynamicQuery in dynamicQueries) { // Request message sb.AppendLine($" /// "); sb.AppendLine($" /// Request message for dynamic query on {dynamicQuery.Name}"); sb.AppendLine($" /// "); sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine($" public sealed class DynamicQuery{dynamicQuery.Name}Request"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine(" public int Page { get; set; }"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(2)]"); sb.AppendLine(" [DataMember(Order = 2)]"); sb.AppendLine(" public int PageSize { get; set; }"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(3)]"); sb.AppendLine(" [DataMember(Order = 3)]"); sb.AppendLine(" public List Filters { get; set; } = new();"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(4)]"); sb.AppendLine(" [DataMember(Order = 4)]"); sb.AppendLine(" public List Sorts { get; set; } = new();"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(5)]"); sb.AppendLine(" [DataMember(Order = 5)]"); sb.AppendLine(" public List Groups { get; set; } = new();"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(6)]"); sb.AppendLine(" [DataMember(Order = 6)]"); sb.AppendLine(" public List Aggregates { get; set; } = new();"); sb.AppendLine(" }"); sb.AppendLine(); // Response message sb.AppendLine($" /// "); sb.AppendLine($" /// Response message for dynamic query on {dynamicQuery.Name}"); sb.AppendLine($" /// "); sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine($" public sealed class DynamicQuery{dynamicQuery.Name}Response"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine($" public List<{dynamicQuery.DestinationTypeFullyQualified}> Data {{ get; set; }} = new();"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(2)]"); sb.AppendLine(" [DataMember(Order = 2)]"); sb.AppendLine(" public long TotalCount { get; set; }"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(3)]"); sb.AppendLine(" [DataMember(Order = 3)]"); sb.AppendLine(" public int NumberOfPages { get; set; }"); sb.AppendLine(" }"); sb.AppendLine(); } // Generate service interface sb.AppendLine(" /// "); sb.AppendLine(" /// gRPC service interface for DynamicQueries"); sb.AppendLine(" /// "); sb.AppendLine(" [ServiceContract]"); sb.AppendLine(" public interface IDynamicQueryService"); sb.AppendLine(" {"); foreach (var dynamicQuery in dynamicQueries) { var methodName = $"Query{dynamicQuery.Name}"; sb.AppendLine($" /// "); sb.AppendLine($" /// Execute dynamic query on {dynamicQuery.Name}"); sb.AppendLine($" /// "); sb.AppendLine(" [OperationContract]"); sb.AppendLine($" System.Threading.Tasks.Task {methodName}Async(DynamicQuery{dynamicQuery.Name}Request request, CallContext context = default);"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static string GenerateDynamicQueryServiceImpl(List dynamicQueries, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using Grpc.Core;"); sb.AppendLine("using System.Threading.Tasks;"); sb.AppendLine("using System.Collections.Generic;"); sb.AppendLine("using System.Linq;"); sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); sb.AppendLine($"using {rootNamespace}.Grpc;"); sb.AppendLine("using Svrnty.CQRS.Abstractions;"); sb.AppendLine("using Svrnty.CQRS.DynamicQuery.Abstractions;"); sb.AppendLine("using PoweredSoft.DynamicQuery.Core;"); sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); sb.AppendLine("{"); sb.AppendLine(" /// "); sb.AppendLine(" /// Auto-generated gRPC service implementation for DynamicQueries"); sb.AppendLine(" /// "); sb.AppendLine(" public sealed class DynamicQueryServiceImpl : DynamicQueryService.DynamicQueryServiceBase"); sb.AppendLine(" {"); sb.AppendLine(" private readonly IServiceProvider _serviceProvider;"); sb.AppendLine(); sb.AppendLine(" public DynamicQueryServiceImpl(IServiceProvider serviceProvider)"); sb.AppendLine(" {"); sb.AppendLine(" _serviceProvider = serviceProvider;"); sb.AppendLine(" }"); sb.AppendLine(); foreach (var dynamicQuery in dynamicQueries) { var methodName = $"Query{dynamicQuery.Name}"; var requestType = $"DynamicQuery{dynamicQuery.Name}Request"; var responseType = $"DynamicQuery{dynamicQuery.Name}Response"; sb.AppendLine($" public override async Task<{responseType}> {methodName}("); sb.AppendLine($" {requestType} request,"); sb.AppendLine(" ServerCallContext context)"); sb.AppendLine(" {"); // Build the dynamic query object if (dynamicQuery.HasParams) { sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}, {dynamicQuery.ParamsTypeFullyQualified}>"); } else { sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}>"); } sb.AppendLine(" {"); sb.AppendLine(" Page = request.Page > 0 ? request.Page : null,"); sb.AppendLine(" PageSize = request.PageSize > 0 ? request.PageSize : null,"); sb.AppendLine(" Filters = ConvertFilters(request.Filters),"); sb.AppendLine(" Sorts = ConvertSorts(request.Sorts),"); sb.AppendLine(" Groups = ConvertGroups(request.Groups),"); sb.AppendLine(" Aggregates = ConvertAggregates(request.Aggregates)"); sb.AppendLine(" };"); sb.AppendLine(); // Get the handler and execute sb.AppendLine($" var handler = _serviceProvider.GetRequiredService>>();"); sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); sb.AppendLine(); // Build response sb.AppendLine($" var response = new {responseType}"); sb.AppendLine(" {"); sb.AppendLine(" TotalRecords = result.TotalRecords,"); sb.AppendLine(" NumberOfPages = (int)(result.NumberOfPages ?? 0)"); sb.AppendLine(" };"); sb.AppendLine(); sb.AppendLine(" if (result.Data != null)"); sb.AppendLine(" {"); sb.AppendLine(" foreach (var item in result.Data)"); sb.AppendLine(" {"); sb.AppendLine($" response.Data.Add(MapTo{dynamicQuery.Name}ProtoModel(item));"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" // TODO: Add aggregates and groups to response if needed"); sb.AppendLine(); sb.AppendLine(" return response;"); sb.AppendLine(" }"); sb.AppendLine(); } // Add helper methods for converting proto messages to AspNetCore types sb.AppendLine(" private static List? ConvertFilters(Google.Protobuf.Collections.RepeatedField protoFilters)"); sb.AppendLine(" {"); sb.AppendLine(" if (protoFilters == null || protoFilters.Count == 0)"); sb.AppendLine(" return null;"); sb.AppendLine(); sb.AppendLine(" var filters = new List();"); sb.AppendLine(" foreach (var protoFilter in protoFilters)"); sb.AppendLine(" {"); sb.AppendLine(" var filter = new Svrnty.CQRS.DynamicQuery.DynamicQueryFilter"); sb.AppendLine(" {"); sb.AppendLine(" Path = protoFilter.Path,"); sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.FilterType)protoFilter.Type).ToString(),"); sb.AppendLine(" Value = protoFilter.Value"); sb.AppendLine(" };"); sb.AppendLine(); sb.AppendLine(" // Handle nested AND filters"); sb.AppendLine(" if (protoFilter.And != null && protoFilter.And.Count > 0)"); sb.AppendLine(" {"); sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(protoFilter.And);"); sb.AppendLine(" filter.And = true;"); sb.AppendLine(" }"); sb.AppendLine(" // Handle nested OR filters"); sb.AppendLine(" else if (protoFilter.Or != null && protoFilter.Or.Count > 0)"); sb.AppendLine(" {"); sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(protoFilter.Or);"); sb.AppendLine(" filter.And = false;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" filters.Add(filter);"); sb.AppendLine(" }"); sb.AppendLine(" return filters;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" private static List ConvertProtoFiltersToList(Google.Protobuf.Collections.RepeatedField protoFilters)"); sb.AppendLine(" {"); sb.AppendLine(" var result = new List();"); sb.AppendLine(" foreach (var pf in protoFilters)"); sb.AppendLine(" {"); sb.AppendLine(" var filter = new Svrnty.CQRS.DynamicQuery.DynamicQueryFilter"); sb.AppendLine(" {"); sb.AppendLine(" Path = pf.Path,"); sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.FilterType)pf.Type).ToString(),"); sb.AppendLine(" Value = pf.Value"); sb.AppendLine(" };"); sb.AppendLine(" if (pf.And != null && pf.And.Count > 0)"); sb.AppendLine(" {"); sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(pf.And);"); sb.AppendLine(" filter.And = true;"); sb.AppendLine(" }"); sb.AppendLine(" else if (pf.Or != null && pf.Or.Count > 0)"); sb.AppendLine(" {"); sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(pf.Or);"); sb.AppendLine(" filter.And = false;"); sb.AppendLine(" }"); sb.AppendLine(" result.Add(filter);"); sb.AppendLine(" }"); sb.AppendLine(" return result;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" private static List? ConvertSorts(Google.Protobuf.Collections.RepeatedField protoSorts)"); sb.AppendLine(" {"); sb.AppendLine(" if (protoSorts == null || protoSorts.Count == 0)"); sb.AppendLine(" return null;"); sb.AppendLine(); sb.AppendLine(" return protoSorts.Select(s => new PoweredSoft.DynamicQuery.Sort"); sb.AppendLine(" {"); sb.AppendLine(" Path = s.Path,"); sb.AppendLine(" Ascending = s.Ascending"); sb.AppendLine(" }).ToList();"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" private static List? ConvertGroups(Google.Protobuf.Collections.RepeatedField protoGroups)"); sb.AppendLine(" {"); sb.AppendLine(" if (protoGroups == null || protoGroups.Count == 0)"); sb.AppendLine(" return null;"); sb.AppendLine(); sb.AppendLine(" return protoGroups.Select(g => new PoweredSoft.DynamicQuery.Group"); sb.AppendLine(" {"); sb.AppendLine(" Path = g.Path"); sb.AppendLine(" }).ToList();"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" private static List? ConvertAggregates(Google.Protobuf.Collections.RepeatedField protoAggregates)"); sb.AppendLine(" {"); sb.AppendLine(" if (protoAggregates == null || protoAggregates.Count == 0)"); sb.AppendLine(" return null;"); sb.AppendLine(); sb.AppendLine(" return protoAggregates.Select(a => new Svrnty.CQRS.DynamicQuery.DynamicQueryAggregate"); sb.AppendLine(" {"); sb.AppendLine(" Path = a.Path,"); sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.AggregateType)a.Type).ToString()"); sb.AppendLine(" }).ToList();"); sb.AppendLine(" }"); sb.AppendLine(); // Add mapper methods for each entity type foreach (var dynamicQuery in dynamicQueries) { var entityName = dynamicQuery.Name; var protoTypeName = $"{entityName.TrimEnd('s')}"; // User from Users sb.AppendLine($" private static {protoTypeName} MapTo{entityName}ProtoModel({dynamicQuery.DestinationTypeFullyQualified} domainModel)"); sb.AppendLine(" {"); sb.AppendLine($" // Use JSON serialization for mapping between domain and proto models"); sb.AppendLine(" var json = System.Text.Json.JsonSerializer.Serialize(domainModel);"); sb.AppendLine($" return System.Text.Json.JsonSerializer.Deserialize<{protoTypeName}>(json, new System.Text.Json.JsonSerializerOptions {{ PropertyNameCaseInsensitive = true }}) ?? new {protoTypeName}();"); sb.AppendLine(" }"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } } }