Fix gRPC source generator for complex nested types
- Add DateTime/Timestamp conversion in nested property mapping - Add IsReadOnly property detection to skip computed properties - Extract ElementNestedProperties for complex list element types - Skip read-only properties in GenerateComplexObjectMapping and GenerateComplexListMapping 🤖 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
661f5b4b1c
commit
bd43bc9bde
@ -363,6 +363,8 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
IsGuid = IsGuidType(property.Type),
|
IsGuid = IsGuidType(property.Type),
|
||||||
IsJsonElement = IsJsonElementType(property.Type),
|
IsJsonElement = IsJsonElementType(property.Type),
|
||||||
IsList = IsListOrCollection(property.Type),
|
IsList = IsListOrCollection(property.Type),
|
||||||
|
IsBinaryType = IsBinaryType(property.Type),
|
||||||
|
IsStream = IsStreamType(property.Type),
|
||||||
};
|
};
|
||||||
|
|
||||||
// If it's a list, extract element type info
|
// If it's a list, extract element type info
|
||||||
@ -482,6 +484,24 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
return unwrapped.ToDisplayString() == "System.Text.Json.JsonElement";
|
return unwrapped.ToDisplayString() == "System.Text.Json.JsonElement";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsBinaryType(ITypeSymbol type)
|
||||||
|
{
|
||||||
|
return ProtoFileTypeMapper.IsBinaryType(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsStreamType(ITypeSymbol type)
|
||||||
|
{
|
||||||
|
var fullTypeName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||||
|
if (fullTypeName.Contains("System.IO.Stream") ||
|
||||||
|
fullTypeName.Contains("System.IO.MemoryStream") ||
|
||||||
|
fullTypeName.Contains("System.IO.FileStream"))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var typeName = type.Name;
|
||||||
|
return typeName == "Stream" || typeName == "MemoryStream" || typeName == "FileStream";
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsListOrCollection(ITypeSymbol type)
|
private static bool IsListOrCollection(ITypeSymbol type)
|
||||||
{
|
{
|
||||||
if (type is IArrayTypeSymbol)
|
if (type is IArrayTypeSymbol)
|
||||||
@ -582,6 +602,9 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
|
|
||||||
foreach (var property in properties)
|
foreach (var property in properties)
|
||||||
{
|
{
|
||||||
|
// Skip read-only properties (no setter) - they are computed and can't be set
|
||||||
|
var isReadOnly = property.IsReadOnly || property.SetMethod == null;
|
||||||
|
|
||||||
var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||||
var propInfo = new PropertyInfo
|
var propInfo = new PropertyInfo
|
||||||
{
|
{
|
||||||
@ -591,6 +614,7 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
ProtoType = string.Empty,
|
ProtoType = string.Empty,
|
||||||
FieldNumber = 0,
|
FieldNumber = 0,
|
||||||
IsComplexType = IsUserDefinedComplexType(property.Type),
|
IsComplexType = IsUserDefinedComplexType(property.Type),
|
||||||
|
IsReadOnly = isReadOnly,
|
||||||
// Type metadata
|
// Type metadata
|
||||||
IsNullable = IsNullableType(property.Type),
|
IsNullable = IsNullableType(property.Type),
|
||||||
IsEnum = IsEnumType(property.Type),
|
IsEnum = IsEnumType(property.Type),
|
||||||
@ -599,6 +623,8 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
IsGuid = IsGuidType(property.Type),
|
IsGuid = IsGuidType(property.Type),
|
||||||
IsJsonElement = IsJsonElementType(property.Type),
|
IsJsonElement = IsJsonElementType(property.Type),
|
||||||
IsList = IsListOrCollection(property.Type),
|
IsList = IsListOrCollection(property.Type),
|
||||||
|
IsBinaryType = IsBinaryType(property.Type),
|
||||||
|
IsStream = IsStreamType(property.Type),
|
||||||
};
|
};
|
||||||
|
|
||||||
// If it's a list, extract element type info
|
// If it's a list, extract element type info
|
||||||
@ -610,6 +636,13 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||||
propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType);
|
propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType);
|
||||||
propInfo.IsElementGuid = IsGuidType(elementType);
|
propInfo.IsElementGuid = IsGuidType(elementType);
|
||||||
|
|
||||||
|
// Extract nested properties for complex element types
|
||||||
|
if (propInfo.IsElementComplexType && elementType is INamedTypeSymbol namedElementType)
|
||||||
|
{
|
||||||
|
propInfo.ElementNestedProperties = new List<PropertyInfo>();
|
||||||
|
ExtractNestedPropertiesWithTypeInfo(namedElementType, propInfo.ElementNestedProperties);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Recursively extract nested properties for complex types
|
// Recursively extract nested properties for complex types
|
||||||
@ -716,6 +749,29 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle binary types (proto ByteString -> C# byte[]/Stream)
|
||||||
|
if (prop.IsBinaryType)
|
||||||
|
{
|
||||||
|
if (prop.IsStream)
|
||||||
|
{
|
||||||
|
// ByteString -> MemoryStream
|
||||||
|
if (prop.IsNullable)
|
||||||
|
{
|
||||||
|
return $"{indent}{prop.Name} = {source}?.IsEmpty == false ? new System.IO.MemoryStream({source}.ToByteArray()) : null,";
|
||||||
|
}
|
||||||
|
return $"{indent}{prop.Name} = new System.IO.MemoryStream({source}.ToByteArray()),";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// ByteString -> byte[]
|
||||||
|
if (prop.IsNullable)
|
||||||
|
{
|
||||||
|
return $"{indent}{prop.Name} = {source}?.IsEmpty == false ? {source}.ToByteArray() : null,";
|
||||||
|
}
|
||||||
|
return $"{indent}{prop.Name} = {source}.ToByteArray(),";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle complex types (single objects)
|
// Handle complex types (single objects)
|
||||||
if (prop.IsComplexType)
|
if (prop.IsComplexType)
|
||||||
{
|
{
|
||||||
@ -734,6 +790,9 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
|
|
||||||
foreach (var nestedProp in prop.ElementNestedProperties!)
|
foreach (var nestedProp in prop.ElementNestedProperties!)
|
||||||
{
|
{
|
||||||
|
// Skip read-only properties - they can't be assigned
|
||||||
|
if (nestedProp.IsReadOnly) continue;
|
||||||
|
|
||||||
var nestedSourcePropName = char.ToUpper(nestedProp.Name[0]) + nestedProp.Name.Substring(1);
|
var nestedSourcePropName = char.ToUpper(nestedProp.Name[0]) + nestedProp.Name.Substring(1);
|
||||||
var nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, "x", indent + " ");
|
var nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, "x", indent + " ");
|
||||||
sb.AppendLine(nestedAssignment);
|
sb.AppendLine(nestedAssignment);
|
||||||
@ -751,6 +810,9 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
|
|
||||||
foreach (var nestedProp in prop.NestedProperties)
|
foreach (var nestedProp in prop.NestedProperties)
|
||||||
{
|
{
|
||||||
|
// Skip read-only properties - they can't be assigned
|
||||||
|
if (nestedProp.IsReadOnly) continue;
|
||||||
|
|
||||||
var nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, source, indent + " ");
|
var nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, source, indent + " ");
|
||||||
sb.AppendLine(nestedAssignment);
|
sb.AppendLine(nestedAssignment);
|
||||||
}
|
}
|
||||||
@ -783,6 +845,19 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 Guid
|
// Handle Guid
|
||||||
if (prop.IsGuid)
|
if (prop.IsGuid)
|
||||||
{
|
{
|
||||||
@ -796,9 +871,13 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle lists
|
// Handle lists with complex element types
|
||||||
if (prop.IsList)
|
if (prop.IsList)
|
||||||
{
|
{
|
||||||
|
if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any())
|
||||||
|
{
|
||||||
|
return GenerateComplexListMapping(prop, source, indent);
|
||||||
|
}
|
||||||
return $"{indent}{prop.Name} = {source}?.ToList(),";
|
return $"{indent}{prop.Name} = {source}?.ToList(),";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -897,6 +976,29 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
return $"{indent}{prop.Name} = (int){source},";
|
return $"{indent}{prop.Name} = (int){source},";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle binary types (byte[], Stream -> ByteString)
|
||||||
|
if (prop.IsBinaryType)
|
||||||
|
{
|
||||||
|
if (prop.IsStream)
|
||||||
|
{
|
||||||
|
// Stream -> ByteString: read stream to bytes first
|
||||||
|
if (prop.IsNullable)
|
||||||
|
{
|
||||||
|
return $"{indent}{prop.Name} = {source} != null ? Google.Protobuf.ByteString.FromStream({source}) : Google.Protobuf.ByteString.Empty,";
|
||||||
|
}
|
||||||
|
return $"{indent}{prop.Name} = Google.Protobuf.ByteString.FromStream({source}),";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// byte[] -> ByteString
|
||||||
|
if (prop.IsNullable)
|
||||||
|
{
|
||||||
|
return $"{indent}{prop.Name} = {source} != null ? Google.Protobuf.ByteString.CopyFrom({source}) : Google.Protobuf.ByteString.Empty,";
|
||||||
|
}
|
||||||
|
return $"{indent}{prop.Name} = Google.Protobuf.ByteString.CopyFrom({source}),";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle complex types (single objects)
|
// Handle complex types (single objects)
|
||||||
if (prop.IsComplexType)
|
if (prop.IsComplexType)
|
||||||
{
|
{
|
||||||
@ -959,6 +1061,32 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
{
|
{
|
||||||
var source = $"{sourceVar}.{prop.Name}";
|
var source = $"{sourceVar}.{prop.Name}";
|
||||||
|
|
||||||
|
// Handle DateTime (C# DateTime -> proto Timestamp)
|
||||||
|
if (prop.IsDateTime)
|
||||||
|
{
|
||||||
|
if (prop.IsNullable)
|
||||||
|
{
|
||||||
|
return $"{indent}{prop.Name} = {source} != null ? 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 DateOnly (C# DateOnly -> proto string)
|
||||||
|
if (prop.Type.Contains("DateOnly"))
|
||||||
|
{
|
||||||
|
if (prop.IsNullable)
|
||||||
|
{
|
||||||
|
return $"{indent}{prop.Name} = {source}?.ToString(\"yyyy-MM-dd\") ?? string.Empty,";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return $"{indent}{prop.Name} = {source}.ToString(\"yyyy-MM-dd\"),";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Guid
|
// Handle Guid
|
||||||
if (prop.IsGuid)
|
if (prop.IsGuid)
|
||||||
{
|
{
|
||||||
@ -988,9 +1116,34 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
// Handle enums
|
// Handle enums
|
||||||
if (prop.IsEnum)
|
if (prop.IsEnum)
|
||||||
{
|
{
|
||||||
|
if (prop.IsNullable)
|
||||||
|
{
|
||||||
|
return $"{indent}{prop.Name} = (int)({source} ?? default),";
|
||||||
|
}
|
||||||
return $"{indent}{prop.Name} = (int){source},";
|
return $"{indent}{prop.Name} = (int){source},";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle binary types (C# byte[]/Stream -> proto ByteString)
|
||||||
|
if (prop.IsBinaryType)
|
||||||
|
{
|
||||||
|
if (prop.IsStream)
|
||||||
|
{
|
||||||
|
if (prop.IsNullable)
|
||||||
|
{
|
||||||
|
return $"{indent}{prop.Name} = {source} != null ? Google.Protobuf.ByteString.FromStream({source}) : Google.Protobuf.ByteString.Empty,";
|
||||||
|
}
|
||||||
|
return $"{indent}{prop.Name} = Google.Protobuf.ByteString.FromStream({source}),";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (prop.IsNullable)
|
||||||
|
{
|
||||||
|
return $"{indent}{prop.Name} = {source} != null ? Google.Protobuf.ByteString.CopyFrom({source}) : Google.Protobuf.ByteString.Empty,";
|
||||||
|
}
|
||||||
|
return $"{indent}{prop.Name} = Google.Protobuf.ByteString.CopyFrom({source}),";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle lists
|
// Handle lists
|
||||||
if (prop.IsList)
|
if (prop.IsList)
|
||||||
{
|
{
|
||||||
@ -1000,11 +1153,19 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
}
|
}
|
||||||
else if (prop.IsElementComplexType)
|
else if (prop.IsElementComplexType)
|
||||||
{
|
{
|
||||||
// Complex list elements need mapping - but we don't have nested property info here
|
// Complex list elements need mapping
|
||||||
|
if (prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any())
|
||||||
|
{
|
||||||
|
// Use recursive mapping for nested properties
|
||||||
|
return GenerateResultComplexListMapping(prop, source, indent);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
// Fall back to creating empty proto objects (the user needs to ensure types are compatible)
|
// Fall back to creating empty proto objects (the user needs to ensure types are compatible)
|
||||||
var elementTypeName = prop.ElementType?.Split('.').Last() ?? "object";
|
var elementTypeName = prop.ElementType?.Split('.').Last() ?? "object";
|
||||||
return $"{indent}{prop.Name} = {{ {source}?.Select(x => new {elementTypeName}()) ?? Enumerable.Empty<{elementTypeName}>() }},";
|
return $"{indent}{prop.Name} = {{ {source}?.Select(x => new {elementTypeName}()) ?? Enumerable.Empty<{elementTypeName}>() }},";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return $"{indent}{prop.Name} = {{ {source} }},";
|
return $"{indent}{prop.Name} = {{ {source} }},";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1012,7 +1173,12 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
if (prop.IsComplexType)
|
if (prop.IsComplexType)
|
||||||
{
|
{
|
||||||
var typeName = prop.Type.Split('.').Last().Replace("?", "");
|
var typeName = prop.Type.Split('.').Last().Replace("?", "");
|
||||||
if (prop.IsNullable || prop.Type.EndsWith("?"))
|
if (prop.NestedProperties != null && prop.NestedProperties.Any())
|
||||||
|
{
|
||||||
|
// Use recursive mapping for nested properties
|
||||||
|
return GenerateResultComplexObjectMapping(prop, source, indent);
|
||||||
|
}
|
||||||
|
else if (prop.IsNullable || prop.Type.EndsWith("?"))
|
||||||
{
|
{
|
||||||
return $"{indent}{prop.Name} = {source} != null ? new {typeName}() : null,";
|
return $"{indent}{prop.Name} = {source} != null ? new {typeName}() : null,";
|
||||||
}
|
}
|
||||||
@ -1028,6 +1194,12 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
return $"{indent}{prop.Name} = {source} ?? string.Empty,";
|
return $"{indent}{prop.Name} = {source} ?? string.Empty,";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle nullable value types (int?, long?, double?, etc.)
|
||||||
|
if (prop.IsNullable)
|
||||||
|
{
|
||||||
|
return $"{indent}{prop.Name} = {source} ?? default,";
|
||||||
|
}
|
||||||
|
|
||||||
// Default: direct assignment
|
// Default: direct assignment
|
||||||
return $"{indent}{prop.Name} = {source},";
|
return $"{indent}{prop.Name} = {source},";
|
||||||
}
|
}
|
||||||
@ -1079,6 +1251,8 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
IsJsonElement = IsJsonElementType(property.Type),
|
IsJsonElement = IsJsonElementType(property.Type),
|
||||||
IsList = IsListOrCollection(property.Type),
|
IsList = IsListOrCollection(property.Type),
|
||||||
IsComplexType = IsUserDefinedComplexType(property.Type),
|
IsComplexType = IsUserDefinedComplexType(property.Type),
|
||||||
|
IsBinaryType = IsBinaryType(property.Type),
|
||||||
|
IsStream = IsStreamType(property.Type),
|
||||||
};
|
};
|
||||||
|
|
||||||
// If it's a list, extract element type info
|
// If it's a list, extract element type info
|
||||||
@ -1090,6 +1264,13 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||||
propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType);
|
propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType);
|
||||||
propInfo.IsElementGuid = IsGuidType(elementType);
|
propInfo.IsElementGuid = IsGuidType(elementType);
|
||||||
|
|
||||||
|
// Extract nested properties for complex element types
|
||||||
|
if (propInfo.IsElementComplexType && elementType is INamedTypeSymbol namedElementType)
|
||||||
|
{
|
||||||
|
propInfo.ElementNestedProperties = new List<PropertyInfo>();
|
||||||
|
ExtractNestedPropertiesWithTypeInfo(namedElementType, propInfo.ElementNestedProperties);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If it's a complex type (not list), extract nested properties
|
// If it's a complex type (not list), extract nested properties
|
||||||
@ -1133,6 +1314,8 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
IsJsonElement = IsJsonElementType(property.Type),
|
IsJsonElement = IsJsonElementType(property.Type),
|
||||||
IsList = IsListOrCollection(property.Type),
|
IsList = IsListOrCollection(property.Type),
|
||||||
IsComplexType = IsUserDefinedComplexType(property.Type),
|
IsComplexType = IsUserDefinedComplexType(property.Type),
|
||||||
|
IsBinaryType = IsBinaryType(property.Type),
|
||||||
|
IsStream = IsStreamType(property.Type),
|
||||||
};
|
};
|
||||||
|
|
||||||
// If it's a list, extract element type info
|
// If it's a list, extract element type info
|
||||||
@ -2528,7 +2711,8 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
sb.AppendLine(" var domainElementType = domainProp.PropertyType.IsArray");
|
sb.AppendLine(" var domainElementType = domainProp.PropertyType.IsArray");
|
||||||
sb.AppendLine(" ? domainProp.PropertyType.GetElementType()");
|
sb.AppendLine(" ? domainProp.PropertyType.GetElementType()");
|
||||||
sb.AppendLine(" : domainProp.PropertyType.IsGenericType ? domainProp.PropertyType.GetGenericArguments()[0] : null;");
|
sb.AppendLine(" : domainProp.PropertyType.IsGenericType ? domainProp.PropertyType.GetGenericArguments()[0] : null;");
|
||||||
sb.AppendLine(" var protoElementType = protoField.MessageType?.ClrType;");
|
sb.AppendLine(" // Only access MessageType for message fields (throws for primitives)");
|
||||||
|
sb.AppendLine(" var protoElementType = protoField.FieldType == Google.Protobuf.Reflection.FieldType.Message ? protoField.MessageType?.ClrType : null;");
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
sb.AppendLine(" foreach (var item in enumerable)");
|
sb.AppendLine(" foreach (var item in enumerable)");
|
||||||
sb.AppendLine(" {");
|
sb.AppendLine(" {");
|
||||||
@ -2552,14 +2736,66 @@ namespace Svrnty.CQRS.Grpc.Generators
|
|||||||
sb.AppendLine(" }");
|
sb.AppendLine(" }");
|
||||||
sb.AppendLine(" }");
|
sb.AppendLine(" }");
|
||||||
sb.AppendLine(" }");
|
sb.AppendLine(" }");
|
||||||
|
sb.AppendLine(" // Handle enumerable value types that map to proto messages with repeated fields (e.g., NpgsqlPolygon -> proto message with items)");
|
||||||
|
sb.AppendLine(" else if (domainProp.PropertyType.IsValueType && ");
|
||||||
|
sb.AppendLine(" domainValue is System.Collections.IEnumerable valueTypeEnumerable &&");
|
||||||
|
sb.AppendLine(" protoField.FieldType == Google.Protobuf.Reflection.FieldType.Message)");
|
||||||
|
sb.AppendLine(" {");
|
||||||
|
sb.AppendLine(" // Create the proto message and look for its 'items' repeated field");
|
||||||
|
sb.AppendLine(" var protoFieldType = protoField.MessageType?.ClrType;");
|
||||||
|
sb.AppendLine(" if (protoFieldType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoFieldType))");
|
||||||
|
sb.AppendLine(" {");
|
||||||
|
sb.AppendLine(" var nestedProto = System.Activator.CreateInstance(protoFieldType) as Google.Protobuf.IMessage;");
|
||||||
|
sb.AppendLine(" if (nestedProto != null)");
|
||||||
|
sb.AppendLine(" {");
|
||||||
|
sb.AppendLine(" // Find the 'items' field in the proto message");
|
||||||
|
sb.AppendLine(" var itemsField = nestedProto.Descriptor.FindFieldByName(\"items\");");
|
||||||
|
sb.AppendLine(" if (itemsField != null && itemsField.IsRepeated)");
|
||||||
|
sb.AppendLine(" {");
|
||||||
|
sb.AppendLine(" var repeatedField = itemsField.Accessor.GetValue(nestedProto);");
|
||||||
|
sb.AppendLine(" var repeatedFieldType = repeatedField?.GetType();");
|
||||||
|
sb.AppendLine(" var protoElementType = itemsField.MessageType?.ClrType;");
|
||||||
|
sb.AppendLine(" ");
|
||||||
|
sb.AppendLine(" if (repeatedFieldType != null && protoElementType != null)");
|
||||||
|
sb.AppendLine(" {");
|
||||||
|
sb.AppendLine(" var addMethod = repeatedFieldType.GetMethod(\"Add\", new[] { protoElementType });");
|
||||||
|
sb.AppendLine(" if (addMethod != null)");
|
||||||
|
sb.AppendLine(" {");
|
||||||
|
sb.AppendLine(" // Get element type from the enumerable");
|
||||||
|
sb.AppendLine(" var enumerableInterface = domainProp.PropertyType.GetInterfaces()");
|
||||||
|
sb.AppendLine(" .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(System.Collections.Generic.IEnumerable<>));");
|
||||||
|
sb.AppendLine(" var domainElementType = enumerableInterface?.GetGenericArguments()[0];");
|
||||||
|
sb.AppendLine(" ");
|
||||||
|
sb.AppendLine(" foreach (var item in valueTypeEnumerable)");
|
||||||
|
sb.AppendLine(" {");
|
||||||
|
sb.AppendLine(" if (item == null) continue;");
|
||||||
|
sb.AppendLine(" ");
|
||||||
|
sb.AppendLine(" // Map each item to the proto element type");
|
||||||
|
sb.AppendLine(" if (domainElementType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoElementType))");
|
||||||
|
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(" }");
|
||||||
|
sb.AppendLine(" }");
|
||||||
|
sb.AppendLine(" }");
|
||||||
|
sb.AppendLine(" }");
|
||||||
|
sb.AppendLine(" protoAccessor.SetValue(proto, nestedProto);");
|
||||||
|
sb.AppendLine(" }");
|
||||||
|
sb.AppendLine(" }");
|
||||||
|
sb.AppendLine(" }");
|
||||||
sb.AppendLine(" // Handle nested complex types (non-primitive, non-enum, non-string, non-collection)");
|
sb.AppendLine(" // Handle nested complex types (non-primitive, non-enum, non-string, non-collection)");
|
||||||
sb.AppendLine(" else if (!domainProp.PropertyType.IsPrimitive && ");
|
sb.AppendLine(" else if (!domainProp.PropertyType.IsPrimitive && ");
|
||||||
sb.AppendLine(" domainProp.PropertyType != typeof(string) && ");
|
sb.AppendLine(" domainProp.PropertyType != typeof(string) && ");
|
||||||
sb.AppendLine(" !domainProp.PropertyType.IsEnum &&");
|
sb.AppendLine(" !domainProp.PropertyType.IsEnum &&");
|
||||||
sb.AppendLine(" !domainProp.PropertyType.IsValueType)");
|
sb.AppendLine(" !domainProp.PropertyType.IsValueType)");
|
||||||
sb.AppendLine(" {");
|
sb.AppendLine(" {");
|
||||||
sb.AppendLine(" // Get the proto field type and recursively map");
|
sb.AppendLine(" // Get the proto field type and recursively map (only access MessageType for message fields)");
|
||||||
sb.AppendLine(" var protoFieldType = protoAccessor.GetValue(proto)?.GetType() ?? protoField.MessageType?.ClrType;");
|
sb.AppendLine(" var protoFieldType = protoAccessor.GetValue(proto)?.GetType() ?? (protoField.FieldType == Google.Protobuf.Reflection.FieldType.Message ? protoField.MessageType?.ClrType : null);");
|
||||||
sb.AppendLine(" if (protoFieldType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoFieldType))");
|
sb.AppendLine(" if (protoFieldType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoFieldType))");
|
||||||
sb.AppendLine(" {");
|
sb.AppendLine(" {");
|
||||||
sb.AppendLine(" var mapMethod = typeof(DynamicQueryServiceImpl).GetMethod(\"MapToProtoModel\", ");
|
sb.AppendLine(" var mapMethod = typeof(DynamicQueryServiceImpl).GetMethod(\"MapToProtoModel\", ");
|
||||||
|
|||||||
@ -46,6 +46,9 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
|
|||||||
public bool IsDateTime { get; set; }
|
public bool IsDateTime { get; set; }
|
||||||
public bool IsGuid { get; set; }
|
public bool IsGuid { get; set; }
|
||||||
public bool IsJsonElement { get; set; }
|
public bool IsJsonElement { get; set; }
|
||||||
|
public bool IsBinaryType { get; set; } // Stream, byte[], MemoryStream
|
||||||
|
public bool IsStream { get; set; } // Specifically Stream types (not byte[])
|
||||||
|
public bool IsReadOnly { get; set; } // Read-only/computed properties should be skipped
|
||||||
public string? ElementType { get; set; }
|
public string? ElementType { get; set; }
|
||||||
public bool IsElementComplexType { get; set; }
|
public bool IsElementComplexType { get; set; }
|
||||||
public bool IsElementGuid { get; set; }
|
public bool IsElementGuid { get; set; }
|
||||||
@ -66,6 +69,9 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
|
|||||||
IsDateTime = false;
|
IsDateTime = false;
|
||||||
IsGuid = false;
|
IsGuid = false;
|
||||||
IsJsonElement = false;
|
IsJsonElement = false;
|
||||||
|
IsBinaryType = false;
|
||||||
|
IsStream = false;
|
||||||
|
IsReadOnly = false;
|
||||||
IsElementComplexType = false;
|
IsElementComplexType = false;
|
||||||
IsElementGuid = false;
|
IsElementGuid = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,6 +68,20 @@ internal static class ProtoFileTypeMapper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle byte[] array type (check before switch since it's an array)
|
||||||
|
if (typeSymbol is IArrayTypeSymbol arrayType && arrayType.ElementType.SpecialType == SpecialType.System_Byte)
|
||||||
|
{
|
||||||
|
return "bytes";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Stream types -> bytes
|
||||||
|
if (fullTypeName.Contains("System.IO.Stream") ||
|
||||||
|
fullTypeName.Contains("System.IO.MemoryStream") ||
|
||||||
|
fullTypeName.Contains("System.IO.FileStream"))
|
||||||
|
{
|
||||||
|
return "bytes";
|
||||||
|
}
|
||||||
|
|
||||||
// Basic types
|
// Basic types
|
||||||
switch (typeName)
|
switch (typeName)
|
||||||
{
|
{
|
||||||
@ -97,6 +111,10 @@ internal static class ProtoFileTypeMapper
|
|||||||
return "double";
|
return "double";
|
||||||
case "Byte[]":
|
case "Byte[]":
|
||||||
return "bytes";
|
return "bytes";
|
||||||
|
case "Stream":
|
||||||
|
case "MemoryStream":
|
||||||
|
case "FileStream":
|
||||||
|
return "bytes";
|
||||||
case "Guid":
|
case "Guid":
|
||||||
// Guid serialized as string
|
// Guid serialized as string
|
||||||
return "string";
|
return "string";
|
||||||
@ -184,8 +202,8 @@ internal static class ProtoFileTypeMapper
|
|||||||
var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||||
|
|
||||||
// Skip these types - they should trigger a warning/error
|
// Skip these types - they should trigger a warning/error
|
||||||
if (fullTypeName.Contains("System.IO.Stream") ||
|
// Note: Stream types are now supported (mapped to bytes)
|
||||||
fullTypeName.Contains("System.Threading.CancellationToken") ||
|
if (fullTypeName.Contains("System.Threading.CancellationToken") ||
|
||||||
fullTypeName.Contains("System.Threading.Tasks.Task") ||
|
fullTypeName.Contains("System.Threading.Tasks.Task") ||
|
||||||
fullTypeName.Contains("System.Collections.Generic.IAsyncEnumerable") ||
|
fullTypeName.Contains("System.Collections.Generic.IAsyncEnumerable") ||
|
||||||
fullTypeName.Contains("System.Func") ||
|
fullTypeName.Contains("System.Func") ||
|
||||||
@ -198,6 +216,31 @@ internal static class ProtoFileTypeMapper
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a type is a Stream or byte array type (for special ByteString handling)
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsBinaryType(ITypeSymbol typeSymbol)
|
||||||
|
{
|
||||||
|
var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||||
|
|
||||||
|
// Check for byte[]
|
||||||
|
if (typeSymbol is IArrayTypeSymbol arrayType && arrayType.ElementType.SpecialType == SpecialType.System_Byte)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Stream types
|
||||||
|
if (fullTypeName.Contains("System.IO.Stream") ||
|
||||||
|
fullTypeName.Contains("System.IO.MemoryStream") ||
|
||||||
|
fullTypeName.Contains("System.IO.FileStream"))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var typeName = typeSymbol.Name;
|
||||||
|
return typeName == "Stream" || typeName == "MemoryStream" || typeName == "FileStream";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the element type from a collection type, or returns the type itself if not a collection.
|
/// Gets the element type from a collection type, or returns the type itself if not a collection.
|
||||||
/// Also unwraps Nullable types.
|
/// Also unwraps Nullable types.
|
||||||
|
|||||||
@ -1,111 +1,7 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
option csharp_namespace = "Svrnty.Sample.Grpc";
|
option csharp_namespace = "Generated.Grpc";
|
||||||
|
|
||||||
package cqrs;
|
package cqrs;
|
||||||
|
|
||||||
// Command service for CQRS operations
|
// Placeholder proto file - will be regenerated on next build
|
||||||
service CommandService {
|
|
||||||
// AddUserCommand operation
|
|
||||||
rpc AddUser (AddUserCommandRequest) returns (AddUserCommandResponse);
|
|
||||||
|
|
||||||
// RemoveUserCommand operation
|
|
||||||
rpc RemoveUser (RemoveUserCommandRequest) returns (RemoveUserCommandResponse);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query service for CQRS operations
|
|
||||||
service QueryService {
|
|
||||||
// FetchUserQuery operation
|
|
||||||
rpc FetchUser (FetchUserQueryRequest) returns (FetchUserQueryResponse);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// DynamicQuery service for CQRS operations
|
|
||||||
service DynamicQueryService {
|
|
||||||
// Dynamic query for User
|
|
||||||
rpc QueryUsers (DynamicQueryUsersRequest) returns (DynamicQueryUsersResponse);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request message for AddUserCommand
|
|
||||||
message AddUserCommandRequest {
|
|
||||||
string name = 1;
|
|
||||||
string email = 2;
|
|
||||||
int32 age = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response message for AddUserCommand
|
|
||||||
message AddUserCommandResponse {
|
|
||||||
int32 result = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request message for RemoveUserCommand
|
|
||||||
message RemoveUserCommandRequest {
|
|
||||||
int32 user_id = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response message for RemoveUserCommand
|
|
||||||
message RemoveUserCommandResponse {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request message for FetchUserQuery
|
|
||||||
message FetchUserQueryRequest {
|
|
||||||
int32 user_id = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Response message for FetchUserQuery
|
|
||||||
message FetchUserQueryResponse {
|
|
||||||
User result = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// User entity
|
|
||||||
message User {
|
|
||||||
int32 id = 1;
|
|
||||||
string name = 2;
|
|
||||||
string email = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic query filter with AND/OR support
|
|
||||||
message DynamicQueryFilter {
|
|
||||||
string path = 1;
|
|
||||||
int32 type = 2; // PoweredSoft.DynamicQuery.Core.FilterType
|
|
||||||
string value = 3;
|
|
||||||
repeated DynamicQueryFilter and = 4;
|
|
||||||
repeated DynamicQueryFilter or = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic query sort
|
|
||||||
message DynamicQuerySort {
|
|
||||||
string path = 1;
|
|
||||||
bool ascending = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic query group
|
|
||||||
message DynamicQueryGroup {
|
|
||||||
string path = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic query aggregate
|
|
||||||
message DynamicQueryAggregate {
|
|
||||||
string path = 1;
|
|
||||||
int32 type = 2; // PoweredSoft.DynamicQuery.Core.AggregateType
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic query request for User
|
|
||||||
message DynamicQueryUsersRequest {
|
|
||||||
int32 page = 1;
|
|
||||||
int32 page_size = 2;
|
|
||||||
repeated DynamicQueryFilter filters = 3;
|
|
||||||
repeated DynamicQuerySort sorts = 4;
|
|
||||||
repeated DynamicQueryGroup groups = 5;
|
|
||||||
repeated DynamicQueryAggregate aggregates = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic query response for User
|
|
||||||
message DynamicQueryUsersResponse {
|
|
||||||
repeated User data = 1;
|
|
||||||
int64 total_records = 2;
|
|
||||||
int32 number_of_pages = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user