diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2313d06..cd78fde 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -27,7 +27,10 @@ "Bash(dotnet --list-sdks:*)", "Bash(dotnet sln:*)", "Bash(pkill:*)", - "Bash(python3:*)" + "Bash(python3:*)", + "Bash(grpcurl:*)", + "Bash(lsof:*)", + "Bash(xargs kill -9)" ], "deny": [], "ask": [] diff --git a/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs b/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs index e147baa..839a6ef 100644 --- a/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs +++ b/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs @@ -40,6 +40,9 @@ namespace Svrnty.CQRS.Grpc.Generators 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) { @@ -48,6 +51,7 @@ namespace Svrnty.CQRS.Grpc.Generators 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) @@ -81,7 +85,68 @@ namespace Svrnty.CQRS.Grpc.Generators var queryType = iface.TypeArguments[0] as INamedTypeSymbol; var resultType = iface.TypeArguments[1] as INamedTypeSymbol; if (queryType != null && resultType != null) - queryMap[queryType] = resultType; + { + // 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)); + } + } } } } @@ -120,10 +185,19 @@ namespace Svrnty.CQRS.Grpc.Generators queries.Add(queryInfo); } - // Generate services if we found any commands or queries - if (commands.Any() || queries.Any()) + // Process discovered dynamic query types + var dynamicQueries = new List(); + foreach (var (sourceType, destinationType, paramsType) in dynamicQueryMap) { - GenerateProtoAndServices(context, commands, queries, compilation); + 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); } } @@ -265,7 +339,52 @@ namespace Svrnty.CQRS.Grpc.Generators return queryInfo; } - private static void GenerateProtoAndServices(SourceProductionContext context, List commands, List queries, Compilation compilation) + 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"; @@ -284,8 +403,15 @@ namespace Svrnty.CQRS.Grpc.Generators 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(), rootNamespace); + var registrationExtensions = GenerateRegistrationExtensions(commands.Any(), queries.Any(), dynamicQueries.Any(), rootNamespace); context.AddSource("GrpcServiceRegistration.g.cs", registrationExtensions); } @@ -542,7 +668,7 @@ namespace Svrnty.CQRS.Grpc.Generators return sb.ToString(); } - private static string GenerateRegistrationExtensions(bool hasCommands, bool hasQueries, string rootNamespace) + private static string GenerateRegistrationExtensions(bool hasCommands, bool hasQueries, bool hasDynamicQueries, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); @@ -605,10 +731,33 @@ namespace Svrnty.CQRS.Grpc.Generators sb.AppendLine(); } - if (hasCommands && hasQueries) + if (hasDynamicQueries) { sb.AppendLine(" /// "); - sb.AppendLine(" /// Registers both Command and Query gRPC services"); + 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(" {"); @@ -618,11 +767,13 @@ namespace Svrnty.CQRS.Grpc.Generators 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 both Command and Query gRPC service endpoints"); + 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(" {"); @@ -630,6 +781,8 @@ namespace Svrnty.CQRS.Grpc.Generators sb.AppendLine(" endpoints.MapGrpcService();"); if (hasQueries) sb.AppendLine(" endpoints.MapGrpcService();"); + if (hasDynamicQueries) + sb.AppendLine(" endpoints.MapGrpcService();"); sb.AppendLine(" return endpoints;"); sb.AppendLine(" }"); } @@ -943,5 +1096,389 @@ namespace Svrnty.CQRS.Grpc.Generators 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.AspNetCore.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}, {dynamicQuery.ParamsTypeFullyQualified}>"); + } + else + { + sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.AspNetCore.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.AspNetCore.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.AspNetCore.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.AspNetCore.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(); + } } } diff --git a/Svrnty.CQRS.Grpc.Generators/Models/DynamicQueryInfo.cs b/Svrnty.CQRS.Grpc.Generators/Models/DynamicQueryInfo.cs new file mode 100644 index 0000000..4c2ac69 --- /dev/null +++ b/Svrnty.CQRS.Grpc.Generators/Models/DynamicQueryInfo.cs @@ -0,0 +1,28 @@ +namespace Svrnty.CQRS.Grpc.Generators.Models +{ + public class DynamicQueryInfo + { + public string Name { get; set; } + public string SourceType { get; set; } + public string SourceTypeFullyQualified { get; set; } + public string DestinationType { get; set; } + public string DestinationTypeFullyQualified { get; set; } + public string? ParamsType { get; set; } + public string? ParamsTypeFullyQualified { get; set; } + public string HandlerInterfaceName { get; set; } + public string QueryInterfaceName { get; set; } + public bool HasParams { get; set; } + + public DynamicQueryInfo() + { + Name = string.Empty; + SourceType = string.Empty; + SourceTypeFullyQualified = string.Empty; + DestinationType = string.Empty; + DestinationTypeFullyQualified = string.Empty; + HandlerInterfaceName = string.Empty; + QueryInterfaceName = string.Empty; + HasParams = false; + } + } +} diff --git a/Svrnty.CQRS.Grpc.Generators/ProtoFileGenerator.cs b/Svrnty.CQRS.Grpc.Generators/ProtoFileGenerator.cs index 7eac118..dd66fa7 100644 --- a/Svrnty.CQRS.Grpc.Generators/ProtoFileGenerator.cs +++ b/Svrnty.CQRS.Grpc.Generators/ProtoFileGenerator.cs @@ -24,6 +24,7 @@ internal class ProtoFileGenerator { var commands = DiscoverCommands(); var queries = DiscoverQueries(); + var dynamicQueries = DiscoverDynamicQueries(); var sb = new StringBuilder(); @@ -76,6 +77,27 @@ internal class ProtoFileGenerator sb.AppendLine(); } + // DynamicQuery Service + if (dynamicQueries.Any()) + { + sb.AppendLine("// DynamicQuery service for CQRS operations"); + sb.AppendLine("service DynamicQueryService {"); + foreach (var dq in dynamicQueries) + { + var entityName = dq.Name; + var pluralName = Pluralize(entityName); + var methodName = $"Query{pluralName}"; + var requestType = $"DynamicQuery{pluralName}Request"; + var responseType = $"DynamicQuery{pluralName}Response"; + + sb.AppendLine($" // Dynamic query for {entityName}"); + sb.AppendLine($" rpc {methodName} ({requestType}) returns ({responseType});"); + sb.AppendLine(); + } + sb.AppendLine("}"); + sb.AppendLine(); + } + // Generate messages for commands foreach (var command in commands) { @@ -90,6 +112,12 @@ internal class ProtoFileGenerator GenerateResponseMessage(query); } + // Generate messages for dynamic queries + foreach (var dq in dynamicQueries) + { + GenerateDynamicQueryMessages(dq); + } + // Append all generated messages sb.Append(_messagesBuilder); @@ -335,4 +363,112 @@ internal class ProtoFileGenerator return $"{type.Name} operation"; } + + private List DiscoverDynamicQueries() + { + // Find IQueryableProvider implementations + var queryableProviderInterface = _compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IQueryableProvider`1"); + if (queryableProviderInterface == null) + return new List(); + + var dynamicQueryTypes = new List(); + var allTypes = _compilation.GetSymbolsWithName(_ => true, SymbolFilter.Type) + .OfType(); + + foreach (var type in allTypes) + { + if (type.IsAbstract || type.IsStatic) + continue; + + foreach (var iface in type.AllInterfaces) + { + if (iface.IsGenericType && SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryableProviderInterface)) + { + // Extract the entity type from IQueryableProvider + var entityType = iface.TypeArguments[0] as INamedTypeSymbol; + if (entityType != null && !dynamicQueryTypes.Contains(entityType, SymbolEqualityComparer.Default)) + { + dynamicQueryTypes.Add(entityType); + } + } + } + } + + return dynamicQueryTypes; + } + + private void GenerateDynamicQueryMessages(INamedTypeSymbol entityType) + { + var pluralName = Pluralize(entityType.Name); + var requestMessageName = $"DynamicQuery{pluralName}Request"; + var responseMessageName = $"DynamicQuery{pluralName}Response"; + + // Common filter/sort/group/aggregate types (only generate once) + if (!_generatedMessages.Contains("DynamicQueryFilter")) + { + _generatedMessages.Add("DynamicQueryFilter"); + _messagesBuilder.AppendLine("// Dynamic query filter with AND/OR support"); + _messagesBuilder.AppendLine("message DynamicQueryFilter {"); + _messagesBuilder.AppendLine(" string path = 1;"); + _messagesBuilder.AppendLine(" int32 type = 2; // PoweredSoft.DynamicQuery.Core.FilterType"); + _messagesBuilder.AppendLine(" string value = 3;"); + _messagesBuilder.AppendLine(" repeated DynamicQueryFilter and = 4;"); + _messagesBuilder.AppendLine(" repeated DynamicQueryFilter or = 5;"); + _messagesBuilder.AppendLine("}"); + _messagesBuilder.AppendLine(); + + _messagesBuilder.AppendLine("// Dynamic query sort"); + _messagesBuilder.AppendLine("message DynamicQuerySort {"); + _messagesBuilder.AppendLine(" string path = 1;"); + _messagesBuilder.AppendLine(" bool ascending = 2;"); + _messagesBuilder.AppendLine("}"); + _messagesBuilder.AppendLine(); + + _messagesBuilder.AppendLine("// Dynamic query group"); + _messagesBuilder.AppendLine("message DynamicQueryGroup {"); + _messagesBuilder.AppendLine(" string path = 1;"); + _messagesBuilder.AppendLine("}"); + _messagesBuilder.AppendLine(); + + _messagesBuilder.AppendLine("// Dynamic query aggregate"); + _messagesBuilder.AppendLine("message DynamicQueryAggregate {"); + _messagesBuilder.AppendLine(" string path = 1;"); + _messagesBuilder.AppendLine(" int32 type = 2; // PoweredSoft.DynamicQuery.Core.AggregateType"); + _messagesBuilder.AppendLine("}"); + _messagesBuilder.AppendLine(); + } + + // Request message + _messagesBuilder.AppendLine($"// Dynamic query request for {entityType.Name}"); + _messagesBuilder.AppendLine($"message {requestMessageName} {{"); + _messagesBuilder.AppendLine(" int32 page = 1;"); + _messagesBuilder.AppendLine(" int32 page_size = 2;"); + _messagesBuilder.AppendLine(" repeated DynamicQueryFilter filters = 3;"); + _messagesBuilder.AppendLine(" repeated DynamicQuerySort sorts = 4;"); + _messagesBuilder.AppendLine(" repeated DynamicQueryGroup groups = 5;"); + _messagesBuilder.AppendLine(" repeated DynamicQueryAggregate aggregates = 6;"); + _messagesBuilder.AppendLine("}"); + _messagesBuilder.AppendLine(); + + // Response message + _messagesBuilder.AppendLine($"// Dynamic query response for {entityType.Name}"); + _messagesBuilder.AppendLine($"message {responseMessageName} {{"); + _messagesBuilder.AppendLine($" repeated {entityType.Name} data = 1;"); + _messagesBuilder.AppendLine(" int64 total_records = 2;"); + _messagesBuilder.AppendLine(" int32 number_of_pages = 3;"); + _messagesBuilder.AppendLine("}"); + _messagesBuilder.AppendLine(); + + // Generate entity message if not already generated + GenerateComplexTypeMessage(entityType); + } + + private static string Pluralize(string word) + { + if (word.EndsWith("y") && word.Length > 1 && !"aeiou".Contains(word[word.Length - 2].ToString())) + return word.Substring(0, word.Length - 1) + "ies"; + if (word.EndsWith("s") || word.EndsWith("x") || word.EndsWith("z") || word.EndsWith("ch") || word.EndsWith("sh")) + return word + "es"; + return word + "s"; + } } diff --git a/Svrnty.Sample/Program.cs b/Svrnty.Sample/Program.cs index af07719..14cdef6 100644 --- a/Svrnty.Sample/Program.cs +++ b/Svrnty.Sample/Program.cs @@ -19,44 +19,33 @@ builder.WebHost.ConfigureKestrel(options => options.ListenLocalhost(6001, o => o.Protocols = HttpProtocols.Http1); }); -// Register command handlers with CQRS and FluentValidation builder.Services.AddCommand(); builder.Services.AddCommand(); -// Register query handlers with CQRS builder.Services.AddQuery(); -// Register PoweredSoft.DynamicQuery services builder.Services.AddTransient(); builder.Services.AddTransient(); -// Register dynamic query for User entity with queryable provider builder.Services.AddDynamicQueryWithProvider(); -// Register discovery services for MinimalApi builder.Services.AddDefaultCommandDiscovery(); builder.Services.AddDefaultQueryDiscovery(); -// Auto-generated: Register gRPC services for both commands and queries (includes reflection) -builder.Services.AddGrpcCommandsAndQueries(); -// Add Swagger/OpenAPI support + builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); -// Auto-generated: Map gRPC endpoints for both commands and queries +builder.Services.AddGrpcCommandsAndQueries(); app.MapGrpcCommandsAndQueries(); - -// Map gRPC reflection service app.MapGrpcReflectionService(); -// Enable Swagger middleware app.UseSwagger(); app.UseSwaggerUI(); -// Map MinimalApi endpoints for commands and queries app.MapSvrntyCommands(); app.MapSvrntyQueries(); app.MapSvrntyDynamicQueries(); diff --git a/Svrnty.Sample/Protos/cqrs_services.proto b/Svrnty.Sample/Protos/cqrs_services.proto index b84bd32..10bad1c 100644 --- a/Svrnty.Sample/Protos/cqrs_services.proto +++ b/Svrnty.Sample/Protos/cqrs_services.proto @@ -21,6 +21,13 @@ service QueryService { } +// DynamicQuery service for CQRS operations +service DynamicQueryService { + // Dynamic query for User + rpc QueryUsers (DynamicQueryUsersRequest) returns (DynamicQueryUsersResponse); + +} + // Request message for AddUserCommand message AddUserCommandRequest { string name = 1; @@ -59,3 +66,46 @@ message User { string email = 3; } +// Dynamic query filter with AND/OR support +message DynamicQueryFilter { + string path = 1; + int32 type = 2; // PoweredSoft.DynamicQuery.Core.FilterType + string value = 3; + repeated DynamicQueryFilter and = 4; + repeated DynamicQueryFilter or = 5; +} + +// Dynamic query sort +message DynamicQuerySort { + string path = 1; + bool ascending = 2; +} + +// Dynamic query group +message DynamicQueryGroup { + string path = 1; +} + +// Dynamic query aggregate +message DynamicQueryAggregate { + string path = 1; + int32 type = 2; // PoweredSoft.DynamicQuery.Core.AggregateType +} + +// Dynamic query request for User +message DynamicQueryUsersRequest { + int32 page = 1; + int32 page_size = 2; + repeated DynamicQueryFilter filters = 3; + repeated DynamicQuerySort sorts = 4; + repeated DynamicQueryGroup groups = 5; + repeated DynamicQueryAggregate aggregates = 6; +} + +// Dynamic query response for User +message DynamicQueryUsersResponse { + repeated User data = 1; + int64 total_records = 2; + int32 number_of_pages = 3; +} +