diff --git a/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs b/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs index 3015b1c..6794fff 100644 --- a/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs +++ b/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs @@ -360,6 +360,8 @@ namespace Svrnty.CQRS.Grpc.Generators IsEnum = IsEnumType(property.Type), IsDecimal = IsDecimalType(property.Type), IsDateTime = IsDateTimeType(property.Type), + IsGuid = IsGuidType(property.Type), + IsJsonElement = IsJsonElementType(property.Type), IsList = IsListOrCollection(property.Type), }; @@ -371,6 +373,7 @@ namespace Svrnty.CQRS.Grpc.Generators { propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType); + propInfo.IsElementGuid = IsGuidType(elementType); // If element is complex, extract nested properties if (propInfo.IsElementComplexType) @@ -467,6 +470,18 @@ namespace Svrnty.CQRS.Grpc.Generators return unwrapped.TypeKind == TypeKind.Enum; } + private static bool IsGuidType(ITypeSymbol type) + { + var unwrapped = UnwrapNullableType(type); + return unwrapped.ToDisplayString() == "System.Guid"; + } + + private static bool IsJsonElementType(ITypeSymbol type) + { + var unwrapped = UnwrapNullableType(type); + return unwrapped.ToDisplayString() == "System.Text.Json.JsonElement"; + } + private static bool IsListOrCollection(ITypeSymbol type) { if (type is IArrayTypeSymbol) @@ -495,6 +510,41 @@ namespace Svrnty.CQRS.Grpc.Generators return null; } + /// + /// Generates the value expression for converting from proto type to C# type + /// + private static string GetProtoToCSharpConversion(PropertyInfo prop, string sourceExpr) + { + if (prop.IsGuid) + { + if (prop.IsNullable) + return $"string.IsNullOrEmpty({sourceExpr}) ? null : System.Guid.Parse({sourceExpr})"; + return $"System.Guid.Parse({sourceExpr})"; + } + if (prop.IsEnum) + { + // Enum is already handled correctly in proto - values match + return $"{sourceExpr}"; + } + // Default: direct assignment + return $"{sourceExpr}!"; + } + + /// + /// Generates the value expression for converting from C# type to proto type + /// + private static string GetCSharpToProtoConversion(PropertyInfo prop, string sourceExpr) + { + if (prop.IsGuid) + { + if (prop.IsNullable) + return $"{sourceExpr}?.ToString() ?? \"\""; + return $"{sourceExpr}.ToString()"; + } + // Default: direct assignment + return sourceExpr; + } + private static void ExtractNestedProperties(INamedTypeSymbol type, List nestedProperties) { var properties = type.GetMembers().OfType() @@ -546,6 +596,8 @@ namespace Svrnty.CQRS.Grpc.Generators IsEnum = IsEnumType(property.Type), IsDecimal = IsDecimalType(property.Type), IsDateTime = IsDateTimeType(property.Type), + IsGuid = IsGuidType(property.Type), + IsJsonElement = IsJsonElementType(property.Type), IsList = IsListOrCollection(property.Type), }; @@ -557,6 +609,7 @@ namespace Svrnty.CQRS.Grpc.Generators { propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType); + propInfo.IsElementGuid = IsGuidType(elementType); } } // Recursively extract nested properties for complex types @@ -606,6 +659,11 @@ namespace Svrnty.CQRS.Grpc.Generators // Complex list: map each element return GenerateComplexListMapping(prop, source, indent); } + else if (prop.IsElementGuid) + { + // List from proto -> List in C# + return $"{indent}{prop.Name} = {source}?.Select(x => System.Guid.Parse(x)).ToList(),"; + } else { // Primitive list: just ToList() @@ -645,6 +703,19 @@ namespace Svrnty.CQRS.Grpc.Generators } } + // Handle Guid (proto string -> C# Guid) + if (prop.IsGuid) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : System.Guid.Parse({source}),"; + } + else + { + return $"{indent}{prop.Name} = System.Guid.Parse({source}),"; + } + } + // Handle complex types (single objects) if (prop.IsComplexType) { @@ -712,6 +783,19 @@ namespace Svrnty.CQRS.Grpc.Generators } } + // Handle Guid + if (prop.IsGuid) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : System.Guid.Parse({source}),"; + } + else + { + return $"{indent}{prop.Name} = System.Guid.Parse({source}),"; + } + } + // Handle lists if (prop.IsList) { @@ -728,6 +812,221 @@ namespace Svrnty.CQRS.Grpc.Generators return $"{indent}{prop.Name} = {source},"; } + /// + /// Generates C# to proto property mapping (reverse of GeneratePropertyAssignment) + /// + private static string GenerateResultPropertyMapping(PropertyInfo prop, string sourceVar, string indent) + { + var source = $"{sourceVar}.{prop.Name}"; + + // Handle lists + if (prop.IsList) + { + if (prop.IsElementComplexType) + { + // Complex list: map each element to proto type + return GenerateResultComplexListMapping(prop, source, indent); + } + else if (prop.IsElementGuid) + { + // List -> repeated string + return $"{indent}{prop.Name} = {{ {source}?.Select(x => x.ToString()) ?? Enumerable.Empty() }},"; + } + else + { + // Primitive list: just copy + return $"{indent}{prop.Name} = {{ {source} ?? Enumerable.Empty<{prop.Type.Replace("System.Collections.Generic.List<", "").Replace(">", "").Replace("?", "")}>() }},"; + } + } + + // Handle Guid (C# Guid -> proto string) + if (prop.IsGuid) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,"; + } + else + { + return $"{indent}{prop.Name} = {source}.ToString(),"; + } + } + + // Handle decimals (C# decimal -> proto string) + if (prop.IsDecimal) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,"; + } + else + { + return $"{indent}{prop.Name} = {source}.ToString(),"; + } + } + + // Handle DateTime (C# DateTime -> proto Timestamp) + if (prop.IsDateTime) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}.HasValue ? Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(System.DateTime.SpecifyKind({source}.Value, System.DateTimeKind.Utc)) : null,"; + } + else + { + return $"{indent}{prop.Name} = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(System.DateTime.SpecifyKind({source}, System.DateTimeKind.Utc)),"; + } + } + + // Handle TimeSpan (C# TimeSpan -> proto Duration) + if (prop.FullyQualifiedType.Contains("System.TimeSpan")) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}.HasValue ? Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan({source}.Value) : null,"; + } + else + { + return $"{indent}{prop.Name} = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan({source}),"; + } + } + + // Handle enums (C# enum -> proto int32) + if (prop.IsEnum) + { + return $"{indent}{prop.Name} = (int){source},"; + } + + // Handle complex types (single objects) + if (prop.IsComplexType) + { + return GenerateResultComplexObjectMapping(prop, source, indent); + } + + // Default: direct assignment (strings, ints, bools, etc.) + if (prop.IsNullable && prop.Type.Contains("string")) + { + return $"{indent}{prop.Name} = {source} ?? string.Empty,"; + } + return $"{indent}{prop.Name} = {source},"; + } + + private static string GenerateResultComplexListMapping(PropertyInfo prop, string source, string indent) + { + var sb = new StringBuilder(); + var protoElementType = prop.ElementType?.Split('.').Last() ?? prop.Type; + sb.AppendLine($"{indent}{prop.Name} = {{"); + sb.AppendLine($"{indent} {source}?.Select(x => new {protoElementType}"); + sb.AppendLine($"{indent} {{"); + + if (prop.ElementNestedProperties != null) + { + foreach (var nestedProp in prop.ElementNestedProperties) + { + var nestedAssignment = GenerateResultNestedPropertyMapping(nestedProp, "x", indent + " "); + sb.AppendLine(nestedAssignment); + } + } + + sb.AppendLine($"{indent} }}) ?? Enumerable.Empty<{protoElementType}>()"); + sb.Append($"{indent}}},"); + return sb.ToString(); + } + + private static string GenerateResultComplexObjectMapping(PropertyInfo prop, string source, string indent) + { + var sb = new StringBuilder(); + var protoType = prop.Type.Split('.').Last().Replace("?", ""); + sb.AppendLine($"{indent}{prop.Name} = {source} != null ? new {protoType}"); + sb.AppendLine($"{indent}{{"); + + foreach (var nestedProp in prop.NestedProperties) + { + var nestedAssignment = GenerateResultNestedPropertyMapping(nestedProp, source, indent + " "); + sb.AppendLine(nestedAssignment); + } + + sb.Append($"{indent}}} : null,"); + return sb.ToString(); + } + + private static string GenerateResultNestedPropertyMapping(PropertyInfo prop, string sourceVar, string indent) + { + var source = $"{sourceVar}.{prop.Name}"; + + // Handle Guid + if (prop.IsGuid) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,"; + } + else + { + return $"{indent}{prop.Name} = {source}.ToString(),"; + } + } + + // Handle decimals + if (prop.IsDecimal) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,"; + } + else + { + return $"{indent}{prop.Name} = {source}.ToString(),"; + } + } + + // Handle enums + if (prop.IsEnum) + { + return $"{indent}{prop.Name} = (int){source},"; + } + + // Handle lists + if (prop.IsList) + { + if (prop.IsElementGuid) + { + return $"{indent}{prop.Name} = {{ {source}?.Select(x => x.ToString()) ?? Enumerable.Empty() }},"; + } + else if (prop.IsElementComplexType) + { + // Complex list elements need mapping - but we don't have nested property info here + // Fall back to creating empty proto objects (the user needs to ensure types are compatible) + var elementTypeName = prop.ElementType?.Split('.').Last() ?? "object"; + return $"{indent}{prop.Name} = {{ {source}?.Select(x => new {elementTypeName}()) ?? Enumerable.Empty<{elementTypeName}>() }},"; + } + return $"{indent}{prop.Name} = {{ {source} }},"; + } + + // Handle complex types (non-list) + if (prop.IsComplexType) + { + var typeName = prop.Type.Split('.').Last().Replace("?", ""); + if (prop.IsNullable || prop.Type.EndsWith("?")) + { + return $"{indent}{prop.Name} = {source} != null ? new {typeName}() : null,"; + } + else + { + return $"{indent}{prop.Name} = new {typeName}(),"; + } + } + + // Handle nullable strings + if (prop.IsNullable && prop.Type.Contains("string")) + { + return $"{indent}{prop.Name} = {source} ?? string.Empty,"; + } + + // Default: direct assignment + return $"{indent}{prop.Name} = {source},"; + } + private static QueryInfo? ExtractQueryInfo(INamedTypeSymbol queryType, INamedTypeSymbol resultType) { var queryInfo = new QueryInfo @@ -759,14 +1058,46 @@ namespace Svrnty.CQRS.Grpc.Generators foreach (var property in resultProperties) { - queryInfo.ResultProperties.Add(new PropertyInfo + var propInfo = new PropertyInfo { Name = property.Name, Type = property.Type.ToDisplayString(), FullyQualifiedType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - ProtoType = string.Empty, // Not needed for result mapping - FieldNumber = 0 // Not needed for result mapping - }); + ProtoType = string.Empty, + FieldNumber = 0, + IsNullable = property.Type.NullableAnnotation == NullableAnnotation.Annotated || + (property.Type is INamedTypeSymbol nt && nt.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T), + IsEnum = IsEnumType(property.Type), + IsDecimal = IsDecimalType(property.Type), + IsDateTime = IsDateTimeType(property.Type), + IsGuid = IsGuidType(property.Type), + IsJsonElement = IsJsonElementType(property.Type), + IsList = IsListOrCollection(property.Type), + IsComplexType = IsUserDefinedComplexType(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); + propInfo.IsElementGuid = IsGuidType(elementType); + } + } + // 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); + } + } + + queryInfo.ResultProperties.Add(propInfo); } } @@ -781,13 +1112,46 @@ namespace Svrnty.CQRS.Grpc.Generators var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional); - queryInfo.Properties.Add(new PropertyInfo + var propInfo = new PropertyInfo { Name = property.Name, Type = propertyType, + FullyQualifiedType = propertyType, ProtoType = protoType, - FieldNumber = fieldNumber++ - }); + FieldNumber = fieldNumber++, + IsNullable = property.Type.NullableAnnotation == NullableAnnotation.Annotated || + (property.Type is INamedTypeSymbol nt && nt.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T), + IsEnum = IsEnumType(property.Type), + IsDecimal = IsDecimalType(property.Type), + IsDateTime = IsDateTimeType(property.Type), + IsGuid = IsGuidType(property.Type), + IsJsonElement = IsJsonElementType(property.Type), + IsList = IsListOrCollection(property.Type), + IsComplexType = IsUserDefinedComplexType(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); + propInfo.IsElementGuid = IsGuidType(elementType); + } + } + // 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); + } + } + + queryInfo.Properties.Add(propInfo); } return queryInfo; @@ -1032,7 +1396,8 @@ namespace Svrnty.CQRS.Grpc.Generators sb.AppendLine(" {"); foreach (var prop in command.Properties) { - sb.AppendLine($" {prop.Name} = request.{prop.Name}!,"); + var conversion = GetProtoToCSharpConversion(prop, $"request.{prop.Name}"); + sb.AppendLine($" {prop.Name} = {conversion},"); } sb.AppendLine(" };"); sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);"); @@ -1048,7 +1413,8 @@ namespace Svrnty.CQRS.Grpc.Generators sb.AppendLine(" {"); foreach (var prop in command.Properties) { - sb.AppendLine($" {prop.Name} = request.{prop.Name}!,"); + var conversion = GetProtoToCSharpConversion(prop, $"request.{prop.Name}"); + sb.AppendLine($" {prop.Name} = {conversion},"); } sb.AppendLine(" };"); sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);"); @@ -1114,7 +1480,8 @@ namespace Svrnty.CQRS.Grpc.Generators sb.AppendLine(" {"); foreach (var prop in query.Properties) { - sb.AppendLine($" {prop.Name} = request.{prop.Name}!,"); + var conversion = GetProtoToCSharpConversion(prop, $"request.{prop.Name}"); + sb.AppendLine($" {prop.Name} = {conversion},"); } sb.AppendLine(" };"); sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); @@ -1554,7 +1921,15 @@ namespace Svrnty.CQRS.Grpc.Generators if (command.HasResult) { sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);"); - sb.AppendLine($" return new {responseType} {{ Result = result }};"); + // Handle Guid result conversion + if (command.ResultFullyQualifiedName?.Contains("System.Guid") == true) + { + sb.AppendLine($" return new {responseType} {{ Result = result.ToString() }};"); + } + else + { + sb.AppendLine($" return new {responseType} {{ Result = result }};"); + } } else { @@ -1617,7 +1992,8 @@ 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 assignment = GeneratePropertyAssignment(prop, "request", " "); + sb.AppendLine(assignment); } sb.AppendLine(" };"); sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);"); @@ -1625,18 +2001,31 @@ namespace Svrnty.CQRS.Grpc.Generators // Generate response with mapping if complex type if (query.IsResultPrimitiveType) { - sb.AppendLine($" return new {responseType} {{ Result = result }};"); + // Handle primitive type result conversion (e.g., Guid.ToString()) + if (query.ResultFullyQualifiedName?.Contains("System.Guid") == true) + { + sb.AppendLine($" return new {responseType} {{ Result = result.ToString() }};"); + } + else + { + sb.AppendLine($" return new {responseType} {{ Result = result }};"); + } } else { // Complex type - need to map from C# type to proto type + sb.AppendLine($" if (result == null)"); + sb.AppendLine($" {{"); + sb.AppendLine($" return new {responseType}();"); + sb.AppendLine($" }}"); sb.AppendLine($" return new {responseType}"); sb.AppendLine(" {"); sb.AppendLine($" Result = new {query.ResultType}"); sb.AppendLine(" {"); foreach (var prop in query.ResultProperties) { - sb.AppendLine($" {prop.Name} = result.{prop.Name},"); + var assignment = GenerateResultPropertyMapping(prop, "result", " "); + sb.AppendLine(assignment); } sb.AppendLine(" }"); sb.AppendLine(" };"); @@ -1672,9 +2061,23 @@ namespace Svrnty.CQRS.Grpc.Generators "System.Guid" }; - return primitiveTypes.Contains(typeName) || - typeName.StartsWith("System.Nullable<") || - typeName.EndsWith("?"); + if (primitiveTypes.Contains(typeName)) + return true; + + // Handle nullable types - check if the underlying type is primitive + if (typeName.EndsWith("?")) + { + var underlyingType = typeName.Substring(0, typeName.Length - 1); + return IsPrimitiveType(underlyingType); + } + + if (typeName.StartsWith("System.Nullable<") && typeName.EndsWith(">")) + { + var underlyingType = typeName.Substring("System.Nullable<".Length, typeName.Length - "System.Nullable<".Length - 1); + return IsPrimitiveType(underlyingType); + } + + return false; } private static string GenerateDynamicQueryMessages(List dynamicQueries, string rootNamespace) @@ -1910,10 +2313,10 @@ namespace Svrnty.CQRS.Grpc.Generators 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(" Filters = ConvertFilters(request.Filters) ?? new(),"); + sb.AppendLine(" Sorts = ConvertSorts(request.Sorts) ?? new(),"); + sb.AppendLine(" Groups = ConvertGroups(request.Groups) ?? new(),"); + sb.AppendLine(" Aggregates = ConvertAggregates(request.Aggregates) ?? new()"); sb.AppendLine(" };"); sb.AppendLine(); @@ -2143,6 +2546,11 @@ namespace Svrnty.CQRS.Grpc.Generators sb.AppendLine(" {"); sb.AppendLine(" protoAccessor.SetValue(proto, ((decimal)domainValue).ToString(System.Globalization.CultureInfo.InvariantCulture));"); sb.AppendLine(" }"); + sb.AppendLine(" // Handle Guid -> string conversion"); + sb.AppendLine(" else if (domainProp.PropertyType == typeof(Guid) || domainProp.PropertyType == typeof(Guid?))"); + sb.AppendLine(" {"); + sb.AppendLine(" protoAccessor.SetValue(proto, ((Guid)domainValue).ToString());"); + sb.AppendLine(" }"); sb.AppendLine(" else"); sb.AppendLine(" {"); sb.AppendLine(" // Direct assignment for primitives, strings, enums"); @@ -2252,7 +2660,7 @@ namespace Svrnty.CQRS.Grpc.Generators Name = type.Name, FullyQualifiedName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), Namespace = type.ContainingNamespace?.ToDisplayString() ?? "", - SubscriptionKeyProperty = subscriptionKeyProp, + SubscriptionKeyProperty = subscriptionKeyProp!, // Already validated as non-null above SubscriptionKeyInfo = keyPropInfo, Properties = properties }); @@ -2343,6 +2751,10 @@ namespace Svrnty.CQRS.Grpc.Generators { sb.AppendLine($" {protoFieldName} = domain.{prop.Name}.ToString(),"); } + else if (prop.IsGuid) + { + sb.AppendLine($" {protoFieldName} = domain.{prop.Name}.ToString(),"); + } else if (prop.IsEnum) { // Map domain enum to proto enum - get simple type name diff --git a/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs b/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs index bd9488c..36e1943 100644 --- a/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs +++ b/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs @@ -44,8 +44,11 @@ namespace Svrnty.CQRS.Grpc.Generators.Models public bool IsNullable { get; set; } public bool IsDecimal { get; set; } public bool IsDateTime { get; set; } + public bool IsGuid { get; set; } + public bool IsJsonElement { get; set; } public string? ElementType { get; set; } public bool IsElementComplexType { get; set; } + public bool IsElementGuid { get; set; } public List? ElementNestedProperties { get; set; } public PropertyInfo() @@ -61,7 +64,10 @@ namespace Svrnty.CQRS.Grpc.Generators.Models IsNullable = false; IsDecimal = false; IsDateTime = false; + IsGuid = false; + IsJsonElement = false; IsElementComplexType = false; + IsElementGuid = false; } } } diff --git a/Svrnty.CQRS.Grpc.Generators/ProtoFileGenerator.cs b/Svrnty.CQRS.Grpc.Generators/ProtoFileGenerator.cs index 6f3102f..e6bdd73 100644 --- a/Svrnty.CQRS.Grpc.Generators/ProtoFileGenerator.cs +++ b/Svrnty.CQRS.Grpc.Generators/ProtoFileGenerator.cs @@ -737,7 +737,7 @@ internal class ProtoFileGenerator FullyQualifiedName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) .Replace("global::", ""), Namespace = type.ContainingNamespace?.ToDisplayString() ?? "", - SubscriptionKeyProperty = subscriptionKeyProp, + SubscriptionKeyProperty = subscriptionKeyProp!, // Already validated as non-null above SubscriptionKeyInfo = keyPropInfo, Properties = properties }); @@ -817,14 +817,16 @@ internal class ProtoFileGenerator foreach (var prop in notification.Properties) { - var protoType = ProtoFileTypeMapper.MapType( - _compilation.GetTypeByMetadataName(prop.FullyQualifiedType) ?? - GetTypeFromName(prop.FullyQualifiedType), - out var needsImport, out var importPath); + var typeSymbol = _compilation.GetTypeByMetadataName(prop.FullyQualifiedType) ?? + GetTypeFromName(prop.FullyQualifiedType); - if (needsImport && importPath != null) + if (typeSymbol != null) { - _requiredImports.Add(importPath); + ProtoFileTypeMapper.MapType(typeSymbol, out var needsImport, out var importPath); + if (needsImport && importPath != null) + { + _requiredImports.Add(importPath); + } } var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name); diff --git a/Svrnty.CQRS.Grpc.Generators/ProtoTypeMapper.cs b/Svrnty.CQRS.Grpc.Generators/ProtoTypeMapper.cs index 14cfaee..a0d26f8 100644 --- a/Svrnty.CQRS.Grpc.Generators/ProtoTypeMapper.cs +++ b/Svrnty.CQRS.Grpc.Generators/ProtoTypeMapper.cs @@ -20,6 +20,54 @@ internal static class ProtoFileTypeMapper // Note: NullableAnnotation.Annotated is for reference type nullability (List?, string?, etc.) // We don't unwrap these - just use the underlying type. Nullable value types are handled later. + // Handle Nullable value types (e.g., int?, decimal?, enum?) FIRST + if (typeSymbol is INamedTypeSymbol nullableType && + nullableType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && + nullableType.TypeArguments.Length == 1) + { + // Unwrap the nullable and map the inner type + return MapType(nullableType.TypeArguments[0], out needsImport, out importPath); + } + + // Handle collections BEFORE basic type checks (to avoid matching List as Guid) + if (typeSymbol is INamedTypeSymbol collectionType) + { + // List, IEnumerable, Array, ICollection etc. (but not Nullable) + var collectionTypeName = collectionType.Name; + if (collectionType.TypeArguments.Length == 1 && + (collectionTypeName.Contains("List") || collectionTypeName.Contains("Collection") || + collectionTypeName.Contains("Enumerable") || collectionTypeName.Contains("Array") || + collectionTypeName.Contains("Set") || collectionTypeName.Contains("IList") || + collectionTypeName.Contains("ICollection") || collectionTypeName.Contains("IEnumerable"))) + { + var elementType = collectionType.TypeArguments[0]; + var protoElementType = MapType(elementType, out needsImport, out importPath); + return $"repeated {protoElementType}"; + } + + // Dictionary + if (collectionType.TypeArguments.Length == 2 && + (collectionTypeName.Contains("Dictionary") || collectionTypeName.Contains("IDictionary"))) + { + var keyType = MapType(collectionType.TypeArguments[0], out var keyNeedsImport, out var keyImportPath); + var valueType = MapType(collectionType.TypeArguments[1], out var valueNeedsImport, out var valueImportPath); + + // Set import flags if either key or value needs imports + if (keyNeedsImport) + { + needsImport = true; + importPath = keyImportPath; + } + if (valueNeedsImport) + { + needsImport = true; + importPath = valueImportPath; // Note: This only captures last import, may need improvement + } + + return $"map<{keyType}, {valueType}>"; + } + } + // Basic types switch (typeName) { @@ -49,81 +97,25 @@ internal static class ProtoFileTypeMapper return "double"; case "Byte[]": return "bytes"; - } - - // Special types that need imports - if (fullTypeName.Contains("System.DateTime")) - { - needsImport = true; - importPath = "google/protobuf/timestamp.proto"; - return "google.protobuf.Timestamp"; - } - - if (fullTypeName.Contains("System.TimeSpan")) - { - needsImport = true; - importPath = "google/protobuf/duration.proto"; - return "google.protobuf.Duration"; - } - - if (fullTypeName.Contains("System.Guid")) - { - // Guid serialized as string - return "string"; - } - - if (fullTypeName.Contains("System.Decimal") || typeName == "Decimal" || fullTypeName == "decimal") - { - // Decimal serialized as string (no native decimal in proto) - return "string"; - } - - // Handle Nullable value types (e.g., int?, decimal?, enum?) - if (typeSymbol is INamedTypeSymbol nullableType && - nullableType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && - nullableType.TypeArguments.Length == 1) - { - // Unwrap the nullable and map the inner type - return MapType(nullableType.TypeArguments[0], out needsImport, out importPath); - } - - // Collections - if (typeSymbol is INamedTypeSymbol collectionType) - { - // List, IEnumerable, Array, ICollection etc. (but not Nullable) - var typeName2 = collectionType.Name; - if (collectionType.TypeArguments.Length == 1 && - (typeName2.Contains("List") || typeName2.Contains("Collection") || - typeName2.Contains("Enumerable") || typeName2.Contains("Array") || - typeName2.Contains("Set") || typeName2.Contains("IList") || - typeName2.Contains("ICollection") || typeName2.Contains("IEnumerable"))) - { - var elementType = collectionType.TypeArguments[0]; - var protoElementType = MapType(elementType, out needsImport, out importPath); - return $"repeated {protoElementType}"; - } - - // Dictionary - if (collectionType.TypeArguments.Length == 2 && - (typeName.Contains("Dictionary") || typeName.Contains("IDictionary"))) - { - var keyType = MapType(collectionType.TypeArguments[0], out var keyNeedsImport, out var keyImportPath); - var valueType = MapType(collectionType.TypeArguments[1], out var valueNeedsImport, out var valueImportPath); - - // Set import flags if either key or value needs imports - if (keyNeedsImport) - { - needsImport = true; - importPath = keyImportPath; - } - if (valueNeedsImport) - { - needsImport = true; - importPath = valueImportPath; // Note: This only captures last import, may need improvement - } - - return $"map<{keyType}, {valueType}>"; - } + case "Guid": + // Guid serialized as string + return "string"; + case "Decimal": + // Decimal serialized as string (no native decimal in proto) + return "string"; + case "DateTime": + case "DateTimeOffset": + needsImport = true; + importPath = "google/protobuf/timestamp.proto"; + return "google.protobuf.Timestamp"; + case "TimeSpan": + needsImport = true; + importPath = "google/protobuf/duration.proto"; + return "google.protobuf.Duration"; + case "JsonElement": + needsImport = true; + importPath = "google/protobuf/struct.proto"; + return "google.protobuf.Struct"; } // Enums