checkpoint

This commit is contained in:
Mathias Beaulieu-Duncan 2025-11-03 11:19:50 -05:00
parent 5ba351de9c
commit ed01f58a0c
6 changed files with 767 additions and 24 deletions

View File

@ -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": []

View File

@ -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<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)
@ -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<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));
}
}
}
}
}
@ -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<DynamicQueryInfo>();
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<CommandInfo> commands, List<QueryInfo> 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<CommandInfo> commands, List<QueryInfo> queries, List<DynamicQueryInfo> 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("// <auto-generated />");
@ -605,10 +731,33 @@ namespace Svrnty.CQRS.Grpc.Generators
sb.AppendLine();
}
if (hasCommands && hasQueries)
if (hasDynamicQueries)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers both Command and Query gRPC services");
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(" {");
@ -618,11 +767,13 @@ namespace Svrnty.CQRS.Grpc.Generators
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 both Command and Query gRPC service endpoints");
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(" {");
@ -630,6 +781,8 @@ namespace Svrnty.CQRS.Grpc.Generators
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(" }");
}
@ -943,5 +1096,389 @@ namespace Svrnty.CQRS.Grpc.Generators
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.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<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.AspNetCore.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.AspNetCore.DynamicQueryFilter>();");
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<Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQueryFilter> ConvertProtoFiltersToList(Google.Protobuf.Collections.RepeatedField<DynamicQueryFilter> protoFilters)");
sb.AppendLine(" {");
sb.AppendLine(" var result = new List<Svrnty.CQRS.DynamicQuery.AspNetCore.DynamicQueryFilter>();");
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<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.AspNetCore.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.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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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<INamedTypeSymbol> DiscoverDynamicQueries()
{
// Find IQueryableProvider<T> implementations
var queryableProviderInterface = _compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IQueryableProvider`1");
if (queryableProviderInterface == null)
return new List<INamedTypeSymbol>();
var dynamicQueryTypes = new List<INamedTypeSymbol>();
var allTypes = _compilation.GetSymbolsWithName(_ => true, SymbolFilter.Type)
.OfType<INamedTypeSymbol>();
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<TEntity>
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";
}
}

View File

@ -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<AddUserCommand, int, AddUserCommandHandler, AddUserCommandValidator>();
builder.Services.AddCommand<RemoveUserCommand, RemoveUserCommandHandler>();
// Register query handlers with CQRS
builder.Services.AddQuery<FetchUserQuery, User, FetchUserQueryHandler>();
// Register PoweredSoft.DynamicQuery services
builder.Services.AddTransient<PoweredSoft.Data.Core.IAsyncQueryableService, SimpleAsyncQueryableService>();
builder.Services.AddTransient<PoweredSoft.DynamicQuery.Core.IQueryHandlerAsync, PoweredSoft.DynamicQuery.QueryHandlerAsync>();
// Register dynamic query for User entity with queryable provider
builder.Services.AddDynamicQueryWithProvider<User, UserQueryableProvider>();
// 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();

View File

@ -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;
}