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 from source 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; } /// /// Collects all types from the compilation and all referenced assemblies /// private static IEnumerable GetAllTypesFromCompilation(Compilation compilation) { var types = new List(); // Get types from the current assembly CollectTypesFromNamespace(compilation.Assembly.GlobalNamespace, types); // Get types from all referenced assemblies foreach (var reference in compilation.References) { var assemblySymbol = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol; if (assemblySymbol != null) { CollectTypesFromNamespace(assemblySymbol.GlobalNamespace, types); } } return types; } private static void CollectTypesFromNamespace(INamespaceSymbol ns, List types) { foreach (var type in ns.GetTypeMembers()) { types.Add(type); // Also collect nested types CollectNestedTypes(type, types); } foreach (var nestedNs in ns.GetNamespaceMembers()) { CollectTypesFromNamespace(nestedNs, types); } } private static void CollectNestedTypes(INamedTypeSymbol type, List types) { foreach (var nestedType in type.GetTypeMembers()) { types.Add(nestedType); CollectNestedTypes(nestedType, types); } } private static void Execute(Compilation compilation, IEnumerable sourceTypes, SourceProductionContext context) { // Get the expected namespace for proto-generated types var rootNamespace = compilation.AssemblyName ?? "Generated"; var grpcNamespace = $"{rootNamespace}.Grpc"; // Check if proto types are available (from Grpc.Tools compilation of .proto file) // If not, skip generation - this happens on first build before proto file is compiled var commandServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.CommandService+CommandServiceBase"); var queryServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.QueryService+QueryServiceBase"); var dynamicQueryServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.DynamicQueryService+DynamicQueryServiceBase"); var notificationServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.NotificationService+NotificationServiceBase"); // If none of the service bases exist, the proto hasn't been compiled yet - skip generation if (commandServiceBase == null && queryServiceBase == null && dynamicQueryServiceBase == null && notificationServiceBase == null) { // Report diagnostic for first build var descriptor = new DiagnosticDescriptor( "CQRSGRPC003", "Proto types not yet available", "gRPC service implementations will be generated on second build after proto file is compiled", "Svrnty.CQRS.Grpc", DiagnosticSeverity.Info, isEnabledByDefault: true); context.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None)); return; } var grpcIgnoreAttribute = compilation.GetTypeByMetadataName("Svrnty.CQRS.Grpc.Abstractions.Attributes.GrpcIgnoreAttribute"); var commandHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`1"); var commandHandlerWithResultInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`2"); var queryHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.IQueryHandler`2"); var dynamicQueryInterface2 = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IDynamicQuery`2"); var dynamicQueryInterface3 = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IDynamicQuery`3"); var queryableProviderInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IQueryableProvider`1"); if (commandHandlerInterface == null || queryHandlerInterface == null) { return; // Handler interfaces not found } var commandMap = new Dictionary(SymbolEqualityComparer.Default); // Command -> Result type (null if no result) var queryMap = new Dictionary(SymbolEqualityComparer.Default); // Query -> Result type var dynamicQueryMap = new List<(INamedTypeSymbol SourceType, INamedTypeSymbol DestinationType, INamedTypeSymbol? ParamsType)>(); // List of (Source, Destination, Params?) // Get all types from the compilation and referenced assemblies var allTypes = GetAllTypesFromCompilation(compilation); // Find all command and query types by looking at handler implementations foreach (var typeSymbol in allTypes) { if (typeSymbol.IsAbstract || typeSymbol.IsStatic) continue; // Check if this type implements ICommandHandler or ICommandHandler foreach (var iface in typeSymbol.AllInterfaces) { if (iface.IsGenericType) { // Check for ICommandHandler if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerInterface) && iface.TypeArguments.Length == 1) { var commandType = iface.TypeArguments[0] as INamedTypeSymbol; if (commandType != null && !commandMap.ContainsKey(commandType)) commandMap[commandType] = null; // No result type } // Check for ICommandHandler else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerWithResultInterface) && iface.TypeArguments.Length == 2) { var commandType = iface.TypeArguments[0] as INamedTypeSymbol; var resultType = iface.TypeArguments[1] as INamedTypeSymbol; if (commandType != null && resultType != null) commandMap[commandType] = resultType; } // Check for IQueryHandler else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryHandlerInterface) && iface.TypeArguments.Length == 2) { var queryType = iface.TypeArguments[0] as INamedTypeSymbol; var resultType = iface.TypeArguments[1] as INamedTypeSymbol; if (queryType != null && resultType != null) { // Check if this is a dynamic query handler if (queryType.IsGenericType && (SymbolEqualityComparer.Default.Equals(queryType.OriginalDefinition, dynamicQueryInterface2) || SymbolEqualityComparer.Default.Equals(queryType.OriginalDefinition, dynamicQueryInterface3))) { // Extract source, destination, and optional params types var sourceType = queryType.TypeArguments[0] as INamedTypeSymbol; var destinationType = queryType.TypeArguments[1] as INamedTypeSymbol; INamedTypeSymbol? paramsType = null; if (queryType.TypeArguments.Length == 3) { paramsType = queryType.TypeArguments[2] as INamedTypeSymbol; } if (sourceType != null && destinationType != null) { // Check if already added (avoid duplicates) var exists = dynamicQueryMap.Any(dq => SymbolEqualityComparer.Default.Equals(dq.SourceType, sourceType) && SymbolEqualityComparer.Default.Equals(dq.DestinationType, destinationType) && (dq.ParamsType == null && paramsType == null || dq.ParamsType != null && paramsType != null && SymbolEqualityComparer.Default.Equals(dq.ParamsType, paramsType))); if (!exists) { dynamicQueryMap.Add((sourceType, destinationType, paramsType)); } } } else { queryMap[queryType] = resultType; } } } } } // Check if this type implements IQueryableProvider - this indicates a dynamic query if (queryableProviderInterface != null) { foreach (var iface in typeSymbol.AllInterfaces) { if (iface.IsGenericType && SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryableProviderInterface)) { // Extract source type from IQueryableProvider var sourceType = iface.TypeArguments[0] as INamedTypeSymbol; if (sourceType != null) { // For IQueryableProvider, we assume TSource = TDestination (no params) var exists = dynamicQueryMap.Any(dq => SymbolEqualityComparer.Default.Equals(dq.SourceType, sourceType) && SymbolEqualityComparer.Default.Equals(dq.DestinationType, sourceType) && dq.ParamsType == null); if (!exists) { dynamicQueryMap.Add((sourceType, sourceType, null)); } } } } } } var commands = new List(); var queries = new List(); // Process discovered command types foreach (var kvp in commandMap) { var commandType = kvp.Key; var resultType = kvp.Value; // Skip if marked with [GrpcIgnore] if (HasGrpcIgnoreAttribute(commandType)) 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 (HasGrpcIgnoreAttribute(queryType)) continue; var queryInfo = ExtractQueryInfo(queryType, resultType); if (queryInfo != null) queries.Add(queryInfo); } // Process discovered dynamic query types var dynamicQueries = new List(); foreach (var (sourceType, destinationType, paramsType) in dynamicQueryMap) { var dynamicQueryInfo = ExtractDynamicQueryInfo(sourceType, destinationType, paramsType); if (dynamicQueryInfo != null) dynamicQueries.Add(dynamicQueryInfo); } // Process discovered notification types (marked with [StreamingNotification]) var notifications = DiscoverNotifications(allTypes, compilation); // Generate services if we found any commands, queries, dynamic queries, or notifications if (commands.Any() || queries.Any() || dynamicQueries.Any() || notifications.Any()) { GenerateProtoAndServices(context, commands, queries, dynamicQueries, notifications, compilation); } } private static bool HasAttribute(INamedTypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol) { return typeSymbol.GetAttributes().Any(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeSymbol)); } private static bool HasGrpcIgnoreAttribute(INamedTypeSymbol typeSymbol) { return typeSymbol.GetAttributes().Any(attr => attr.AttributeClass?.Name == "GrpcIgnoreAttribute"); } private static bool ImplementsInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? interfaceSymbol) { if (interfaceSymbol == null) return false; return typeSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, interfaceSymbol)); } private static bool ImplementsGenericInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? genericInterfaceSymbol) { if (genericInterfaceSymbol == null) return false; return typeSymbol.AllInterfaces.Any(i => i.IsGenericType && SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, genericInterfaceSymbol)); } private static CommandInfo? ExtractCommandInfo(INamedTypeSymbol commandType, INamedTypeSymbol? resultType) { var commandInfo = new CommandInfo { Name = commandType.Name, FullyQualifiedName = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), Namespace = commandType.ContainingNamespace.ToDisplayString(), Properties = new List() }; // Set result type if provided if (resultType != null) { commandInfo.ResultType = resultType.Name; commandInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); // Use fully qualified names to avoid ambiguity with proto-generated types var commandTypeFullyQualified = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var resultTypeFullyQualified = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandTypeFullyQualified}, {resultTypeFullyQualified}>"; } else { var commandTypeFullyQualified = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandTypeFullyQualified}>"; } // Extract properties var properties = commandType.GetMembers().OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) .ToList(); int fieldNumber = 1; foreach (var property in properties) { var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional); var propInfo = new PropertyInfo { Name = property.Name, Type = propertyType, FullyQualifiedType = propertyType, ProtoType = protoType, FieldNumber = fieldNumber++, IsComplexType = IsUserDefinedComplexType(property.Type), // New type metadata fields IsNullable = IsNullableType(property.Type), IsEnum = IsEnumType(property.Type), IsDecimal = IsDecimalType(property.Type), IsDateTime = IsDateTimeType(property.Type), IsList = IsListOrCollection(property.Type), }; // If it's a list, extract element type info if (propInfo.IsList) { var elementType = GetListElementType(property.Type); if (elementType != null) { propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType); // If element is complex, extract nested properties if (propInfo.IsElementComplexType) { var unwrappedElement = UnwrapNullableType(elementType); if (unwrappedElement is INamedTypeSymbol namedElementType) { propInfo.ElementNestedProperties = new List(); ExtractNestedPropertiesWithTypeInfo(namedElementType, propInfo.ElementNestedProperties); } } } } // If it's a complex type (not list), extract nested properties else if (propInfo.IsComplexType) { var unwrapped = UnwrapNullableType(property.Type); if (unwrapped is INamedTypeSymbol namedType) { ExtractNestedPropertiesWithTypeInfo(namedType, propInfo.NestedProperties); } } commandInfo.Properties.Add(propInfo); } return commandInfo; } private static bool IsUserDefinedComplexType(ITypeSymbol type) { if (type == null) return false; // Unwrap nullable first var unwrapped = UnwrapNullableType(type); if (unwrapped.TypeKind != TypeKind.Class && unwrapped.TypeKind != TypeKind.Struct) return false; var fullName = unwrapped.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); // Exclude system types and primitives if (fullName.StartsWith("global::System.")) return false; if (IsPrimitiveType(fullName)) return false; return true; } private static ITypeSymbol UnwrapNullableType(ITypeSymbol type) { // Handle Nullable (value type nullability) if (type is INamedTypeSymbol namedType && namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && namedType.TypeArguments.Length == 1) { return namedType.TypeArguments[0]; } return type; } private static bool IsNullableType(ITypeSymbol type) { // Check for Nullable (value type nullability) if (type is INamedTypeSymbol namedType && namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) { return true; } // Check for reference type nullability (C# 8.0+) if (type.NullableAnnotation == NullableAnnotation.Annotated) { return true; } return false; } private static bool IsDecimalType(ITypeSymbol type) { var unwrapped = UnwrapNullableType(type); return unwrapped.SpecialType == SpecialType.System_Decimal; } private static bool IsDateTimeType(ITypeSymbol type) { var unwrapped = UnwrapNullableType(type); return unwrapped.SpecialType == SpecialType.System_DateTime; } private static bool IsEnumType(ITypeSymbol type) { var unwrapped = UnwrapNullableType(type); return unwrapped.TypeKind == TypeKind.Enum; } private static bool IsListOrCollection(ITypeSymbol type) { if (type is IArrayTypeSymbol) return true; if (type is INamedTypeSymbol namedType && namedType.IsGenericType) { var typeName = namedType.OriginalDefinition.ToDisplayString(); return typeName.StartsWith("System.Collections.Generic.List<") || typeName.StartsWith("System.Collections.Generic.IList<") || typeName.StartsWith("System.Collections.Generic.ICollection<") || typeName.StartsWith("System.Collections.Generic.IEnumerable<"); } return false; } private static ITypeSymbol? GetListElementType(ITypeSymbol type) { if (type is IArrayTypeSymbol arrayType) return arrayType.ElementType; if (type is INamedTypeSymbol namedType && namedType.IsGenericType && namedType.TypeArguments.Length > 0) { return namedType.TypeArguments[0]; } return null; } private static void ExtractNestedProperties(INamedTypeSymbol type, List nestedProperties) { var properties = type.GetMembers().OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) .ToList(); foreach (var property in properties) { var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var propInfo = new PropertyInfo { Name = property.Name, Type = propertyType, FullyQualifiedType = propertyType, ProtoType = string.Empty, FieldNumber = 0, IsComplexType = IsUserDefinedComplexType(property.Type), }; // Recursively extract nested properties for complex types if (propInfo.IsComplexType && property.Type is INamedTypeSymbol namedType) { ExtractNestedProperties(namedType, propInfo.NestedProperties); } nestedProperties.Add(propInfo); } } private static void ExtractNestedPropertiesWithTypeInfo(INamedTypeSymbol type, List nestedProperties) { var properties = type.GetMembers().OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) .ToList(); foreach (var property in properties) { var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var propInfo = new PropertyInfo { Name = property.Name, Type = propertyType, FullyQualifiedType = propertyType, ProtoType = string.Empty, FieldNumber = 0, IsComplexType = IsUserDefinedComplexType(property.Type), // Type metadata IsNullable = IsNullableType(property.Type), IsEnum = IsEnumType(property.Type), IsDecimal = IsDecimalType(property.Type), IsDateTime = IsDateTimeType(property.Type), IsList = IsListOrCollection(property.Type), }; // If it's a list, extract element type info if (propInfo.IsList) { var elementType = GetListElementType(property.Type); if (elementType != null) { propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType); } } // Recursively extract nested properties for complex types else if (propInfo.IsComplexType) { var unwrapped = UnwrapNullableType(property.Type); if (unwrapped is INamedTypeSymbol namedType) { ExtractNestedPropertiesWithTypeInfo(namedType, propInfo.NestedProperties); } } nestedProperties.Add(propInfo); } } private static void GenerateNestedPropertyMapping(StringBuilder sb, List properties, string sourcePrefix, string indent) { foreach (var prop in properties) { var sourcePropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1); if (prop.IsComplexType) { // Generate nested object mapping sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{sourcePropName} != null ? new {prop.FullyQualifiedType}"); sb.AppendLine($"{indent}{{"); GenerateNestedPropertyMapping(sb, prop.NestedProperties, $"{sourcePrefix}.{sourcePropName}", indent + " "); sb.AppendLine($"{indent}}} : null!,"); } else { sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{sourcePropName},"); } } } private static string GeneratePropertyAssignment(PropertyInfo prop, string requestVar, string indent) { var requestPropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1); var source = $"{requestVar}.{requestPropName}"; // Handle lists if (prop.IsList) { if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any()) { // Complex list: map each element return GenerateComplexListMapping(prop, source, indent); } else { // Primitive list: just ToList() return $"{indent}{prop.Name} = {source}?.ToList(),"; } } // Handle enums (proto int32 -> C# enum) if (prop.IsEnum) { return $"{indent}{prop.Name} = ({prop.FullyQualifiedType}){source},"; } // Handle decimals (proto string -> C# decimal) if (prop.IsDecimal) { if (prop.IsNullable) { return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : decimal.Parse({source}),"; } else { return $"{indent}{prop.Name} = decimal.Parse({source}),"; } } // Handle DateTime (proto Timestamp -> C# DateTime) if (prop.IsDateTime) { if (prop.IsNullable) { return $"{indent}{prop.Name} = {source} == null ? (System.DateTime?)null : {source}.ToDateTime(),"; } else { return $"{indent}{prop.Name} = {source}.ToDateTime(),"; } } // Handle complex types (single objects) if (prop.IsComplexType) { return GenerateComplexObjectMapping(prop, source, indent); } // Default: direct assignment return $"{indent}{prop.Name} = {source},"; } private static string GenerateComplexListMapping(PropertyInfo prop, string source, string indent) { var sb = new StringBuilder(); sb.AppendLine($"{indent}{prop.Name} = {source}?.Select(x => new {prop.ElementType}"); sb.AppendLine($"{indent}{{"); foreach (var nestedProp in prop.ElementNestedProperties!) { var nestedSourcePropName = char.ToUpper(nestedProp.Name[0]) + nestedProp.Name.Substring(1); var nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, "x", indent + " "); sb.AppendLine(nestedAssignment); } sb.Append($"{indent}}}).ToList(),"); return sb.ToString(); } private static string GenerateComplexObjectMapping(PropertyInfo prop, string source, string indent) { var sb = new StringBuilder(); sb.AppendLine($"{indent}{prop.Name} = {source} != null ? new {prop.FullyQualifiedType}"); sb.AppendLine($"{indent}{{"); foreach (var nestedProp in prop.NestedProperties) { var nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, source, indent + " "); sb.AppendLine(nestedAssignment); } sb.Append($"{indent}}} : null!,"); return sb.ToString(); } private static string GenerateNestedPropertyAssignment(PropertyInfo prop, string sourceVar, string indent) { var sourcePropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1); var source = $"{sourceVar}.{sourcePropName}"; // Handle enums if (prop.IsEnum) { return $"{indent}{prop.Name} = ({prop.FullyQualifiedType}){source},"; } // Handle decimals if (prop.IsDecimal) { if (prop.IsNullable) { return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : decimal.Parse({source}),"; } else { return $"{indent}{prop.Name} = decimal.Parse({source}),"; } } // Handle lists if (prop.IsList) { return $"{indent}{prop.Name} = {source}?.ToList(),"; } // Handle complex types if (prop.IsComplexType && prop.NestedProperties.Any()) { return GenerateComplexObjectMapping(prop, source, indent); } // Default: direct assignment return $"{indent}{prop.Name} = {source},"; } private static QueryInfo? ExtractQueryInfo(INamedTypeSymbol queryType, INamedTypeSymbol resultType) { var queryInfo = new QueryInfo { Name = queryType.Name, FullyQualifiedName = queryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), Namespace = queryType.ContainingNamespace.ToDisplayString(), Properties = new List() }; // Set result type queryInfo.ResultType = resultType.Name; queryInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); // Use fully qualified names to avoid ambiguity with proto-generated types var queryTypeFullyQualified = queryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var resultTypeFullyQualified = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); queryInfo.HandlerInterfaceName = $"IQueryHandler<{queryTypeFullyQualified}, {resultTypeFullyQualified}>"; // Check if result type is primitive var resultTypeString = resultType.ToDisplayString(); queryInfo.IsResultPrimitiveType = IsPrimitiveType(resultTypeString); // Extract result type properties if it's a complex type if (!queryInfo.IsResultPrimitiveType) { var resultProperties = resultType.GetMembers().OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) .ToList(); foreach (var property in resultProperties) { queryInfo.ResultProperties.Add(new PropertyInfo { Name = property.Name, Type = property.Type.ToDisplayString(), FullyQualifiedType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), ProtoType = string.Empty, // Not needed for result mapping FieldNumber = 0 // Not needed for result mapping }); } } // Extract properties var properties = queryType.GetMembers().OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) .ToList(); int fieldNumber = 1; foreach (var property in properties) { var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional); queryInfo.Properties.Add(new PropertyInfo { Name = property.Name, Type = propertyType, ProtoType = protoType, FieldNumber = fieldNumber++ }); } return queryInfo; } private static DynamicQueryInfo? ExtractDynamicQueryInfo(INamedTypeSymbol sourceType, INamedTypeSymbol destinationType, INamedTypeSymbol? paramsType) { var dynamicQueryInfo = new DynamicQueryInfo { SourceType = sourceType.Name, SourceTypeFullyQualified = sourceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), DestinationType = destinationType.Name, DestinationTypeFullyQualified = destinationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), HasParams = paramsType != null }; if (paramsType != null) { dynamicQueryInfo.ParamsType = paramsType.Name; dynamicQueryInfo.ParamsTypeFullyQualified = paramsType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); } // Pluralize destination type for naming (e.g., User -> Users) dynamicQueryInfo.Name = Pluralize(destinationType.Name); // Build interface names if (paramsType != null) { dynamicQueryInfo.QueryInterfaceName = $"IDynamicQuery<{dynamicQueryInfo.SourceTypeFullyQualified}, {dynamicQueryInfo.DestinationTypeFullyQualified}, {dynamicQueryInfo.ParamsTypeFullyQualified}>"; dynamicQueryInfo.HandlerInterfaceName = $"IQueryHandler<{dynamicQueryInfo.QueryInterfaceName}, IQueryExecutionResult<{dynamicQueryInfo.DestinationTypeFullyQualified}>>"; } else { dynamicQueryInfo.QueryInterfaceName = $"IDynamicQuery<{dynamicQueryInfo.SourceTypeFullyQualified}, {dynamicQueryInfo.DestinationTypeFullyQualified}>"; dynamicQueryInfo.HandlerInterfaceName = $"IQueryHandler<{dynamicQueryInfo.QueryInterfaceName}, IQueryExecutionResult<{dynamicQueryInfo.DestinationTypeFullyQualified}>>"; } return dynamicQueryInfo; } private static string Pluralize(string word) { // Simple pluralization logic - can be enhanced with Pluralize.NET if needed if (word.EndsWith("y") && word.Length > 1 && !"aeiou".Contains(word[word.Length - 2])) return word.Substring(0, word.Length - 1) + "ies"; if (word.EndsWith("s") || word.EndsWith("x") || word.EndsWith("z") || word.EndsWith("ch") || word.EndsWith("sh")) return word + "es"; return word + "s"; } private static void GenerateProtoAndServices(SourceProductionContext context, List commands, List queries, List dynamicQueries, List notifications, 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 service implementations for notifications (streaming) if (notifications.Any()) { var notificationService = GenerateNotificationServiceImpl(notifications, rootNamespace); context.AddSource("NotificationServiceImpl.g.cs", notificationService); } // Generate registration extensions var registrationExtensions = GenerateRegistrationExtensions(commands.Any(), queries.Any(), dynamicQueries.Any(), notifications.Any(), rootNamespace); context.AddSource("GrpcServiceRegistration.g.cs", registrationExtensions); } private static string GenerateCommandMessages(List commands, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using System.Runtime.Serialization;"); sb.AppendLine("using ProtoBuf;"); sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages"); sb.AppendLine("{"); foreach (var command in commands) { // Generate command DTO sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine($" public sealed class {command.Name}Dto"); sb.AppendLine(" {"); foreach (var prop in command.Properties) { sb.AppendLine($" [ProtoMember({prop.FieldNumber})]"); sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]"); sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine(); // Generate result DTO if command has a result if (command.HasResult) { sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine($" public sealed class {command.Name}ResultDto"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine($" public {command.ResultFullyQualifiedName} Result {{ get; set; }}"); sb.AppendLine(" }"); sb.AppendLine(); } } sb.AppendLine("}"); return sb.ToString(); } private static string GenerateQueryMessages(List queries, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using System.Runtime.Serialization;"); sb.AppendLine("using ProtoBuf;"); sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages"); sb.AppendLine("{"); foreach (var query in queries) { // Generate query DTO sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine($" public sealed class {query.Name}Dto"); sb.AppendLine(" {"); foreach (var prop in query.Properties) { sb.AppendLine($" [ProtoMember({prop.FieldNumber})]"); sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]"); sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine(); // Generate result DTO sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine($" public sealed class {query.Name}ResultDto"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine($" public {query.ResultFullyQualifiedName} Result {{ get; set; }}"); sb.AppendLine(" }"); sb.AppendLine(); } sb.AppendLine("}"); return sb.ToString(); } private static string GenerateCommandService(List commands, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using System.ServiceModel;"); sb.AppendLine("using System.Threading;"); sb.AppendLine("using System.Threading.Tasks;"); sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); sb.AppendLine($"using {rootNamespace}.Grpc.Messages;"); sb.AppendLine("using Svrnty.CQRS.Abstractions;"); sb.AppendLine("using ProtoBuf.Grpc;"); sb.AppendLine(); // Generate service interface sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); sb.AppendLine("{"); sb.AppendLine(" [ServiceContract]"); sb.AppendLine(" public interface ICommandService"); sb.AppendLine(" {"); foreach (var command in commands) { if (command.HasResult) { sb.AppendLine($" [OperationContract]"); sb.AppendLine($" Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);"); } else { sb.AppendLine($" [OperationContract]"); sb.AppendLine($" Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);"); } sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine(); // Generate service implementation sb.AppendLine(" public sealed class CommandService : ICommandService"); sb.AppendLine(" {"); sb.AppendLine(" private readonly IServiceProvider _serviceProvider;"); sb.AppendLine(); sb.AppendLine(" public CommandService(IServiceProvider serviceProvider)"); sb.AppendLine(" {"); sb.AppendLine(" _serviceProvider = serviceProvider;"); sb.AppendLine(" }"); sb.AppendLine(); foreach (var command in commands) { if (command.HasResult) { sb.AppendLine($" public async Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)"); sb.AppendLine(" {"); sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();"); sb.AppendLine($" var command = new {command.FullyQualifiedName}"); sb.AppendLine(" {"); foreach (var prop in command.Properties) { sb.AppendLine($" {prop.Name} = request.{prop.Name}!,"); } sb.AppendLine(" };"); sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);"); sb.AppendLine($" return new {command.Name}ResultDto {{ Result = result }};"); sb.AppendLine(" }"); } else { sb.AppendLine($" public async Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)"); sb.AppendLine(" {"); sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();"); sb.AppendLine($" var command = new {command.FullyQualifiedName}"); sb.AppendLine(" {"); foreach (var prop in command.Properties) { sb.AppendLine($" {prop.Name} = request.{prop.Name}!,"); } sb.AppendLine(" };"); sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);"); sb.AppendLine(" }"); } sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static string GenerateQueryService(List queries, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using System.ServiceModel;"); sb.AppendLine("using System.Threading;"); sb.AppendLine("using System.Threading.Tasks;"); sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); sb.AppendLine($"using {rootNamespace}.Grpc.Messages;"); sb.AppendLine("using Svrnty.CQRS.Abstractions;"); sb.AppendLine("using ProtoBuf.Grpc;"); sb.AppendLine(); // Generate service interface sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); sb.AppendLine("{"); sb.AppendLine(" [ServiceContract]"); sb.AppendLine(" public interface IQueryService"); sb.AppendLine(" {"); foreach (var query in queries) { sb.AppendLine($" [OperationContract]"); sb.AppendLine($" Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default);"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine(); // Generate service implementation sb.AppendLine(" public sealed class QueryService : IQueryService"); sb.AppendLine(" {"); sb.AppendLine(" private readonly IServiceProvider _serviceProvider;"); sb.AppendLine(); sb.AppendLine(" public QueryService(IServiceProvider serviceProvider)"); sb.AppendLine(" {"); sb.AppendLine(" _serviceProvider = serviceProvider;"); sb.AppendLine(" }"); sb.AppendLine(); foreach (var query in queries) { sb.AppendLine($" public async Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default)"); sb.AppendLine(" {"); sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();"); sb.AppendLine($" var query = new {query.FullyQualifiedName}"); sb.AppendLine(" {"); foreach (var prop in query.Properties) { sb.AppendLine($" {prop.Name} = request.{prop.Name}!,"); } sb.AppendLine(" };"); sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); sb.AppendLine($" return new {query.Name}ResultDto {{ Result = result }};"); sb.AppendLine(" }"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static string GenerateRegistrationExtensions(bool hasCommands, bool hasQueries, bool hasDynamicQueries, bool hasNotifications, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("using Microsoft.AspNetCore.Builder;"); sb.AppendLine("using Microsoft.AspNetCore.Routing;"); sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); sb.AppendLine($"using {rootNamespace}.Grpc.Services;"); if (hasNotifications) { sb.AppendLine("using Svrnty.CQRS.Notifications.Grpc;"); } sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.Extensions"); sb.AppendLine("{"); sb.AppendLine(" /// "); sb.AppendLine(" /// Auto-generated extension methods for registering and mapping gRPC services"); sb.AppendLine(" /// "); sb.AppendLine(" public static class GrpcServiceRegistrationExtensions"); sb.AppendLine(" {"); if (hasCommands) { sb.AppendLine(" /// "); sb.AppendLine(" /// Registers the auto-generated Command gRPC service"); sb.AppendLine(" /// "); sb.AppendLine(" public static IServiceCollection AddGrpcCommandService(this IServiceCollection services)"); sb.AppendLine(" {"); sb.AppendLine(" services.AddGrpc();"); sb.AppendLine(" services.AddSingleton();"); sb.AppendLine(" return services;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Maps the auto-generated Command gRPC service endpoints"); sb.AppendLine(" /// "); sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommands(this IEndpointRouteBuilder endpoints)"); sb.AppendLine(" {"); sb.AppendLine(" endpoints.MapGrpcService();"); sb.AppendLine(" return endpoints;"); sb.AppendLine(" }"); sb.AppendLine(); } if (hasQueries) { sb.AppendLine(" /// "); sb.AppendLine(" /// Registers the auto-generated Query gRPC service"); sb.AppendLine(" /// "); sb.AppendLine(" public static IServiceCollection AddGrpcQueryService(this IServiceCollection services)"); sb.AppendLine(" {"); sb.AppendLine(" services.AddGrpc();"); sb.AppendLine(" services.AddSingleton();"); sb.AppendLine(" return services;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Maps the auto-generated Query gRPC service endpoints"); sb.AppendLine(" /// "); sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcQueries(this IEndpointRouteBuilder endpoints)"); sb.AppendLine(" {"); sb.AppendLine(" endpoints.MapGrpcService();"); sb.AppendLine(" return endpoints;"); sb.AppendLine(" }"); sb.AppendLine(); } if (hasDynamicQueries) { sb.AppendLine(" /// "); sb.AppendLine(" /// Registers the auto-generated DynamicQuery gRPC service"); sb.AppendLine(" /// "); sb.AppendLine(" public static IServiceCollection AddGrpcDynamicQueryService(this IServiceCollection services)"); sb.AppendLine(" {"); sb.AppendLine(" services.AddGrpc();"); sb.AppendLine(" services.AddSingleton();"); sb.AppendLine(" return services;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Maps the auto-generated DynamicQuery gRPC service endpoints"); sb.AppendLine(" /// "); sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcDynamicQueries(this IEndpointRouteBuilder endpoints)"); sb.AppendLine(" {"); sb.AppendLine(" endpoints.MapGrpcService();"); sb.AppendLine(" return endpoints;"); sb.AppendLine(" }"); sb.AppendLine(); } if (hasNotifications) { sb.AppendLine(" /// "); sb.AppendLine(" /// Registers the auto-generated Notification streaming gRPC service"); sb.AppendLine(" /// "); sb.AppendLine(" public static IServiceCollection AddGrpcNotificationService(this IServiceCollection services)"); sb.AppendLine(" {"); sb.AppendLine(" services.AddGrpc();"); sb.AppendLine(" services.AddStreamingNotifications();"); sb.AppendLine(" services.AddSingleton();"); sb.AppendLine(" return services;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Maps the auto-generated Notification streaming gRPC service endpoints"); sb.AppendLine(" /// "); sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcNotifications(this IEndpointRouteBuilder endpoints)"); sb.AppendLine(" {"); sb.AppendLine(" endpoints.MapGrpcService();"); sb.AppendLine(" return endpoints;"); sb.AppendLine(" }"); sb.AppendLine(); } if (hasCommands || hasQueries || hasDynamicQueries || hasNotifications) { sb.AppendLine(" /// "); sb.AppendLine(" /// Registers all auto-generated gRPC services (Commands, Queries, DynamicQueries, and Notifications)"); sb.AppendLine(" /// "); sb.AppendLine(" public static IServiceCollection AddGrpcCommandsAndQueries(this IServiceCollection services)"); sb.AppendLine(" {"); sb.AppendLine(" services.AddGrpc();"); sb.AppendLine(" services.AddGrpcReflection();"); if (hasCommands) sb.AppendLine(" services.AddSingleton();"); if (hasQueries) sb.AppendLine(" services.AddSingleton();"); if (hasDynamicQueries) sb.AppendLine(" services.AddSingleton();"); if (hasNotifications) { sb.AppendLine(" services.AddStreamingNotifications();"); sb.AppendLine(" services.AddSingleton();"); } sb.AppendLine(" return services;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Maps all auto-generated gRPC service endpoints (Commands, Queries, DynamicQueries, and Notifications)"); sb.AppendLine(" /// "); sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommandsAndQueries(this IEndpointRouteBuilder endpoints)"); sb.AppendLine(" {"); if (hasCommands) sb.AppendLine(" endpoints.MapGrpcService();"); if (hasQueries) sb.AppendLine(" endpoints.MapGrpcService();"); if (hasDynamicQueries) sb.AppendLine(" endpoints.MapGrpcService();"); if (hasNotifications) sb.AppendLine(" endpoints.MapGrpcService();"); sb.AppendLine(" return endpoints;"); sb.AppendLine(" }"); sb.AppendLine(); // Add configuration-based methods sb.AppendLine(" /// "); sb.AppendLine(" /// Registers gRPC services based on configuration"); sb.AppendLine(" /// "); sb.AppendLine(" public static IServiceCollection AddGrpcFromConfiguration(this IServiceCollection services)"); sb.AppendLine(" {"); sb.AppendLine(" var config = services.BuildServiceProvider().GetService();"); sb.AppendLine(" var grpcOptions = config?.GetConfiguration();"); sb.AppendLine(" if (grpcOptions != null)"); sb.AppendLine(" {"); sb.AppendLine(" services.AddGrpc();"); sb.AppendLine(" if (grpcOptions.ShouldEnableReflection)"); sb.AppendLine(" services.AddGrpcReflection();"); sb.AppendLine(); if (hasCommands) { sb.AppendLine(" if (grpcOptions.GetShouldMapCommands())"); sb.AppendLine(" services.AddSingleton();"); } if (hasQueries) { sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); sb.AppendLine(" services.AddSingleton();"); } if (hasDynamicQueries) { sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); sb.AppendLine(" services.AddSingleton();"); } if (hasNotifications) { sb.AppendLine(" // Always register notification service if it exists"); sb.AppendLine(" services.AddStreamingNotifications();"); sb.AppendLine(" services.AddSingleton();"); } sb.AppendLine(" }"); sb.AppendLine(" return services;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Maps gRPC service endpoints based on configuration"); sb.AppendLine(" /// "); sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcFromConfiguration(this IEndpointRouteBuilder endpoints)"); sb.AppendLine(" {"); sb.AppendLine(" var config = endpoints.ServiceProvider.GetService();"); sb.AppendLine(" var grpcOptions = config?.GetConfiguration();"); sb.AppendLine(" if (grpcOptions != null)"); sb.AppendLine(" {"); if (hasCommands) { sb.AppendLine(" if (grpcOptions.GetShouldMapCommands())"); sb.AppendLine(" endpoints.MapGrpcService();"); } if (hasQueries) { sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); sb.AppendLine(" endpoints.MapGrpcService();"); } if (hasDynamicQueries) { sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())"); sb.AppendLine(" endpoints.MapGrpcService();"); } if (hasNotifications) { sb.AppendLine(" // Always map notification service if it exists"); sb.AppendLine(" endpoints.MapGrpcService();"); } sb.AppendLine(); sb.AppendLine(" if (grpcOptions.ShouldEnableReflection)"); sb.AppendLine(" endpoints.MapGrpcReflectionService();"); sb.AppendLine(" }"); sb.AppendLine(" return endpoints;"); sb.AppendLine(" }"); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static string ToCamelCase(string str) { if (string.IsNullOrEmpty(str) || char.IsLower(str[0])) return str; return char.ToLowerInvariant(str[0]) + str.Substring(1); } // New methods for standard gRPC generation private static string GenerateCommandsProto(List commands, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("syntax = \"proto3\";"); sb.AppendLine(); sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";"); sb.AppendLine(); sb.AppendLine("package cqrs;"); sb.AppendLine(); sb.AppendLine("// Command service for CQRS operations"); sb.AppendLine("service CommandService {"); foreach (var command in commands) { var methodName = command.Name.Replace("Command", ""); sb.AppendLine($" // {command.Name}"); sb.AppendLine($" rpc {methodName} ({command.Name}Request) returns ({command.Name}Response);"); } sb.AppendLine("}"); sb.AppendLine(); // Generate message types foreach (var command in commands) { // Request message sb.AppendLine($"message {command.Name}Request {{"); foreach (var prop in command.Properties) { sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};"); } sb.AppendLine("}"); sb.AppendLine(); // Response message sb.AppendLine($"message {command.Name}Response {{"); if (command.HasResult) { sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(command.ResultFullyQualifiedName!, out _, out _)} result = 1;"); } sb.AppendLine("}"); sb.AppendLine(); } return sb.ToString(); } private static string GenerateQueriesProto(List queries, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("syntax = \"proto3\";"); sb.AppendLine(); sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";"); sb.AppendLine(); sb.AppendLine("package cqrs;"); sb.AppendLine(); sb.AppendLine("// Query service for CQRS operations"); sb.AppendLine("service QueryService {"); foreach (var query in queries) { var methodName = query.Name.Replace("Query", ""); sb.AppendLine($" // {query.Name}"); sb.AppendLine($" rpc {methodName} ({query.Name}Request) returns ({query.Name}Response);"); } sb.AppendLine("}"); sb.AppendLine(); // Generate message types foreach (var query in queries) { // Request message sb.AppendLine($"message {query.Name}Request {{"); foreach (var prop in query.Properties) { sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};"); } sb.AppendLine("}"); sb.AppendLine(); // Response message sb.AppendLine($"message {query.Name}Response {{"); sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(query.ResultFullyQualifiedName, out _, out _)} result = 1;"); sb.AppendLine("}"); sb.AppendLine(); } return sb.ToString(); } private static string GenerateCommandServiceImpl(List commands, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using Grpc.Core;"); sb.AppendLine("using System.Threading.Tasks;"); sb.AppendLine("using System.Linq;"); sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); sb.AppendLine("using FluentValidation;"); sb.AppendLine("using Google.Rpc;"); sb.AppendLine("using Google.Protobuf.WellKnownTypes;"); sb.AppendLine($"using {rootNamespace}.Grpc;"); sb.AppendLine("using Svrnty.CQRS.Abstractions;"); sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); sb.AppendLine("{"); sb.AppendLine(" /// "); sb.AppendLine(" /// Auto-generated gRPC service implementation for Commands"); sb.AppendLine(" /// "); sb.AppendLine(" public sealed class CommandServiceImpl : CommandService.CommandServiceBase"); sb.AppendLine(" {"); sb.AppendLine(" private readonly IServiceScopeFactory _scopeFactory;"); sb.AppendLine(); sb.AppendLine(" public CommandServiceImpl(IServiceScopeFactory scopeFactory)"); sb.AppendLine(" {"); sb.AppendLine(" _scopeFactory = scopeFactory;"); 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(" using var scope = _scopeFactory.CreateScope();"); sb.AppendLine(" var serviceProvider = scope.ServiceProvider;"); sb.AppendLine(); sb.AppendLine($" var command = new {command.FullyQualifiedName}"); sb.AppendLine(" {"); foreach (var prop in command.Properties) { var assignment = GeneratePropertyAssignment(prop, "request", " "); sb.AppendLine(assignment); } sb.AppendLine(" };"); sb.AppendLine(); sb.AppendLine(" // Validate command if validator is registered"); sb.AppendLine($" var validator = serviceProvider.GetService>();"); sb.AppendLine(" if (validator != null)"); sb.AppendLine(" {"); sb.AppendLine(" var validationResult = await validator.ValidateAsync(command, context.CancellationToken);"); sb.AppendLine(" if (!validationResult.IsValid)"); sb.AppendLine(" {"); sb.AppendLine(" // Create Rich Error Model with structured field violations"); sb.AppendLine(" var badRequest = new BadRequest();"); sb.AppendLine(" foreach (var error in validationResult.Errors)"); sb.AppendLine(" {"); sb.AppendLine(" badRequest.FieldViolations.Add(new BadRequest.Types.FieldViolation"); sb.AppendLine(" {"); sb.AppendLine(" Field = error.PropertyName,"); sb.AppendLine(" Description = error.ErrorMessage"); sb.AppendLine(" });"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" var status = new Google.Rpc.Status"); sb.AppendLine(" {"); sb.AppendLine(" Code = (int)Code.InvalidArgument,"); sb.AppendLine(" Message = \"Validation failed\","); sb.AppendLine(" Details = { Any.Pack(badRequest) }"); sb.AppendLine(" };"); sb.AppendLine(); sb.AppendLine(" throw status.ToRpcException();"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine($" var handler = serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();"); if (command.HasResult) { sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);"); sb.AppendLine($" return new {responseType} {{ Result = result }};"); } else { sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);"); sb.AppendLine($" return new {responseType}();"); } sb.AppendLine(" }"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static string GenerateQueryServiceImpl(List queries, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using Grpc.Core;"); sb.AppendLine("using System.Threading.Tasks;"); sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); sb.AppendLine($"using {rootNamespace}.Grpc;"); sb.AppendLine("using Svrnty.CQRS.Abstractions;"); sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); sb.AppendLine("{"); sb.AppendLine(" /// "); sb.AppendLine(" /// Auto-generated gRPC service implementation for Queries"); sb.AppendLine(" /// "); sb.AppendLine(" public sealed class QueryServiceImpl : QueryService.QueryServiceBase"); sb.AppendLine(" {"); sb.AppendLine(" private readonly IServiceScopeFactory _scopeFactory;"); sb.AppendLine(); sb.AppendLine(" public QueryServiceImpl(IServiceScopeFactory scopeFactory)"); sb.AppendLine(" {"); sb.AppendLine(" _scopeFactory = scopeFactory;"); 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(" using var scope = _scopeFactory.CreateScope();"); sb.AppendLine(" var serviceProvider = scope.ServiceProvider;"); sb.AppendLine(); sb.AppendLine($" var handler = serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();"); sb.AppendLine($" var query = new {query.FullyQualifiedName}"); sb.AppendLine(" {"); foreach (var prop in query.Properties) { sb.AppendLine($" {prop.Name} = request.{char.ToUpper(prop.Name[0]) + prop.Name.Substring(1)},"); } sb.AppendLine(" };"); sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); // Generate response with mapping if complex type if (query.IsResultPrimitiveType) { sb.AppendLine($" return new {responseType} {{ Result = result }};"); } else { // Complex type - need to map from C# type to proto type sb.AppendLine($" return new {responseType}"); sb.AppendLine(" {"); sb.AppendLine($" Result = new {query.ResultType}"); sb.AppendLine(" {"); foreach (var prop in query.ResultProperties) { sb.AppendLine($" {prop.Name} = result.{prop.Name},"); } sb.AppendLine(" }"); sb.AppendLine(" };"); } sb.AppendLine(" }"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static bool IsPrimitiveType(string typeName) { // Check for common primitive and built-in types var primitiveTypes = new[] { "int", "System.Int32", "long", "System.Int64", "short", "System.Int16", "byte", "System.Byte", "bool", "System.Boolean", "float", "System.Single", "double", "System.Double", "decimal", "System.Decimal", "string", "System.String", "System.DateTime", "System.DateTimeOffset", "System.TimeSpan", "System.Guid" }; return primitiveTypes.Contains(typeName) || typeName.StartsWith("System.Nullable<") || typeName.EndsWith("?"); } private static string GenerateDynamicQueryMessages(List dynamicQueries, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using System.Collections.Generic;"); sb.AppendLine("using System.ServiceModel;"); sb.AppendLine("using System.Runtime.Serialization;"); sb.AppendLine("using ProtoBuf;"); sb.AppendLine("using ProtoBuf.Grpc;"); sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.DynamicQuery"); sb.AppendLine("{"); // Common message types sb.AppendLine(" /// "); sb.AppendLine(" /// Dynamic query filter with support for nested AND/OR logic"); sb.AppendLine(" /// "); sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine(" public sealed class DynamicQueryFilter"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine(" public string Path { get; set; } = string.Empty;"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(2)]"); sb.AppendLine(" [DataMember(Order = 2)]"); sb.AppendLine(" public int Type { get; set; } // Maps to PoweredSoft.DynamicQuery.Core.FilterType"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(3)]"); sb.AppendLine(" [DataMember(Order = 3)]"); sb.AppendLine(" public string Value { get; set; } = string.Empty;"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(4)]"); sb.AppendLine(" [DataMember(Order = 4)]"); sb.AppendLine(" public List? And { get; set; }"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(5)]"); sb.AppendLine(" [DataMember(Order = 5)]"); sb.AppendLine(" public List? Or { get; set; }"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Dynamic query sort"); sb.AppendLine(" /// "); sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine(" public sealed class DynamicQuerySort"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine(" public string Path { get; set; } = string.Empty;"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(2)]"); sb.AppendLine(" [DataMember(Order = 2)]"); sb.AppendLine(" public bool Ascending { get; set; } = true;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Dynamic query group"); sb.AppendLine(" /// "); sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine(" public sealed class DynamicQueryGroup"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine(" public string Path { get; set; } = string.Empty;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// Dynamic query aggregate"); sb.AppendLine(" /// "); sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine(" public sealed class DynamicQueryAggregate"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine(" public string Path { get; set; } = string.Empty;"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(2)]"); sb.AppendLine(" [DataMember(Order = 2)]"); sb.AppendLine(" public int Type { get; set; } // Maps to PoweredSoft.DynamicQuery.Core.AggregateType"); sb.AppendLine(" }"); sb.AppendLine(); // Generate request/response messages for each dynamic query foreach (var dynamicQuery in dynamicQueries) { // Request message sb.AppendLine($" /// "); sb.AppendLine($" /// Request message for dynamic query on {dynamicQuery.Name}"); sb.AppendLine($" /// "); sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine($" public sealed class DynamicQuery{dynamicQuery.Name}Request"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine(" public int Page { get; set; }"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(2)]"); sb.AppendLine(" [DataMember(Order = 2)]"); sb.AppendLine(" public int PageSize { get; set; }"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(3)]"); sb.AppendLine(" [DataMember(Order = 3)]"); sb.AppendLine(" public List Filters { get; set; } = new();"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(4)]"); sb.AppendLine(" [DataMember(Order = 4)]"); sb.AppendLine(" public List Sorts { get; set; } = new();"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(5)]"); sb.AppendLine(" [DataMember(Order = 5)]"); sb.AppendLine(" public List Groups { get; set; } = new();"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(6)]"); sb.AppendLine(" [DataMember(Order = 6)]"); sb.AppendLine(" public List Aggregates { get; set; } = new();"); sb.AppendLine(" }"); sb.AppendLine(); // Response message sb.AppendLine($" /// "); sb.AppendLine($" /// Response message for dynamic query on {dynamicQuery.Name}"); sb.AppendLine($" /// "); sb.AppendLine(" [ProtoContract]"); sb.AppendLine(" [DataContract]"); sb.AppendLine($" public sealed class DynamicQuery{dynamicQuery.Name}Response"); sb.AppendLine(" {"); sb.AppendLine(" [ProtoMember(1)]"); sb.AppendLine(" [DataMember(Order = 1)]"); sb.AppendLine($" public List<{dynamicQuery.DestinationTypeFullyQualified}> Data {{ get; set; }} = new();"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(2)]"); sb.AppendLine(" [DataMember(Order = 2)]"); sb.AppendLine(" public long TotalCount { get; set; }"); sb.AppendLine(); sb.AppendLine(" [ProtoMember(3)]"); sb.AppendLine(" [DataMember(Order = 3)]"); sb.AppendLine(" public int NumberOfPages { get; set; }"); sb.AppendLine(" }"); sb.AppendLine(); } // Generate service interface sb.AppendLine(" /// "); sb.AppendLine(" /// gRPC service interface for DynamicQueries"); sb.AppendLine(" /// "); sb.AppendLine(" [ServiceContract]"); sb.AppendLine(" public interface IDynamicQueryService"); sb.AppendLine(" {"); foreach (var dynamicQuery in dynamicQueries) { var methodName = $"Query{dynamicQuery.Name}"; sb.AppendLine($" /// "); sb.AppendLine($" /// Execute dynamic query on {dynamicQuery.Name}"); sb.AppendLine($" /// "); sb.AppendLine(" [OperationContract]"); sb.AppendLine($" System.Threading.Tasks.Task {methodName}Async(DynamicQuery{dynamicQuery.Name}Request request, CallContext context = default);"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static string GenerateDynamicQueryServiceImpl(List dynamicQueries, string rootNamespace) { var sb = new StringBuilder(); sb.AppendLine("// "); sb.AppendLine("#nullable enable"); sb.AppendLine("using Grpc.Core;"); sb.AppendLine("using System.Threading.Tasks;"); sb.AppendLine("using System.Collections.Generic;"); sb.AppendLine("using System.Linq;"); sb.AppendLine("using Microsoft.Extensions.DependencyInjection;"); sb.AppendLine($"using {rootNamespace}.Grpc;"); sb.AppendLine("using Svrnty.CQRS.Abstractions;"); sb.AppendLine("using Svrnty.CQRS.DynamicQuery.Abstractions;"); sb.AppendLine("using PoweredSoft.DynamicQuery.Core;"); sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); sb.AppendLine("{"); sb.AppendLine(" /// "); sb.AppendLine(" /// Auto-generated gRPC service implementation for DynamicQueries"); sb.AppendLine(" /// "); sb.AppendLine(" public sealed class DynamicQueryServiceImpl : DynamicQueryService.DynamicQueryServiceBase"); sb.AppendLine(" {"); sb.AppendLine(" private readonly IServiceScopeFactory _scopeFactory;"); sb.AppendLine(); sb.AppendLine(" public DynamicQueryServiceImpl(IServiceScopeFactory scopeFactory)"); sb.AppendLine(" {"); sb.AppendLine(" _scopeFactory = scopeFactory;"); 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(" {"); sb.AppendLine(" using var scope = _scopeFactory.CreateScope();"); sb.AppendLine(" var serviceProvider = scope.ServiceProvider;"); sb.AppendLine(); // Build the dynamic query object if (dynamicQuery.HasParams) { sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}, {dynamicQuery.ParamsTypeFullyQualified}>"); } else { sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}>"); } sb.AppendLine(" {"); sb.AppendLine(" Page = request.Page > 0 ? request.Page : null,"); sb.AppendLine(" PageSize = request.PageSize > 0 ? request.PageSize : null,"); sb.AppendLine(" Filters = ConvertFilters(request.Filters),"); sb.AppendLine(" Sorts = ConvertSorts(request.Sorts),"); sb.AppendLine(" Groups = ConvertGroups(request.Groups),"); sb.AppendLine(" Aggregates = ConvertAggregates(request.Aggregates)"); sb.AppendLine(" };"); sb.AppendLine(); // Get the handler and execute sb.AppendLine($" var handler = serviceProvider.GetRequiredService>>();"); sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); sb.AppendLine(); // Build response sb.AppendLine($" var response = new {responseType}"); sb.AppendLine(" {"); sb.AppendLine(" TotalRecords = result.TotalRecords,"); sb.AppendLine(" NumberOfPages = (int)(result.NumberOfPages ?? 0)"); sb.AppendLine(" };"); sb.AppendLine(); sb.AppendLine(" if (result.Data != null)"); sb.AppendLine(" {"); sb.AppendLine(" foreach (var item in result.Data)"); sb.AppendLine(" {"); sb.AppendLine($" response.Data.Add(MapTo{dynamicQuery.Name}ProtoModel(item));"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" // TODO: Add aggregates and groups to response if needed"); sb.AppendLine(); sb.AppendLine(" return response;"); sb.AppendLine(" }"); sb.AppendLine(); } // Add helper methods for converting proto messages to AspNetCore types sb.AppendLine(" private static List? ConvertFilters(Google.Protobuf.Collections.RepeatedField protoFilters)"); sb.AppendLine(" {"); sb.AppendLine(" if (protoFilters == null || protoFilters.Count == 0)"); sb.AppendLine(" return null;"); sb.AppendLine(); sb.AppendLine(" var filters = new List();"); sb.AppendLine(" foreach (var protoFilter in protoFilters)"); sb.AppendLine(" {"); sb.AppendLine(" var filter = new Svrnty.CQRS.DynamicQuery.DynamicQueryFilter"); sb.AppendLine(" {"); sb.AppendLine(" Path = protoFilter.Path,"); sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.FilterType)protoFilter.Type).ToString(),"); sb.AppendLine(" Value = protoFilter.Value"); sb.AppendLine(" };"); sb.AppendLine(); sb.AppendLine(" // Handle nested AND filters"); sb.AppendLine(" if (protoFilter.And != null && protoFilter.And.Count > 0)"); sb.AppendLine(" {"); sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(protoFilter.And);"); sb.AppendLine(" filter.And = true;"); sb.AppendLine(" }"); sb.AppendLine(" // Handle nested OR filters"); sb.AppendLine(" else if (protoFilter.Or != null && protoFilter.Or.Count > 0)"); sb.AppendLine(" {"); sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(protoFilter.Or);"); sb.AppendLine(" filter.And = false;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" filters.Add(filter);"); sb.AppendLine(" }"); sb.AppendLine(" return filters;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" private static List ConvertProtoFiltersToList(Google.Protobuf.Collections.RepeatedField protoFilters)"); sb.AppendLine(" {"); sb.AppendLine(" var result = new List();"); sb.AppendLine(" foreach (var pf in protoFilters)"); sb.AppendLine(" {"); sb.AppendLine(" var filter = new Svrnty.CQRS.DynamicQuery.DynamicQueryFilter"); sb.AppendLine(" {"); sb.AppendLine(" Path = pf.Path,"); sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.FilterType)pf.Type).ToString(),"); sb.AppendLine(" Value = pf.Value"); sb.AppendLine(" };"); sb.AppendLine(" if (pf.And != null && pf.And.Count > 0)"); sb.AppendLine(" {"); sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(pf.And);"); sb.AppendLine(" filter.And = true;"); sb.AppendLine(" }"); sb.AppendLine(" else if (pf.Or != null && pf.Or.Count > 0)"); sb.AppendLine(" {"); sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(pf.Or);"); sb.AppendLine(" filter.And = false;"); sb.AppendLine(" }"); sb.AppendLine(" result.Add(filter);"); sb.AppendLine(" }"); sb.AppendLine(" return result;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" private static List? ConvertSorts(Google.Protobuf.Collections.RepeatedField protoSorts)"); sb.AppendLine(" {"); sb.AppendLine(" if (protoSorts == null || protoSorts.Count == 0)"); sb.AppendLine(" return null;"); sb.AppendLine(); sb.AppendLine(" return protoSorts.Select(s => new PoweredSoft.DynamicQuery.Sort"); sb.AppendLine(" {"); sb.AppendLine(" Path = s.Path,"); sb.AppendLine(" Ascending = s.Ascending"); sb.AppendLine(" }).ToList();"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" private static List? ConvertGroups(Google.Protobuf.Collections.RepeatedField protoGroups)"); sb.AppendLine(" {"); sb.AppendLine(" if (protoGroups == null || protoGroups.Count == 0)"); sb.AppendLine(" return null;"); sb.AppendLine(); sb.AppendLine(" return protoGroups.Select(g => new PoweredSoft.DynamicQuery.Group"); sb.AppendLine(" {"); sb.AppendLine(" Path = g.Path"); sb.AppendLine(" }).ToList();"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" private static List? ConvertAggregates(Google.Protobuf.Collections.RepeatedField protoAggregates)"); sb.AppendLine(" {"); sb.AppendLine(" if (protoAggregates == null || protoAggregates.Count == 0)"); sb.AppendLine(" return null;"); sb.AppendLine(); sb.AppendLine(" return protoAggregates.Select(a => new Svrnty.CQRS.DynamicQuery.DynamicQueryAggregate"); sb.AppendLine(" {"); sb.AppendLine(" Path = a.Path,"); sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.AggregateType)a.Type).ToString()"); sb.AppendLine(" }).ToList();"); sb.AppendLine(" }"); sb.AppendLine(); // Add generic reflection-based mapper helper sb.AppendLine(" private static TProto MapToProtoModel(TDomain domainModel) where TProto : Google.Protobuf.IMessage, new()"); sb.AppendLine(" {"); sb.AppendLine(" if (domainModel == null) return new TProto();"); sb.AppendLine(" var proto = new TProto();"); sb.AppendLine(" var domainProps = typeof(TDomain).GetProperties();"); sb.AppendLine(" var protoDesc = proto.Descriptor;"); sb.AppendLine(); sb.AppendLine(" foreach (var domainProp in domainProps)"); sb.AppendLine(" {"); sb.AppendLine(" // Convert property name to proto field name (PascalCase to snake_case)"); sb.AppendLine(" var protoFieldName = ToSnakeCase(domainProp.Name);"); sb.AppendLine(" var protoField = protoDesc.FindFieldByName(protoFieldName);"); sb.AppendLine(" if (protoField == null) continue;"); sb.AppendLine(); sb.AppendLine(" var domainValue = domainProp.GetValue(domainModel);"); sb.AppendLine(" if (domainValue == null) continue;"); sb.AppendLine(); sb.AppendLine(" var protoAccessor = protoField.Accessor;"); sb.AppendLine(); sb.AppendLine(" // Handle DateTime -> Timestamp conversion"); sb.AppendLine(" if (domainProp.PropertyType == typeof(DateTime) || domainProp.PropertyType == typeof(DateTime?))"); sb.AppendLine(" {"); sb.AppendLine(" var dateTime = (DateTime)domainValue;"); sb.AppendLine(" // Ensure UTC for Timestamp conversion"); sb.AppendLine(" if (dateTime.Kind != DateTimeKind.Utc)"); sb.AppendLine(" dateTime = dateTime.ToUniversalTime();"); sb.AppendLine(" protoAccessor.SetValue(proto, Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(dateTime));"); sb.AppendLine(" }"); sb.AppendLine(" else if (domainProp.PropertyType == typeof(DateTimeOffset) || domainProp.PropertyType == typeof(DateTimeOffset?))"); sb.AppendLine(" {"); sb.AppendLine(" var dateTimeOffset = (DateTimeOffset)domainValue;"); sb.AppendLine(" protoAccessor.SetValue(proto, Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(dateTimeOffset));"); sb.AppendLine(" }"); sb.AppendLine(" // Handle collections (List, IList, etc.) - must check before complex types"); sb.AppendLine(" else if (protoField.IsRepeated && domainValue is System.Collections.IEnumerable enumerable && domainProp.PropertyType != typeof(string))"); sb.AppendLine(" {"); sb.AppendLine(" var repeatedField = protoAccessor.GetValue(proto);"); sb.AppendLine(" if (repeatedField == null) continue;"); sb.AppendLine(); sb.AppendLine(" // Get the element type of the RepeatedField"); sb.AppendLine(" var repeatedFieldType = repeatedField.GetType();"); sb.AppendLine(" var repeatedElementType = repeatedFieldType.IsGenericType ? repeatedFieldType.GetGenericArguments()[0] : null;"); sb.AppendLine(" if (repeatedElementType == null) continue;"); sb.AppendLine(); sb.AppendLine(" // Get Add(T) method with specific parameter type to avoid ambiguity"); sb.AppendLine(" var addMethod = repeatedFieldType.GetMethod(\"Add\", new[] { repeatedElementType });"); sb.AppendLine(" if (addMethod == null) continue;"); sb.AppendLine(); sb.AppendLine(" // Get element types"); sb.AppendLine(" var domainElementType = domainProp.PropertyType.IsArray"); sb.AppendLine(" ? domainProp.PropertyType.GetElementType()"); sb.AppendLine(" : domainProp.PropertyType.IsGenericType ? domainProp.PropertyType.GetGenericArguments()[0] : null;"); sb.AppendLine(" var protoElementType = protoField.MessageType?.ClrType;"); sb.AppendLine(); sb.AppendLine(" foreach (var item in enumerable)"); sb.AppendLine(" {"); sb.AppendLine(" if (item == null) continue;"); sb.AppendLine(); sb.AppendLine(" // Check if elements need mapping (complex types)"); sb.AppendLine(" if (protoElementType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoElementType) && domainElementType != null)"); sb.AppendLine(" {"); sb.AppendLine(" var mapMethod = typeof(DynamicQueryServiceImpl).GetMethod(\"MapToProtoModel\","); sb.AppendLine(" System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!"); sb.AppendLine(" .MakeGenericMethod(domainElementType, protoElementType);"); sb.AppendLine(" var mappedItem = mapMethod.Invoke(null, new[] { item });"); sb.AppendLine(" if (mappedItem != null)"); sb.AppendLine(" addMethod.Invoke(repeatedField, new[] { mappedItem });"); sb.AppendLine(" }"); sb.AppendLine(" else"); sb.AppendLine(" {"); sb.AppendLine(" // Primitive types, enums, strings - add directly"); sb.AppendLine(" try { addMethod.Invoke(repeatedField, new[] { item }); }"); sb.AppendLine(" catch { /* Type mismatch, skip */ }"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(" // Handle nested complex types (non-primitive, non-enum, non-string, non-collection)"); sb.AppendLine(" else if (!domainProp.PropertyType.IsPrimitive && "); sb.AppendLine(" domainProp.PropertyType != typeof(string) && "); sb.AppendLine(" !domainProp.PropertyType.IsEnum &&"); sb.AppendLine(" !domainProp.PropertyType.IsValueType)"); sb.AppendLine(" {"); sb.AppendLine(" // Get the proto field type and recursively map"); sb.AppendLine(" var protoFieldType = protoAccessor.GetValue(proto)?.GetType() ?? protoField.MessageType?.ClrType;"); sb.AppendLine(" if (protoFieldType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoFieldType))"); sb.AppendLine(" {"); sb.AppendLine(" var mapMethod = typeof(DynamicQueryServiceImpl).GetMethod(\"MapToProtoModel\", "); sb.AppendLine(" System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!"); sb.AppendLine(" .MakeGenericMethod(domainProp.PropertyType, protoFieldType);"); sb.AppendLine(" var nestedProto = mapMethod.Invoke(null, new[] { domainValue });"); sb.AppendLine(" if (nestedProto != null)"); sb.AppendLine(" protoAccessor.SetValue(proto, nestedProto);"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(" // Handle decimal -> string conversion"); sb.AppendLine(" else if (domainProp.PropertyType == typeof(decimal) || domainProp.PropertyType == typeof(decimal?))"); sb.AppendLine(" {"); sb.AppendLine(" protoAccessor.SetValue(proto, ((decimal)domainValue).ToString(System.Globalization.CultureInfo.InvariantCulture));"); sb.AppendLine(" }"); sb.AppendLine(" else"); sb.AppendLine(" {"); sb.AppendLine(" // Direct assignment for primitives, strings, enums"); sb.AppendLine(" try { protoAccessor.SetValue(proto, domainValue); }"); sb.AppendLine(" catch { /* Type mismatch, skip */ }"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(" return proto;"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" private static string ToSnakeCase(string str)"); sb.AppendLine(" {"); sb.AppendLine(" if (string.IsNullOrEmpty(str)) return str;"); sb.AppendLine(" var result = new System.Text.StringBuilder();"); sb.AppendLine(" for (int i = 0; i < str.Length; i++)"); sb.AppendLine(" {"); sb.AppendLine(" if (i > 0 && char.IsUpper(str[i]))"); sb.AppendLine(" result.Append('_');"); sb.AppendLine(" result.Append(char.ToLowerInvariant(str[i]));"); sb.AppendLine(" }"); sb.AppendLine(" return result.ToString();"); 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($" return MapToProtoModel<{dynamicQuery.DestinationTypeFullyQualified}, {protoTypeName}>(domainModel);"); sb.AppendLine(" }"); sb.AppendLine(); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } /// /// Discovers types marked with [StreamingNotification] attribute /// private static List DiscoverNotifications(IEnumerable allTypes, Compilation compilation) { var streamingNotificationAttribute = compilation.GetTypeByMetadataName( "Svrnty.CQRS.Notifications.Abstractions.StreamingNotificationAttribute"); if (streamingNotificationAttribute == null) return new List(); var notifications = new List(); foreach (var type in allTypes) { if (type.IsAbstract || type.IsStatic) continue; var attr = type.GetAttributes() .FirstOrDefault(a => SymbolEqualityComparer.Default.Equals( a.AttributeClass, streamingNotificationAttribute)); if (attr == null) continue; // Extract SubscriptionKey from attribute var subscriptionKeyArg = attr.NamedArguments .FirstOrDefault(a => a.Key == "SubscriptionKey"); var subscriptionKeyProp = subscriptionKeyArg.Value.Value as string; if (string.IsNullOrEmpty(subscriptionKeyProp)) continue; // Get all properties of the notification type var properties = new List(); int fieldNumber = 1; foreach (var prop in type.GetMembers().OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public)) { var propType = prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var protoType = ProtoTypeMapper.MapToProtoType(propType, out _, out _); properties.Add(new PropertyInfo { Name = prop.Name, Type = propType, FullyQualifiedType = propType, ProtoType = protoType, FieldNumber = fieldNumber++, IsEnum = prop.Type.TypeKind == TypeKind.Enum, IsDecimal = propType.Contains("decimal") || propType.Contains("Decimal"), IsDateTime = propType.Contains("DateTime") }); } // Find the subscription key property info var keyPropInfo = properties.FirstOrDefault(p => p.Name == subscriptionKeyProp); if (keyPropInfo == null) continue; notifications.Add(new NotificationInfo { Name = type.Name, FullyQualifiedName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), Namespace = type.ContainingNamespace?.ToDisplayString() ?? "", SubscriptionKeyProperty = subscriptionKeyProp, SubscriptionKeyInfo = keyPropInfo, Properties = properties }); } return notifications; } /// /// Generates the NotificationServiceImpl class for streaming notifications /// private static string GenerateNotificationServiceImpl(List notifications, 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.Threading;"); sb.AppendLine("using Google.Protobuf.WellKnownTypes;"); sb.AppendLine($"using {rootNamespace}.Grpc;"); sb.AppendLine("using Svrnty.CQRS.Notifications.Grpc;"); sb.AppendLine(); sb.AppendLine($"namespace {rootNamespace}.Grpc.Services"); sb.AppendLine("{"); sb.AppendLine(" /// "); sb.AppendLine(" /// Auto-generated gRPC service implementation for streaming Notifications"); sb.AppendLine(" /// "); sb.AppendLine(" public sealed class NotificationServiceImpl : NotificationService.NotificationServiceBase"); sb.AppendLine(" {"); sb.AppendLine(" private readonly NotificationSubscriptionManager _subscriptionManager;"); sb.AppendLine(); sb.AppendLine(" public NotificationServiceImpl(NotificationSubscriptionManager subscriptionManager)"); sb.AppendLine(" {"); sb.AppendLine(" _subscriptionManager = subscriptionManager;"); sb.AppendLine(" }"); foreach (var notification in notifications) { var methodName = $"SubscribeTo{notification.Name}"; var requestType = $"SubscribeTo{notification.Name}Request"; var keyPropName = notification.SubscriptionKeyProperty; // Proto uses PascalCase for C# properties var keyPropPascal = ToPascalCaseHelper(ToSnakeCaseHelper(keyPropName)); sb.AppendLine(); sb.AppendLine($" public override async Task {methodName}("); sb.AppendLine($" {requestType} request,"); sb.AppendLine($" IServerStreamWriter<{notification.Name}> responseStream,"); sb.AppendLine(" ServerCallContext context)"); sb.AppendLine(" {"); sb.AppendLine($" // Subscribe with mapper from domain notification to proto message"); sb.AppendLine($" using var subscription = _subscriptionManager.Subscribe<{notification.FullyQualifiedName}, {notification.Name}>("); sb.AppendLine($" request.{keyPropPascal},"); sb.AppendLine($" responseStream,"); sb.AppendLine($" domainNotification => Map{notification.Name}(domainNotification));"); sb.AppendLine(); sb.AppendLine(" // Keep the stream alive until client disconnects"); sb.AppendLine(" try"); sb.AppendLine(" {"); sb.AppendLine(" await Task.Delay(Timeout.Infinite, context.CancellationToken);"); sb.AppendLine(" }"); sb.AppendLine(" catch (OperationCanceledException)"); sb.AppendLine(" {"); sb.AppendLine(" // Client disconnected - normal behavior"); sb.AppendLine(" }"); sb.AppendLine(" }"); } // Generate mapper methods foreach (var notification in notifications) { sb.AppendLine(); sb.AppendLine($" private static {notification.Name} Map{notification.Name}({notification.FullyQualifiedName} domain)"); sb.AppendLine(" {"); sb.AppendLine($" return new {notification.Name}"); sb.AppendLine(" {"); foreach (var prop in notification.Properties) { var protoFieldName = ToPascalCaseHelper(ToSnakeCaseHelper(prop.Name)); if (prop.IsDateTime) { sb.AppendLine($" {protoFieldName} = Timestamp.FromDateTime(domain.{prop.Name}.ToUniversalTime()),"); } else if (prop.IsDecimal) { sb.AppendLine($" {protoFieldName} = domain.{prop.Name}.ToString(),"); } else if (prop.IsEnum) { // Map domain enum to proto enum - get simple type name var simpleTypeName = prop.Type.Replace("global::", "").Split('.').Last(); sb.AppendLine($" {protoFieldName} = ({simpleTypeName})((int)domain.{prop.Name}),"); } else { sb.AppendLine($" {protoFieldName} = domain.{prop.Name},"); } } sb.AppendLine(" };"); sb.AppendLine(" }"); } sb.AppendLine(" }"); sb.AppendLine("}"); return sb.ToString(); } private static string ToSnakeCaseHelper(string str) { if (string.IsNullOrEmpty(str)) return str; var result = new StringBuilder(); for (int i = 0; i < str.Length; i++) { if (i > 0 && char.IsUpper(str[i])) result.Append('_'); result.Append(char.ToLowerInvariant(str[i])); } return result.ToString(); } private static string ToPascalCaseHelper(string snakeCase) { if (string.IsNullOrEmpty(snakeCase)) return snakeCase; var parts = snakeCase.Split('_'); return string.Join("", parts.Select(p => p.Length > 0 ? char.ToUpperInvariant(p[0]) + p.Substring(1).ToLowerInvariant() : "")); } } }