dotnet-cqrs/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs

1551 lines
80 KiB
C#

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<INamedTypeSymbol?> 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<INamedTypeSymbol, INamedTypeSymbol?>(SymbolEqualityComparer.Default); // Command -> Result type (null if no result)
var queryMap = new Dictionary<INamedTypeSymbol, INamedTypeSymbol>(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<T> or ICommandHandler<T, TResult>
foreach (var iface in typeSymbol.AllInterfaces)
{
if (iface.IsGenericType)
{
// Check for ICommandHandler<TCommand>
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<TCommand, TResult>
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<TQuery, TResult>
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<T> - 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<TSource>
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<CommandInfo>();
var queries = new List<QueryInfo>();
// 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<DynamicQueryInfo>();
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<PropertyInfo>()
};
// 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<IPropertySymbol>()
.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<PropertyInfo>()
};
// 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<IPropertySymbol>()
.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<IPropertySymbol>()
.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<CommandInfo> commands, List<QueryInfo> queries, List<DynamicQueryInfo> 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<CommandInfo> commands, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
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<QueryInfo> queries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
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<CommandInfo> commands, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
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<QueryInfo> queries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
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("// <auto-generated />");
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(" /// <summary>");
sb.AppendLine(" /// Auto-generated extension methods for registering and mapping gRPC services");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static class GrpcServiceRegistrationExtensions");
sb.AppendLine(" {");
if (hasCommands)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers the auto-generated Command gRPC service");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IServiceCollection AddGrpcCommandService(this IServiceCollection services)");
sb.AppendLine(" {");
sb.AppendLine(" services.AddGrpc();");
sb.AppendLine(" services.AddSingleton<CommandServiceImpl>();");
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps the auto-generated Command gRPC service endpoints");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommands(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
sb.AppendLine(" endpoints.MapGrpcService<CommandServiceImpl>();");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
sb.AppendLine();
}
if (hasQueries)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers the auto-generated Query gRPC service");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IServiceCollection AddGrpcQueryService(this IServiceCollection services)");
sb.AppendLine(" {");
sb.AppendLine(" services.AddGrpc();");
sb.AppendLine(" services.AddSingleton<QueryServiceImpl>();");
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps the auto-generated Query gRPC service endpoints");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcQueries(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
sb.AppendLine(" endpoints.MapGrpcService<QueryServiceImpl>();");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
sb.AppendLine();
}
if (hasDynamicQueries)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers the auto-generated DynamicQuery gRPC service");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IServiceCollection AddGrpcDynamicQueryService(this IServiceCollection services)");
sb.AppendLine(" {");
sb.AppendLine(" services.AddGrpc();");
sb.AppendLine(" services.AddSingleton<DynamicQueryServiceImpl>();");
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps the auto-generated DynamicQuery gRPC service endpoints");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcDynamicQueries(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
sb.AppendLine(" endpoints.MapGrpcService<DynamicQueryServiceImpl>();");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
sb.AppendLine();
}
if (hasCommands || hasQueries || hasDynamicQueries)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers all auto-generated gRPC services (Commands, Queries, and DynamicQueries)");
sb.AppendLine(" /// </summary>");
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<CommandServiceImpl>();");
if (hasQueries)
sb.AppendLine(" services.AddSingleton<QueryServiceImpl>();");
if (hasDynamicQueries)
sb.AppendLine(" services.AddSingleton<DynamicQueryServiceImpl>();");
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps all auto-generated gRPC service endpoints (Commands, Queries, and DynamicQueries)");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommandsAndQueries(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
if (hasCommands)
sb.AppendLine(" endpoints.MapGrpcService<CommandServiceImpl>();");
if (hasQueries)
sb.AppendLine(" endpoints.MapGrpcService<QueryServiceImpl>();");
if (hasDynamicQueries)
sb.AppendLine(" endpoints.MapGrpcService<DynamicQueryServiceImpl>();");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
sb.AppendLine();
// Add configuration-based methods
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers gRPC services based on configuration");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IServiceCollection AddGrpcFromConfiguration(this IServiceCollection services)");
sb.AppendLine(" {");
sb.AppendLine(" var config = services.BuildServiceProvider().GetService<Svrnty.CQRS.Configuration.CqrsConfiguration>();");
sb.AppendLine(" var grpcOptions = config?.GetConfiguration<Svrnty.CQRS.Grpc.GrpcCqrsOptions>();");
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<CommandServiceImpl>();");
}
if (hasQueries)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())");
sb.AppendLine(" services.AddSingleton<QueryServiceImpl>();");
}
if (hasDynamicQueries)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())");
sb.AppendLine(" services.AddSingleton<DynamicQueryServiceImpl>();");
}
sb.AppendLine(" }");
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps gRPC service endpoints based on configuration");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcFromConfiguration(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
sb.AppendLine(" var config = endpoints.ServiceProvider.GetService<Svrnty.CQRS.Configuration.CqrsConfiguration>();");
sb.AppendLine(" var grpcOptions = config?.GetConfiguration<Svrnty.CQRS.Grpc.GrpcCqrsOptions>();");
sb.AppendLine(" if (grpcOptions != null)");
sb.AppendLine(" {");
if (hasCommands)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapCommands())");
sb.AppendLine(" endpoints.MapGrpcService<CommandServiceImpl>();");
}
if (hasQueries)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())");
sb.AppendLine(" endpoints.MapGrpcService<QueryServiceImpl>();");
}
if (hasDynamicQueries)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())");
sb.AppendLine(" endpoints.MapGrpcService<DynamicQueryServiceImpl>();");
}
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<CommandInfo> 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<QueryInfo> 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<CommandInfo> commands, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
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(" /// <summary>");
sb.AppendLine(" /// Auto-generated gRPC service implementation for Commands");
sb.AppendLine(" /// </summary>");
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<IValidator<{command.FullyQualifiedName}>>();");
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<QueryInfo> queries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
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(" /// <summary>");
sb.AppendLine(" /// Auto-generated gRPC service implementation for Queries");
sb.AppendLine(" /// </summary>");
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<DynamicQueryInfo> dynamicQueries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
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(" /// <summary>");
sb.AppendLine(" /// Dynamic query filter with support for nested AND/OR logic");
sb.AppendLine(" /// </summary>");
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<DynamicQueryFilter>? And { get; set; }");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(5)]");
sb.AppendLine(" [DataMember(Order = 5)]");
sb.AppendLine(" public List<DynamicQueryFilter>? Or { get; set; }");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Dynamic query sort");
sb.AppendLine(" /// </summary>");
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(" /// <summary>");
sb.AppendLine(" /// Dynamic query group");
sb.AppendLine(" /// </summary>");
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(" /// <summary>");
sb.AppendLine(" /// Dynamic query aggregate");
sb.AppendLine(" /// </summary>");
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($" /// <summary>");
sb.AppendLine($" /// Request message for dynamic query on {dynamicQuery.Name}");
sb.AppendLine($" /// </summary>");
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<DynamicQueryFilter> Filters { get; set; } = new();");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(4)]");
sb.AppendLine(" [DataMember(Order = 4)]");
sb.AppendLine(" public List<DynamicQuerySort> Sorts { get; set; } = new();");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(5)]");
sb.AppendLine(" [DataMember(Order = 5)]");
sb.AppendLine(" public List<DynamicQueryGroup> Groups { get; set; } = new();");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(6)]");
sb.AppendLine(" [DataMember(Order = 6)]");
sb.AppendLine(" public List<DynamicQueryAggregate> Aggregates { get; set; } = new();");
sb.AppendLine(" }");
sb.AppendLine();
// Response message
sb.AppendLine($" /// <summary>");
sb.AppendLine($" /// Response message for dynamic query on {dynamicQuery.Name}");
sb.AppendLine($" /// </summary>");
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(" /// <summary>");
sb.AppendLine(" /// gRPC service interface for DynamicQueries");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" [ServiceContract]");
sb.AppendLine(" public interface IDynamicQueryService");
sb.AppendLine(" {");
foreach (var dynamicQuery in dynamicQueries)
{
var methodName = $"Query{dynamicQuery.Name}";
sb.AppendLine($" /// <summary>");
sb.AppendLine($" /// Execute dynamic query on {dynamicQuery.Name}");
sb.AppendLine($" /// </summary>");
sb.AppendLine(" [OperationContract]");
sb.AppendLine($" System.Threading.Tasks.Task<DynamicQuery{dynamicQuery.Name}Response> {methodName}Async(DynamicQuery{dynamicQuery.Name}Request request, CallContext context = default);");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static string GenerateDynamicQueryServiceImpl(List<DynamicQueryInfo> dynamicQueries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
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(" /// <summary>");
sb.AppendLine(" /// Auto-generated gRPC service implementation for DynamicQueries");
sb.AppendLine(" /// </summary>");
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<IQueryHandler<{dynamicQuery.QueryInterfaceName}, IQueryExecutionResult<{dynamicQuery.DestinationTypeFullyQualified}>>>();");
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<Svrnty.CQRS.DynamicQuery.DynamicQueryFilter>? ConvertFilters(Google.Protobuf.Collections.RepeatedField<DynamicQueryFilter> protoFilters)");
sb.AppendLine(" {");
sb.AppendLine(" if (protoFilters == null || protoFilters.Count == 0)");
sb.AppendLine(" return null;");
sb.AppendLine();
sb.AppendLine(" var filters = new List<Svrnty.CQRS.DynamicQuery.DynamicQueryFilter>();");
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<Svrnty.CQRS.DynamicQuery.DynamicQueryFilter> ConvertProtoFiltersToList(Google.Protobuf.Collections.RepeatedField<DynamicQueryFilter> protoFilters)");
sb.AppendLine(" {");
sb.AppendLine(" var result = new List<Svrnty.CQRS.DynamicQuery.DynamicQueryFilter>();");
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<PoweredSoft.DynamicQuery.Sort>? ConvertSorts(Google.Protobuf.Collections.RepeatedField<DynamicQuerySort> 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<PoweredSoft.DynamicQuery.Group>? ConvertGroups(Google.Protobuf.Collections.RepeatedField<DynamicQueryGroup> 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<Svrnty.CQRS.DynamicQuery.DynamicQueryAggregate>? ConvertAggregates(Google.Protobuf.Collections.RepeatedField<DynamicQueryAggregate> 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();
}
}
}