diff --git a/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs b/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs index 3015b1c..057e338 100644 --- a/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs +++ b/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs @@ -350,6 +350,7 @@ namespace Svrnty.CQRS.Grpc.Generators var propInfo = new PropertyInfo { Name = property.Name, + ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(property.Name)), Type = propertyType, FullyQualifiedType = propertyType, ProtoType = protoType, @@ -507,6 +508,7 @@ namespace Svrnty.CQRS.Grpc.Generators var propInfo = new PropertyInfo { Name = property.Name, + ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(property.Name)), Type = propertyType, FullyQualifiedType = propertyType, ProtoType = string.Empty, @@ -536,6 +538,7 @@ namespace Svrnty.CQRS.Grpc.Generators var propInfo = new PropertyInfo { Name = property.Name, + ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(property.Name)), Type = propertyType, FullyQualifiedType = propertyType, ProtoType = string.Empty, @@ -577,26 +580,26 @@ namespace Svrnty.CQRS.Grpc.Generators { foreach (var prop in properties) { - var sourcePropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1); + // Use ProtoPropertyName to access the proto source property if (prop.IsComplexType) { // Generate nested object mapping - sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{sourcePropName} != null ? new {prop.FullyQualifiedType}"); + sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{prop.ProtoPropertyName} != null ? new {prop.FullyQualifiedType}"); sb.AppendLine($"{indent}{{"); - GenerateNestedPropertyMapping(sb, prop.NestedProperties, $"{sourcePrefix}.{sourcePropName}", indent + " "); + GenerateNestedPropertyMapping(sb, prop.NestedProperties, $"{sourcePrefix}.{prop.ProtoPropertyName}", indent + " "); sb.AppendLine($"{indent}}} : null!,"); } else { - sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{sourcePropName},"); + sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{prop.ProtoPropertyName},"); } } } private static string GeneratePropertyAssignment(PropertyInfo prop, string requestVar, string indent) { - var requestPropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1); - var source = $"{requestVar}.{requestPropName}"; + // Use ProtoPropertyName to access the proto request property + var source = $"{requestVar}.{prop.ProtoPropertyName}"; // Handle lists if (prop.IsList) @@ -663,7 +666,7 @@ namespace Svrnty.CQRS.Grpc.Generators foreach (var nestedProp in prop.ElementNestedProperties!) { - var nestedSourcePropName = char.ToUpper(nestedProp.Name[0]) + nestedProp.Name.Substring(1); + // Use ProtoPropertyName for accessing proto source properties var nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, "x", indent + " "); sb.AppendLine(nestedAssignment); } @@ -690,8 +693,8 @@ namespace Svrnty.CQRS.Grpc.Generators private static string GenerateNestedPropertyAssignment(PropertyInfo prop, string sourceVar, string indent) { - var sourcePropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1); - var source = $"{sourceVar}.{sourcePropName}"; + // Use ProtoPropertyName to access the proto source property + var source = $"{sourceVar}.{prop.ProtoPropertyName}"; // Handle enums if (prop.IsEnum) @@ -750,27 +753,112 @@ namespace Svrnty.CQRS.Grpc.Generators var resultTypeString = resultType.ToDisplayString(); queryInfo.IsResultPrimitiveType = IsPrimitiveType(resultTypeString); - // Extract result type properties if it's a complex type - if (!queryInfo.IsResultPrimitiveType) + // 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("?")) { - var resultProperties = resultType.GetMembers().OfType() + 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, - Type = property.Type.ToDisplayString(), - FullyQualifiedType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + 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 + 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 + // Extract properties with type metadata for request mapping var properties = queryType.GetMembers().OfType() .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic) .ToList(); @@ -779,14 +867,30 @@ namespace Svrnty.CQRS.Grpc.Generators 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++ + FieldNumber = fieldNumber++, + IsEnum = isEnum, + IsDecimal = isDecimal, + IsDateTime = isDateTime, + IsNullable = isRequestPropNullable }); } @@ -1579,6 +1683,7 @@ namespace Svrnty.CQRS.Grpc.Generators 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;"); @@ -1617,29 +1722,130 @@ namespace Svrnty.CQRS.Grpc.Generators sb.AppendLine(" {"); foreach (var prop in query.Properties) { - sb.AppendLine($" {prop.Name} = request.{char.ToUpper(prop.Name[0]) + prop.Name.Substring(1)},"); + 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 if complex type + // Generate response with mapping based on result type if (query.IsResultPrimitiveType) { sb.AppendLine($" return new {responseType} {{ Result = result }};"); } - else + else if (query.IsResultCollection) { - // Complex type - need to map from C# type to proto type - sb.AppendLine($" return new {responseType}"); + // 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(" {"); - sb.AppendLine($" Result = new {query.ResultType}"); - sb.AppendLine(" {"); foreach (var prop in query.ResultProperties) { - sb.AppendLine($" {prop.Name} = result.{prop.Name},"); + // 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(" }"); @@ -1672,9 +1878,201 @@ namespace Svrnty.CQRS.Grpc.Generators "System.Guid" }; - return primitiveTypes.Contains(typeName) || - typeName.StartsWith("System.Nullable<") || - typeName.EndsWith("?"); + // 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}"; + } + + // Default: direct assignment (works for compatible types like int, long, string, bool) + return accessor; + } + + /// + /// 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) @@ -2232,6 +2630,7 @@ namespace Svrnty.CQRS.Grpc.Generators properties.Add(new PropertyInfo { Name = prop.Name, + ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(prop.Name)), Type = propType, FullyQualifiedType = propType, ProtoType = protoType, @@ -2382,8 +2781,27 @@ namespace Svrnty.CQRS.Grpc.Generators { if (string.IsNullOrEmpty(snakeCase)) return snakeCase; var parts = snakeCase.Split('_'); - return string.Join("", parts.Select(p => - p.Length > 0 ? char.ToUpperInvariant(p[0]) + p.Substring(1).ToLowerInvariant() : "")); + 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(); } } } diff --git a/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs b/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs index bd9488c..555fa04 100644 --- a/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs +++ b/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs @@ -31,6 +31,11 @@ namespace Svrnty.CQRS.Grpc.Generators.Models public class PropertyInfo { public string Name { get; set; } + /// + /// The property name as generated by Grpc.Tools from the proto field name. + /// This may differ from the C# property name due to casing differences. + /// + public string ProtoPropertyName { get; set; } public string Type { get; set; } public string FullyQualifiedType { get; set; } public string ProtoType { get; set; } @@ -51,6 +56,7 @@ namespace Svrnty.CQRS.Grpc.Generators.Models public PropertyInfo() { Name = string.Empty; + ProtoPropertyName = string.Empty; Type = string.Empty; FullyQualifiedType = string.Empty; ProtoType = string.Empty; diff --git a/Svrnty.CQRS.Grpc.Generators/Models/QueryInfo.cs b/Svrnty.CQRS.Grpc.Generators/Models/QueryInfo.cs index 93924f4..9b8b64d 100644 --- a/Svrnty.CQRS.Grpc.Generators/Models/QueryInfo.cs +++ b/Svrnty.CQRS.Grpc.Generators/Models/QueryInfo.cs @@ -13,6 +13,26 @@ namespace Svrnty.CQRS.Grpc.Generators.Models public string HandlerInterfaceName { get; set; } public List ResultProperties { get; set; } public bool IsResultPrimitiveType { get; set; } + /// + /// True if the result type is a collection (List, IEnumerable, etc.) + /// + public bool IsResultCollection { get; set; } + /// + /// The element type name if IsResultCollection is true + /// + public string ResultElementType { get; set; } + /// + /// The fully qualified element type name if IsResultCollection is true + /// + public string ResultElementTypeFullyQualified { get; set; } + /// + /// True if the result type is nullable (ends with ? or is Nullable) + /// + public bool IsResultNullable { get; set; } + /// + /// The result type name without the nullable annotation (e.g., "CnfFoodDetailItem" instead of "CnfFoodDetailItem?") + /// + public string ResultTypeWithoutNullable { get; set; } public QueryInfo() { @@ -25,6 +45,11 @@ namespace Svrnty.CQRS.Grpc.Generators.Models HandlerInterfaceName = string.Empty; ResultProperties = new List(); IsResultPrimitiveType = false; + IsResultCollection = false; + ResultElementType = string.Empty; + ResultElementTypeFullyQualified = string.Empty; + IsResultNullable = false; + ResultTypeWithoutNullable = string.Empty; } } } diff --git a/Svrnty.CQRS.Grpc.Generators/ProtoFileGenerator.cs b/Svrnty.CQRS.Grpc.Generators/ProtoFileGenerator.cs index 6f3102f..4119d54 100644 --- a/Svrnty.CQRS.Grpc.Generators/ProtoFileGenerator.cs +++ b/Svrnty.CQRS.Grpc.Generators/ProtoFileGenerator.cs @@ -403,9 +403,14 @@ internal class ProtoFileGenerator _messagesBuilder.AppendLine(); // Generate complex type message after closing the response message - if (resultType != null && IsComplexType(resultType)) + // Use GetElementOrUnderlyingType to extract element type from collections (e.g., List -> CnfFoodItem) + if (resultType != null) { - GenerateComplexTypeMessage(resultType as INamedTypeSymbol); + var underlyingType = ProtoFileTypeMapper.GetElementOrUnderlyingType(resultType); + if (IsComplexType(underlyingType) && underlyingType is INamedTypeSymbol namedType) + { + GenerateComplexTypeMessage(namedType); + } } } diff --git a/Svrnty.CQRS.Grpc.Generators/build/Svrnty.CQRS.Grpc.Generators.props b/Svrnty.CQRS.Grpc.Generators/build/Svrnty.CQRS.Grpc.Generators.props index aa8cc45..6ee0e22 100644 --- a/Svrnty.CQRS.Grpc.Generators/build/Svrnty.CQRS.Grpc.Generators.props +++ b/Svrnty.CQRS.Grpc.Generators/build/Svrnty.CQRS.Grpc.Generators.props @@ -2,5 +2,15 @@ $(SvrntyCqrsGrpcGeneratorsVersion) + + + <_SvrntyCqrsGrpcGeneratorsPath Condition="Exists('$(MSBuildThisFileDirectory)..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll + <_SvrntyCqrsGrpcGeneratorsPath Condition="'$(_SvrntyCqrsGrpcGeneratorsPath)' == '' AND Exists('$(MSBuildThisFileDirectory)..\bin\Debug\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)..\bin\Debug\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll + <_SvrntyCqrsGrpcGeneratorsPath Condition="'$(_SvrntyCqrsGrpcGeneratorsPath)' == '' AND Exists('$(MSBuildThisFileDirectory)..\bin\Release\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)..\bin\Release\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll + + + + +