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, ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(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, ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(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, ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(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) { // Use ProtoPropertyName to access the proto source property if (prop.IsComplexType) { // Generate nested object mapping sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{prop.ProtoPropertyName} != null ? new {prop.FullyQualifiedType}"); sb.AppendLine($"{indent}{{"); GenerateNestedPropertyMapping(sb, prop.NestedProperties, $"{sourcePrefix}.{prop.ProtoPropertyName}", indent + " "); sb.AppendLine($"{indent}}} : null!,"); } else { sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{prop.ProtoPropertyName},"); } } } private static string GeneratePropertyAssignment(PropertyInfo prop, string requestVar, string indent) { // Use ProtoPropertyName to access the proto request property var source = $"{requestVar}.{prop.ProtoPropertyName}"; // 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!) { // Use ProtoPropertyName for accessing proto source properties 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) { // Use ProtoPropertyName to access the proto source property var source = $"{sourceVar}.{prop.ProtoPropertyName}"; // 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); // Check if result type is nullable and compute non-nullable name var isNullable = resultType.NullableAnnotation == NullableAnnotation.Annotated || resultTypeString.EndsWith("?"); queryInfo.IsResultNullable = isNullable; // Use the short type name (without namespace) for proto type mapping // This matches how ResultElementType is used for collections var shortTypeName = resultType.Name; if (shortTypeName.EndsWith("?")) { queryInfo.ResultTypeWithoutNullable = shortTypeName.Substring(0, shortTypeName.Length - 1); } else { queryInfo.ResultTypeWithoutNullable = shortTypeName; } // Check if result type is a collection and extract element type var (isCollection, elementType) = GetCollectionElementType(resultType); queryInfo.IsResultCollection = isCollection; // Determine which type to extract properties from INamedTypeSymbol? typeToExtractFrom = null; if (isCollection && elementType != null) { queryInfo.ResultElementType = elementType.Name; queryInfo.ResultElementTypeFullyQualified = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); typeToExtractFrom = elementType; } else if (!queryInfo.IsResultPrimitiveType && resultType is INamedTypeSymbol namedType) { typeToExtractFrom = namedType; } // Extract result type properties if it's a complex type (or collection of complex type) if (typeToExtractFrom != null) { var resultProperties = typeToExtractFrom.GetMembers().OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) .ToList(); foreach (var property in resultProperties) { var propType = property.Type; var propTypeStr = propType.ToDisplayString(); // Check for type categories var isEnum = propType.TypeKind == TypeKind.Enum; var isDecimal = propTypeStr == "decimal" || propTypeStr == "System.Decimal" || propTypeStr == "decimal?" || propTypeStr == "System.Nullable"; var isDateTime = propTypeStr.Contains("DateTime") || propTypeStr.Contains("DateTimeOffset"); var isPropNullable = propType.NullableAnnotation == NullableAnnotation.Annotated || propTypeStr.EndsWith("?") || propTypeStr.StartsWith("System.Nullable<"); var isString = propTypeStr == "string" || propTypeStr == "System.String" || propTypeStr == "string?" || propTypeStr == "System.String?"; // Check if property is a collection var (isPropCollection, propElementType) = GetCollectionElementType(propType); var isElementComplexType = false; List? elementNestedProperties = null; if (isPropCollection && propElementType != null) { isElementComplexType = propElementType.TypeKind != TypeKind.Enum && !IsPrimitiveType(propElementType.ToDisplayString()); if (isElementComplexType) { elementNestedProperties = new List(); ExtractNestedPropertiesWithTypeInfo(propElementType, elementNestedProperties); } } // Check if property itself is a complex type var isComplexType = !isEnum && !isPropCollection && !IsPrimitiveType(propTypeStr.TrimEnd('?')); List? nestedProperties = null; if (isComplexType && propType is INamedTypeSymbol namedPropType) { nestedProperties = new List(); ExtractNestedPropertiesWithTypeInfo(namedPropType, nestedProperties); } queryInfo.ResultProperties.Add(new PropertyInfo { Name = property.Name, ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(property.Name)), Type = propTypeStr, FullyQualifiedType = propType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), ProtoType = string.Empty, // Not needed for result mapping FieldNumber = 0, // Not needed for result mapping IsEnum = isEnum, IsDecimal = isDecimal, IsDateTime = isDateTime, IsNullable = isPropNullable || isString, // Treat strings as nullable for null-coalescing IsList = isPropCollection, IsComplexType = isComplexType, NestedProperties = nestedProperties ?? new List(), ElementType = propElementType?.ToDisplayString(), IsElementComplexType = isElementComplexType, ElementNestedProperties = elementNestedProperties }); } } // Extract properties with type metadata for request mapping 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 propertyTypeShort = property.Type.ToDisplayString(); var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional); // Determine type metadata for proper request mapping var isEnum = property.Type.TypeKind == TypeKind.Enum; var isDecimal = propertyTypeShort == "decimal" || propertyTypeShort == "System.Decimal" || propertyTypeShort == "decimal?" || propertyTypeShort.Contains("Nullable"); var isDateTime = propertyTypeShort.Contains("DateTime") || propertyTypeShort.Contains("DateTimeOffset"); var isRequestPropNullable = property.Type.NullableAnnotation == NullableAnnotation.Annotated || propertyTypeShort.EndsWith("?") || propertyTypeShort.StartsWith("System.Nullable<"); queryInfo.Properties.Add(new PropertyInfo { Name = property.Name, ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(property.Name)), Type = propertyType, FullyQualifiedType = propertyType, ProtoType = protoType, FieldNumber = fieldNumber++, IsEnum = isEnum, IsDecimal = isDecimal, IsDateTime = isDateTime, IsNullable = isRequestPropNullable }); } 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 System.Linq;"); 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) { var requestMapping = GenerateRequestPropertyMapping(prop); sb.AppendLine($" {prop.Name} = {requestMapping},"); } sb.AppendLine(" };"); sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); // Generate response with mapping based on result type if (query.IsResultPrimitiveType) { sb.AppendLine($" return new {responseType} {{ Result = result }};"); } else if (query.IsResultCollection) { // Collection result - use AddRange with LINQ Select for type conversion var elementTypeName = query.ResultElementType; sb.AppendLine($" var response = new {responseType}();"); sb.AppendLine($" response.Result.AddRange(result.Select(item => new {elementTypeName}"); sb.AppendLine(" {"); foreach (var prop in query.ResultProperties) { // Skip collection properties (they need separate AddRange handling, not supported in collection results) if (prop.IsList) continue; var mapping = GeneratePropertyMapping("item", prop); if (mapping != null) { sb.AppendLine($" {prop.ProtoPropertyName} = {mapping},"); } } sb.AppendLine(" }));"); sb.AppendLine(" return response;"); } else { // Single complex object - map properties with type conversions // Use the non-nullable type name for the proto message var protoResultType = query.ResultTypeWithoutNullable; if (query.IsResultNullable) { // Handle nullable result - check for null and return empty response sb.AppendLine(" if (result == null)"); sb.AppendLine(" {"); sb.AppendLine($" return new {responseType}();"); sb.AppendLine(" }"); sb.AppendLine(); } // Separate simple properties from collection properties var simpleProps = query.ResultProperties.Where(p => !p.IsList).ToList(); var collectionProps = query.ResultProperties.Where(p => p.IsList).ToList(); if (collectionProps.Count == 0) { // No collection properties - simple case sb.AppendLine($" return new {responseType}"); sb.AppendLine(" {"); sb.AppendLine($" Result = new {protoResultType}"); sb.AppendLine(" {"); foreach (var prop in simpleProps) { var mapping = GeneratePropertyMapping("result", prop); sb.AppendLine($" {prop.ProtoPropertyName} = {mapping},"); } sb.AppendLine(" }"); sb.AppendLine(" };"); } else { // Has collection properties - need to use AddRange sb.AppendLine($" var protoResult = new {protoResultType}"); sb.AppendLine(" {"); foreach (var prop in simpleProps) { var mapping = GeneratePropertyMapping("result", prop); sb.AppendLine($" {prop.ProtoPropertyName} = {mapping},"); } sb.AppendLine(" };"); sb.AppendLine(); // Add collection properties with AddRange foreach (var prop in collectionProps) { var elementTypeName = prop.ElementType; if (!string.IsNullOrEmpty(elementTypeName)) { var lastDot = elementTypeName.LastIndexOf('.'); if (lastDot >= 0) { elementTypeName = elementTypeName.Substring(lastDot + 1); } } if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Count > 0) { // Complex element type - map properties sb.AppendLine($" if (result.{prop.Name} != null)"); sb.AppendLine(" {"); sb.AppendLine($" protoResult.{prop.ProtoPropertyName}.AddRange(result.{prop.Name}.Select(item => new {elementTypeName}"); sb.AppendLine(" {"); foreach (var nestedProp in prop.ElementNestedProperties) { var nestedMapping = GeneratePropertyMapping("item", nestedProp); if (nestedMapping != null) { sb.AppendLine($" {nestedProp.ProtoPropertyName} = {nestedMapping},"); } } sb.AppendLine(" }));"); sb.AppendLine(" }"); } else { // Simple element type or no nested properties - direct copy sb.AppendLine($" if (result.{prop.Name} != null)"); sb.AppendLine(" {"); sb.AppendLine($" protoResult.{prop.ProtoPropertyName}.AddRange(result.{prop.Name});"); sb.AppendLine(" }"); } sb.AppendLine(); } sb.AppendLine($" return new {responseType} {{ Result = protoResult }};"); } } 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" }; // Handle nullable types - extract the underlying type first var typeToCheck = typeName; if (typeName.EndsWith("?")) { typeToCheck = typeName.Substring(0, typeName.Length - 1); } else if (typeName.StartsWith("System.Nullable<") && typeName.EndsWith(">")) { typeToCheck = typeName.Substring("System.Nullable<".Length, typeName.Length - "System.Nullable<".Length - 1); } return primitiveTypes.Contains(typeToCheck); } /// /// Generates the mapping expression for a property from C# domain type to proto type /// private static string GeneratePropertyMapping(string sourceVar, PropertyInfo prop) { var accessor = $"{sourceVar}.{prop.Name}"; // Handle decimal → string conversion if (prop.IsDecimal) { if (prop.IsNullable) return $"{accessor}?.ToString() ?? \"\""; return $"{accessor}.ToString()"; } // Handle DateTime/DateTimeOffset → string conversion if (prop.IsDateTime) { if (prop.IsNullable) return $"{accessor}?.ToString(\"O\") ?? \"\""; return $"{accessor}.ToString(\"O\")"; } // Handle enum → proto enum conversion (cast with same underlying value) if (prop.IsEnum) { // Extract just the enum name (without namespace) for the proto enum type var enumTypeName = prop.Type; var lastDot = enumTypeName.LastIndexOf('.'); if (lastDot >= 0) { enumTypeName = enumTypeName.Substring(lastDot + 1); } // Remove nullable suffix if present if (enumTypeName.EndsWith("?")) { enumTypeName = enumTypeName.Substring(0, enumTypeName.Length - 1); } return $"({enumTypeName})(int){accessor}"; } // Handle collection properties - these need special handling with AddRange if (prop.IsList) { // Return a special marker indicating this needs AddRange treatment return null; } // Handle nullable strings with null coalescing if (prop.IsNullable && (prop.Type.Contains("string") || prop.Type == "string" || prop.Type == "string?")) { return $"{accessor} ?? \"\""; } // Handle collections (nested repeated fields) if (prop.IsList && prop.IsElementComplexType && prop.ElementNestedProperties != null) { var sb = new StringBuilder(); sb.Append($"new global::Google.Protobuf.Collections.RepeatedField<{prop.ElementType}>()"); // Note: For nested collections, we'd need to generate AddRange call separately // For now, return the accessor - caller should handle complex cases return accessor; } // Handle complex nested types if (prop.IsComplexType && prop.NestedProperties.Count > 0) { var sb = new StringBuilder(); sb.Append($"new {prop.Type}"); sb.AppendLine(); sb.Append(" {"); foreach (var nestedProp in prop.NestedProperties) { var nestedMapping = GeneratePropertyMapping(accessor, nestedProp); sb.AppendLine(); sb.Append($" {nestedProp.ProtoPropertyName} = {nestedMapping},"); } sb.AppendLine(); sb.Append(" }"); return sb.ToString(); } // Default: direct assignment (works for primitives and simple types) return accessor; } /// /// Generates the mapping expression for a request property from proto type to C# domain type /// private static string GenerateRequestPropertyMapping(PropertyInfo prop) { // Use the proto property name (computed from snake_case conversion) var accessor = $"request.{prop.ProtoPropertyName}"; // Handle string → decimal conversion (proto uses string for decimal) if (prop.IsDecimal) { if (prop.IsNullable) return $"string.IsNullOrEmpty({accessor}) ? null : decimal.Parse({accessor})"; return $"decimal.Parse({accessor})"; } // Handle string → DateTime conversion (proto uses string for DateTime) if (prop.IsDateTime) { if (prop.IsNullable) return $"string.IsNullOrEmpty({accessor}) ? null : global::System.DateTime.Parse({accessor})"; return $"global::System.DateTime.Parse({accessor})"; } // Handle proto enum → domain enum conversion if (prop.IsEnum) { // Cast from proto enum (int) to domain enum return $"({prop.Type})(int){accessor}"; } // Handle nullable numeric types (int?, long?, etc.) // In protobuf3, numeric fields default to 0 when not set. // For nullable C# types, we treat 0 as null (unset). if (prop.IsNullable && IsNullableNumericType(prop.Type)) { return $"{accessor} == 0 ? null : {accessor}"; } // Default: direct assignment (works for compatible types like int, long, string, bool) return accessor; } /// /// Checks if the type is a nullable numeric type (int?, long?, short?, etc.) /// private static bool IsNullableNumericType(string typeName) { // Check for common nullable numeric types var nullableNumericTypes = new[] { "int?", "System.Int32?", "global::System.Int32?", "long?", "System.Int64?", "global::System.Int64?", "short?", "System.Int16?", "global::System.Int16?", "byte?", "System.Byte?", "global::System.Byte?", "uint?", "System.UInt32?", "global::System.UInt32?", "ulong?", "System.UInt64?", "global::System.UInt64?", "ushort?", "System.UInt16?", "global::System.UInt16?", "sbyte?", "System.SByte?", "global::System.SByte?", "float?", "System.Single?", "global::System.Single?", "double?", "System.Double?", "global::System.Double?", }; return nullableNumericTypes.Contains(typeName); } /// /// Checks if a type is a collection type and returns the element type if so /// private static (bool IsCollection, INamedTypeSymbol? ElementType) GetCollectionElementType(ITypeSymbol type) { // Check if it's a named type if (type is not INamedTypeSymbol namedType) return (false, null); // Explicitly exclude string (it implements IEnumerable but we don't want to treat it as a collection) var typeDisplayName = type.ToDisplayString(); if (typeDisplayName == "string" || typeDisplayName == "System.String" || typeDisplayName == "string?" || typeDisplayName == "System.String?") { return (false, null); } // Check for common collection types: List, IList, IEnumerable, ICollection, T[] var typeFullName = namedType.OriginalDefinition.ToDisplayString(); // Check for array if (type is IArrayTypeSymbol arrayType) { if (arrayType.ElementType is INamedTypeSymbol arrayElementType) return (true, arrayElementType); return (true, null); } // Collection type patterns to check var collectionPatterns = new[] { "System.Collections.Generic.List", "System.Collections.Generic.IList", "System.Collections.Generic.IEnumerable", "System.Collections.Generic.ICollection", "System.Collections.Generic.IReadOnlyList", "System.Collections.Generic.IReadOnlyCollection" }; foreach (var pattern in collectionPatterns) { if (typeFullName == pattern && namedType.TypeArguments.Length > 0) { var elementType = namedType.TypeArguments[0] as INamedTypeSymbol; return (true, elementType); } } // Also check interfaces for types that implement IEnumerable foreach (var iface in namedType.AllInterfaces) { var ifaceFullName = iface.OriginalDefinition.ToDisplayString(); if (ifaceFullName == "System.Collections.Generic.IEnumerable" && iface.TypeArguments.Length > 0) { var elementType = iface.TypeArguments[0] as INamedTypeSymbol; return (true, elementType); } } return (false, null); } 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, ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(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('_'); var result = new StringBuilder(); foreach (var part in parts) { if (part.Length == 0) continue; // Capitalize the first letter result.Append(char.ToUpperInvariant(part[0])); // Process remaining characters - capitalize letters that follow digits for (int i = 1; i < part.Length; i++) { char c = part[i]; if (i > 0 && char.IsLetter(c) && char.IsDigit(part[i - 1])) { result.Append(char.ToUpperInvariant(c)); } else { result.Append(char.ToLowerInvariant(c)); } } } return result.ToString(); } } }