fix: add Guid to string conversion in gRPC source generator
The MapToProtoModel function was silently failing when mapping Guid properties to proto string fields, causing IDs to be empty in gRPC responses. Added explicit Guid → string conversion handling. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9b9e2cbdbe
commit
f76dbb1a97
@ -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the value expression for converting from proto type to C# type
|
||||
/// </summary>
|
||||
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}!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the value expression for converting from C# type to proto type
|
||||
/// </summary>
|
||||
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<PropertyInfo> nestedProperties)
|
||||
{
|
||||
var properties = type.GetMembers().OfType<IPropertySymbol>()
|
||||
@ -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<string> from proto -> List<Guid> 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},";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates C# to proto property mapping (reverse of GeneratePropertyAssignment)
|
||||
/// </summary>
|
||||
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<Guid> -> repeated string
|
||||
return $"{indent}{prop.Name} = {{ {source}?.Select(x => x.ToString()) ?? Enumerable.Empty<string>() }},";
|
||||
}
|
||||
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<string>() }},";
|
||||
}
|
||||
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<DynamicQueryInfo> 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
|
||||
|
||||
@ -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<PropertyInfo>? 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -20,6 +20,54 @@ internal static class ProtoFileTypeMapper
|
||||
// Note: NullableAnnotation.Annotated is for reference type nullability (List<T>?, string?, etc.)
|
||||
// We don't unwrap these - just use the underlying type. Nullable<T> value types are handled later.
|
||||
|
||||
// Handle Nullable<T> 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<Guid> as Guid)
|
||||
if (typeSymbol is INamedTypeSymbol collectionType)
|
||||
{
|
||||
// List, IEnumerable, Array, ICollection etc. (but not Nullable<T>)
|
||||
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<K, V>
|
||||
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<T> 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<T>)
|
||||
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<K, V>
|
||||
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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user