Compare commits
2 Commits
main
...
feat/nutri
| Author | SHA1 | Date | |
|---|---|---|---|
| 46e739eead | |||
| 179b06374d |
@ -350,6 +350,7 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
var propInfo = new PropertyInfo
|
||||
{
|
||||
Name = property.Name,
|
||||
ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(property.Name)),
|
||||
Type = propertyType,
|
||||
FullyQualifiedType = propertyType,
|
||||
ProtoType = protoType,
|
||||
@ -507,6 +508,7 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
var propInfo = new PropertyInfo
|
||||
{
|
||||
Name = property.Name,
|
||||
ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(property.Name)),
|
||||
Type = propertyType,
|
||||
FullyQualifiedType = propertyType,
|
||||
ProtoType = string.Empty,
|
||||
@ -536,6 +538,7 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
var propInfo = new PropertyInfo
|
||||
{
|
||||
Name = property.Name,
|
||||
ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(property.Name)),
|
||||
Type = propertyType,
|
||||
FullyQualifiedType = propertyType,
|
||||
ProtoType = string.Empty,
|
||||
@ -577,26 +580,26 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
{
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
var sourcePropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1);
|
||||
// Use ProtoPropertyName to access the proto source property
|
||||
if (prop.IsComplexType)
|
||||
{
|
||||
// Generate nested object mapping
|
||||
sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{sourcePropName} != null ? new {prop.FullyQualifiedType}");
|
||||
sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{prop.ProtoPropertyName} != null ? new {prop.FullyQualifiedType}");
|
||||
sb.AppendLine($"{indent}{{");
|
||||
GenerateNestedPropertyMapping(sb, prop.NestedProperties, $"{sourcePrefix}.{sourcePropName}", indent + " ");
|
||||
GenerateNestedPropertyMapping(sb, prop.NestedProperties, $"{sourcePrefix}.{prop.ProtoPropertyName}", indent + " ");
|
||||
sb.AppendLine($"{indent}}} : null!,");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{sourcePropName},");
|
||||
sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{prop.ProtoPropertyName},");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GeneratePropertyAssignment(PropertyInfo prop, string requestVar, string indent)
|
||||
{
|
||||
var requestPropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1);
|
||||
var source = $"{requestVar}.{requestPropName}";
|
||||
// Use ProtoPropertyName to access the proto request property
|
||||
var source = $"{requestVar}.{prop.ProtoPropertyName}";
|
||||
|
||||
// Handle lists
|
||||
if (prop.IsList)
|
||||
@ -663,7 +666,7 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
|
||||
foreach (var nestedProp in prop.ElementNestedProperties!)
|
||||
{
|
||||
var nestedSourcePropName = char.ToUpper(nestedProp.Name[0]) + nestedProp.Name.Substring(1);
|
||||
// Use ProtoPropertyName for accessing proto source properties
|
||||
var nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, "x", indent + " ");
|
||||
sb.AppendLine(nestedAssignment);
|
||||
}
|
||||
@ -690,8 +693,8 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
|
||||
private static string GenerateNestedPropertyAssignment(PropertyInfo prop, string sourceVar, string indent)
|
||||
{
|
||||
var sourcePropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1);
|
||||
var source = $"{sourceVar}.{sourcePropName}";
|
||||
// Use ProtoPropertyName to access the proto source property
|
||||
var source = $"{sourceVar}.{prop.ProtoPropertyName}";
|
||||
|
||||
// Handle enums
|
||||
if (prop.IsEnum)
|
||||
@ -750,27 +753,112 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
var resultTypeString = resultType.ToDisplayString();
|
||||
queryInfo.IsResultPrimitiveType = IsPrimitiveType(resultTypeString);
|
||||
|
||||
// Extract result type properties if it's a complex type
|
||||
if (!queryInfo.IsResultPrimitiveType)
|
||||
// Check if result type is nullable and compute non-nullable name
|
||||
var isNullable = resultType.NullableAnnotation == NullableAnnotation.Annotated ||
|
||||
resultTypeString.EndsWith("?");
|
||||
queryInfo.IsResultNullable = isNullable;
|
||||
|
||||
// Use the short type name (without namespace) for proto type mapping
|
||||
// This matches how ResultElementType is used for collections
|
||||
var shortTypeName = resultType.Name;
|
||||
if (shortTypeName.EndsWith("?"))
|
||||
{
|
||||
var resultProperties = resultType.GetMembers().OfType<IPropertySymbol>()
|
||||
queryInfo.ResultTypeWithoutNullable = shortTypeName.Substring(0, shortTypeName.Length - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
queryInfo.ResultTypeWithoutNullable = shortTypeName;
|
||||
}
|
||||
|
||||
// Check if result type is a collection and extract element type
|
||||
var (isCollection, elementType) = GetCollectionElementType(resultType);
|
||||
queryInfo.IsResultCollection = isCollection;
|
||||
|
||||
// Determine which type to extract properties from
|
||||
INamedTypeSymbol? typeToExtractFrom = null;
|
||||
if (isCollection && elementType != null)
|
||||
{
|
||||
queryInfo.ResultElementType = elementType.Name;
|
||||
queryInfo.ResultElementTypeFullyQualified = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
typeToExtractFrom = elementType;
|
||||
}
|
||||
else if (!queryInfo.IsResultPrimitiveType && resultType is INamedTypeSymbol namedType)
|
||||
{
|
||||
typeToExtractFrom = namedType;
|
||||
}
|
||||
|
||||
// Extract result type properties if it's a complex type (or collection of complex type)
|
||||
if (typeToExtractFrom != null)
|
||||
{
|
||||
var resultProperties = typeToExtractFrom.GetMembers().OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
|
||||
.ToList();
|
||||
|
||||
foreach (var property in resultProperties)
|
||||
{
|
||||
var propType = property.Type;
|
||||
var propTypeStr = propType.ToDisplayString();
|
||||
|
||||
// Check for type categories
|
||||
var isEnum = propType.TypeKind == TypeKind.Enum;
|
||||
var isDecimal = propTypeStr == "decimal" || propTypeStr == "System.Decimal" ||
|
||||
propTypeStr == "decimal?" || propTypeStr == "System.Nullable<System.Decimal>";
|
||||
var isDateTime = propTypeStr.Contains("DateTime") || propTypeStr.Contains("DateTimeOffset");
|
||||
var isPropNullable = propType.NullableAnnotation == NullableAnnotation.Annotated ||
|
||||
propTypeStr.EndsWith("?") ||
|
||||
propTypeStr.StartsWith("System.Nullable<");
|
||||
var isString = propTypeStr == "string" || propTypeStr == "System.String" ||
|
||||
propTypeStr == "string?" || propTypeStr == "System.String?";
|
||||
|
||||
// Check if property is a collection
|
||||
var (isPropCollection, propElementType) = GetCollectionElementType(propType);
|
||||
var isElementComplexType = false;
|
||||
List<PropertyInfo>? elementNestedProperties = null;
|
||||
|
||||
if (isPropCollection && propElementType != null)
|
||||
{
|
||||
isElementComplexType = propElementType.TypeKind != TypeKind.Enum &&
|
||||
!IsPrimitiveType(propElementType.ToDisplayString());
|
||||
if (isElementComplexType)
|
||||
{
|
||||
elementNestedProperties = new List<PropertyInfo>();
|
||||
ExtractNestedPropertiesWithTypeInfo(propElementType, elementNestedProperties);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if property itself is a complex type
|
||||
var isComplexType = !isEnum && !isPropCollection &&
|
||||
!IsPrimitiveType(propTypeStr.TrimEnd('?'));
|
||||
List<PropertyInfo>? nestedProperties = null;
|
||||
if (isComplexType && propType is INamedTypeSymbol namedPropType)
|
||||
{
|
||||
nestedProperties = new List<PropertyInfo>();
|
||||
ExtractNestedPropertiesWithTypeInfo(namedPropType, nestedProperties);
|
||||
}
|
||||
|
||||
queryInfo.ResultProperties.Add(new PropertyInfo
|
||||
{
|
||||
Name = property.Name,
|
||||
Type = property.Type.ToDisplayString(),
|
||||
FullyQualifiedType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(property.Name)),
|
||||
Type = propTypeStr,
|
||||
FullyQualifiedType = propType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
ProtoType = string.Empty, // Not needed for result mapping
|
||||
FieldNumber = 0 // Not needed for result mapping
|
||||
FieldNumber = 0, // Not needed for result mapping
|
||||
IsEnum = isEnum,
|
||||
IsDecimal = isDecimal,
|
||||
IsDateTime = isDateTime,
|
||||
IsNullable = isPropNullable || isString, // Treat strings as nullable for null-coalescing
|
||||
IsList = isPropCollection,
|
||||
IsComplexType = isComplexType,
|
||||
NestedProperties = nestedProperties ?? new List<PropertyInfo>(),
|
||||
ElementType = propElementType?.ToDisplayString(),
|
||||
IsElementComplexType = isElementComplexType,
|
||||
ElementNestedProperties = elementNestedProperties
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Extract properties
|
||||
// Extract properties with type metadata for request mapping
|
||||
var properties = queryType.GetMembers().OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
|
||||
.ToList();
|
||||
@ -779,14 +867,30 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
var propertyTypeShort = property.Type.ToDisplayString();
|
||||
var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional);
|
||||
|
||||
// Determine type metadata for proper request mapping
|
||||
var isEnum = property.Type.TypeKind == TypeKind.Enum;
|
||||
var isDecimal = propertyTypeShort == "decimal" || propertyTypeShort == "System.Decimal" ||
|
||||
propertyTypeShort == "decimal?" || propertyTypeShort.Contains("Nullable<System.Decimal>");
|
||||
var isDateTime = propertyTypeShort.Contains("DateTime") || propertyTypeShort.Contains("DateTimeOffset");
|
||||
var isRequestPropNullable = property.Type.NullableAnnotation == NullableAnnotation.Annotated ||
|
||||
propertyTypeShort.EndsWith("?") ||
|
||||
propertyTypeShort.StartsWith("System.Nullable<");
|
||||
|
||||
queryInfo.Properties.Add(new PropertyInfo
|
||||
{
|
||||
Name = property.Name,
|
||||
ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(property.Name)),
|
||||
Type = propertyType,
|
||||
FullyQualifiedType = propertyType,
|
||||
ProtoType = protoType,
|
||||
FieldNumber = fieldNumber++
|
||||
FieldNumber = fieldNumber++,
|
||||
IsEnum = isEnum,
|
||||
IsDecimal = isDecimal,
|
||||
IsDateTime = isDateTime,
|
||||
IsNullable = isRequestPropNullable
|
||||
});
|
||||
}
|
||||
|
||||
@ -1579,6 +1683,7 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine("using Grpc.Core;");
|
||||
sb.AppendLine("using System.Threading.Tasks;");
|
||||
sb.AppendLine("using System.Linq;");
|
||||
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
|
||||
sb.AppendLine($"using {rootNamespace}.Grpc;");
|
||||
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
|
||||
@ -1617,29 +1722,130 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in query.Properties)
|
||||
{
|
||||
sb.AppendLine($" {prop.Name} = request.{char.ToUpper(prop.Name[0]) + prop.Name.Substring(1)},");
|
||||
var requestMapping = GenerateRequestPropertyMapping(prop);
|
||||
sb.AppendLine($" {prop.Name} = {requestMapping},");
|
||||
}
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);");
|
||||
|
||||
// Generate response with mapping if complex type
|
||||
// Generate response with mapping based on result type
|
||||
if (query.IsResultPrimitiveType)
|
||||
{
|
||||
sb.AppendLine($" return new {responseType} {{ Result = result }};");
|
||||
}
|
||||
else
|
||||
else if (query.IsResultCollection)
|
||||
{
|
||||
// Complex type - need to map from C# type to proto type
|
||||
sb.AppendLine($" return new {responseType}");
|
||||
// Collection result - use AddRange with LINQ Select for type conversion
|
||||
var elementTypeName = query.ResultElementType;
|
||||
sb.AppendLine($" var response = new {responseType}();");
|
||||
sb.AppendLine($" response.Result.AddRange(result.Select(item => new {elementTypeName}");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" Result = new {query.ResultType}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in query.ResultProperties)
|
||||
{
|
||||
sb.AppendLine($" {prop.Name} = result.{prop.Name},");
|
||||
// Skip collection properties (they need separate AddRange handling, not supported in collection results)
|
||||
if (prop.IsList) continue;
|
||||
|
||||
var mapping = GeneratePropertyMapping("item", prop);
|
||||
if (mapping != null)
|
||||
{
|
||||
sb.AppendLine($" {prop.ProtoPropertyName} = {mapping},");
|
||||
}
|
||||
}
|
||||
sb.AppendLine(" }));");
|
||||
sb.AppendLine(" return response;");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Single complex object - map properties with type conversions
|
||||
// Use the non-nullable type name for the proto message
|
||||
var protoResultType = query.ResultTypeWithoutNullable;
|
||||
|
||||
if (query.IsResultNullable)
|
||||
{
|
||||
// Handle nullable result - check for null and return empty response
|
||||
sb.AppendLine(" if (result == null)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" return new {responseType}();");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Separate simple properties from collection properties
|
||||
var simpleProps = query.ResultProperties.Where(p => !p.IsList).ToList();
|
||||
var collectionProps = query.ResultProperties.Where(p => p.IsList).ToList();
|
||||
|
||||
if (collectionProps.Count == 0)
|
||||
{
|
||||
// No collection properties - simple case
|
||||
sb.AppendLine($" return new {responseType}");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" Result = new {protoResultType}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in simpleProps)
|
||||
{
|
||||
var mapping = GeneratePropertyMapping("result", prop);
|
||||
sb.AppendLine($" {prop.ProtoPropertyName} = {mapping},");
|
||||
}
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" };");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Has collection properties - need to use AddRange
|
||||
sb.AppendLine($" var protoResult = new {protoResultType}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in simpleProps)
|
||||
{
|
||||
var mapping = GeneratePropertyMapping("result", prop);
|
||||
sb.AppendLine($" {prop.ProtoPropertyName} = {mapping},");
|
||||
}
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine();
|
||||
|
||||
// Add collection properties with AddRange
|
||||
foreach (var prop in collectionProps)
|
||||
{
|
||||
var elementTypeName = prop.ElementType;
|
||||
if (!string.IsNullOrEmpty(elementTypeName))
|
||||
{
|
||||
var lastDot = elementTypeName.LastIndexOf('.');
|
||||
if (lastDot >= 0)
|
||||
{
|
||||
elementTypeName = elementTypeName.Substring(lastDot + 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Count > 0)
|
||||
{
|
||||
// Complex element type - map properties
|
||||
sb.AppendLine($" if (result.{prop.Name} != null)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" protoResult.{prop.ProtoPropertyName}.AddRange(result.{prop.Name}.Select(item => new {elementTypeName}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var nestedProp in prop.ElementNestedProperties)
|
||||
{
|
||||
var nestedMapping = GeneratePropertyMapping("item", nestedProp);
|
||||
if (nestedMapping != null)
|
||||
{
|
||||
sb.AppendLine($" {nestedProp.ProtoPropertyName} = {nestedMapping},");
|
||||
}
|
||||
}
|
||||
sb.AppendLine(" }));");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Simple element type or no nested properties - direct copy
|
||||
sb.AppendLine($" if (result.{prop.Name} != null)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine($" protoResult.{prop.ProtoPropertyName}.AddRange(result.{prop.Name});");
|
||||
sb.AppendLine(" }");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine($" return new {responseType} {{ Result = protoResult }};");
|
||||
}
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" };");
|
||||
}
|
||||
|
||||
sb.AppendLine(" }");
|
||||
@ -1672,9 +1878,231 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
"System.Guid"
|
||||
};
|
||||
|
||||
return primitiveTypes.Contains(typeName) ||
|
||||
typeName.StartsWith("System.Nullable<") ||
|
||||
typeName.EndsWith("?");
|
||||
// Handle nullable types - extract the underlying type first
|
||||
var typeToCheck = typeName;
|
||||
if (typeName.EndsWith("?"))
|
||||
{
|
||||
typeToCheck = typeName.Substring(0, typeName.Length - 1);
|
||||
}
|
||||
else if (typeName.StartsWith("System.Nullable<") && typeName.EndsWith(">"))
|
||||
{
|
||||
typeToCheck = typeName.Substring("System.Nullable<".Length, typeName.Length - "System.Nullable<".Length - 1);
|
||||
}
|
||||
|
||||
return primitiveTypes.Contains(typeToCheck);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the mapping expression for a property from C# domain type to proto type
|
||||
/// </summary>
|
||||
private static string GeneratePropertyMapping(string sourceVar, PropertyInfo prop)
|
||||
{
|
||||
var accessor = $"{sourceVar}.{prop.Name}";
|
||||
|
||||
// Handle decimal → string conversion
|
||||
if (prop.IsDecimal)
|
||||
{
|
||||
if (prop.IsNullable)
|
||||
return $"{accessor}?.ToString() ?? \"\"";
|
||||
return $"{accessor}.ToString()";
|
||||
}
|
||||
|
||||
// Handle DateTime/DateTimeOffset → string conversion
|
||||
if (prop.IsDateTime)
|
||||
{
|
||||
if (prop.IsNullable)
|
||||
return $"{accessor}?.ToString(\"O\") ?? \"\"";
|
||||
return $"{accessor}.ToString(\"O\")";
|
||||
}
|
||||
|
||||
// Handle enum → proto enum conversion (cast with same underlying value)
|
||||
if (prop.IsEnum)
|
||||
{
|
||||
// Extract just the enum name (without namespace) for the proto enum type
|
||||
var enumTypeName = prop.Type;
|
||||
var lastDot = enumTypeName.LastIndexOf('.');
|
||||
if (lastDot >= 0)
|
||||
{
|
||||
enumTypeName = enumTypeName.Substring(lastDot + 1);
|
||||
}
|
||||
// Remove nullable suffix if present
|
||||
if (enumTypeName.EndsWith("?"))
|
||||
{
|
||||
enumTypeName = enumTypeName.Substring(0, enumTypeName.Length - 1);
|
||||
}
|
||||
return $"({enumTypeName})(int){accessor}";
|
||||
}
|
||||
|
||||
// Handle collection properties - these need special handling with AddRange
|
||||
if (prop.IsList)
|
||||
{
|
||||
// Return a special marker indicating this needs AddRange treatment
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle nullable strings with null coalescing
|
||||
if (prop.IsNullable && (prop.Type.Contains("string") || prop.Type == "string" || prop.Type == "string?"))
|
||||
{
|
||||
return $"{accessor} ?? \"\"";
|
||||
}
|
||||
|
||||
// Handle collections (nested repeated fields)
|
||||
if (prop.IsList && prop.IsElementComplexType && prop.ElementNestedProperties != null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"new global::Google.Protobuf.Collections.RepeatedField<{prop.ElementType}>()");
|
||||
// Note: For nested collections, we'd need to generate AddRange call separately
|
||||
// For now, return the accessor - caller should handle complex cases
|
||||
return accessor;
|
||||
}
|
||||
|
||||
// Handle complex nested types
|
||||
if (prop.IsComplexType && prop.NestedProperties.Count > 0)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"new {prop.Type}");
|
||||
sb.AppendLine();
|
||||
sb.Append(" {");
|
||||
foreach (var nestedProp in prop.NestedProperties)
|
||||
{
|
||||
var nestedMapping = GeneratePropertyMapping(accessor, nestedProp);
|
||||
sb.AppendLine();
|
||||
sb.Append($" {nestedProp.ProtoPropertyName} = {nestedMapping},");
|
||||
}
|
||||
sb.AppendLine();
|
||||
sb.Append(" }");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// Default: direct assignment (works for primitives and simple types)
|
||||
return accessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the mapping expression for a request property from proto type to C# domain type
|
||||
/// </summary>
|
||||
private static string GenerateRequestPropertyMapping(PropertyInfo prop)
|
||||
{
|
||||
// Use the proto property name (computed from snake_case conversion)
|
||||
var accessor = $"request.{prop.ProtoPropertyName}";
|
||||
|
||||
// Handle string → decimal conversion (proto uses string for decimal)
|
||||
if (prop.IsDecimal)
|
||||
{
|
||||
if (prop.IsNullable)
|
||||
return $"string.IsNullOrEmpty({accessor}) ? null : decimal.Parse({accessor})";
|
||||
return $"decimal.Parse({accessor})";
|
||||
}
|
||||
|
||||
// Handle string → DateTime conversion (proto uses string for DateTime)
|
||||
if (prop.IsDateTime)
|
||||
{
|
||||
if (prop.IsNullable)
|
||||
return $"string.IsNullOrEmpty({accessor}) ? null : global::System.DateTime.Parse({accessor})";
|
||||
return $"global::System.DateTime.Parse({accessor})";
|
||||
}
|
||||
|
||||
// Handle proto enum → domain enum conversion
|
||||
if (prop.IsEnum)
|
||||
{
|
||||
// Cast from proto enum (int) to domain enum
|
||||
return $"({prop.Type})(int){accessor}";
|
||||
}
|
||||
|
||||
// Handle nullable numeric types (int?, long?, etc.)
|
||||
// In protobuf3, numeric fields default to 0 when not set.
|
||||
// For nullable C# types, we treat 0 as null (unset).
|
||||
if (prop.IsNullable && IsNullableNumericType(prop.Type))
|
||||
{
|
||||
return $"{accessor} == 0 ? null : {accessor}";
|
||||
}
|
||||
|
||||
// Default: direct assignment (works for compatible types like int, long, string, bool)
|
||||
return accessor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the type is a nullable numeric type (int?, long?, short?, etc.)
|
||||
/// </summary>
|
||||
private static bool IsNullableNumericType(string typeName)
|
||||
{
|
||||
// Check for common nullable numeric types
|
||||
var nullableNumericTypes = new[]
|
||||
{
|
||||
"int?", "System.Int32?", "global::System.Int32?",
|
||||
"long?", "System.Int64?", "global::System.Int64?",
|
||||
"short?", "System.Int16?", "global::System.Int16?",
|
||||
"byte?", "System.Byte?", "global::System.Byte?",
|
||||
"uint?", "System.UInt32?", "global::System.UInt32?",
|
||||
"ulong?", "System.UInt64?", "global::System.UInt64?",
|
||||
"ushort?", "System.UInt16?", "global::System.UInt16?",
|
||||
"sbyte?", "System.SByte?", "global::System.SByte?",
|
||||
"float?", "System.Single?", "global::System.Single?",
|
||||
"double?", "System.Double?", "global::System.Double?",
|
||||
};
|
||||
return nullableNumericTypes.Contains(typeName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a type is a collection type and returns the element type if so
|
||||
/// </summary>
|
||||
private static (bool IsCollection, INamedTypeSymbol? ElementType) GetCollectionElementType(ITypeSymbol type)
|
||||
{
|
||||
// Check if it's a named type
|
||||
if (type is not INamedTypeSymbol namedType)
|
||||
return (false, null);
|
||||
|
||||
// Explicitly exclude string (it implements IEnumerable<char> but we don't want to treat it as a collection)
|
||||
var typeDisplayName = type.ToDisplayString();
|
||||
if (typeDisplayName == "string" || typeDisplayName == "System.String" ||
|
||||
typeDisplayName == "string?" || typeDisplayName == "System.String?")
|
||||
{
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
// Check for common collection types: List<T>, IList<T>, IEnumerable<T>, ICollection<T>, T[]
|
||||
var typeFullName = namedType.OriginalDefinition.ToDisplayString();
|
||||
|
||||
// Check for array
|
||||
if (type is IArrayTypeSymbol arrayType)
|
||||
{
|
||||
if (arrayType.ElementType is INamedTypeSymbol arrayElementType)
|
||||
return (true, arrayElementType);
|
||||
return (true, null);
|
||||
}
|
||||
|
||||
// Collection type patterns to check
|
||||
var collectionPatterns = new[]
|
||||
{
|
||||
"System.Collections.Generic.List<T>",
|
||||
"System.Collections.Generic.IList<T>",
|
||||
"System.Collections.Generic.IEnumerable<T>",
|
||||
"System.Collections.Generic.ICollection<T>",
|
||||
"System.Collections.Generic.IReadOnlyList<T>",
|
||||
"System.Collections.Generic.IReadOnlyCollection<T>"
|
||||
};
|
||||
|
||||
foreach (var pattern in collectionPatterns)
|
||||
{
|
||||
if (typeFullName == pattern && namedType.TypeArguments.Length > 0)
|
||||
{
|
||||
var elementType = namedType.TypeArguments[0] as INamedTypeSymbol;
|
||||
return (true, elementType);
|
||||
}
|
||||
}
|
||||
|
||||
// Also check interfaces for types that implement IEnumerable<T>
|
||||
foreach (var iface in namedType.AllInterfaces)
|
||||
{
|
||||
var ifaceFullName = iface.OriginalDefinition.ToDisplayString();
|
||||
if (ifaceFullName == "System.Collections.Generic.IEnumerable<T>" && iface.TypeArguments.Length > 0)
|
||||
{
|
||||
var elementType = iface.TypeArguments[0] as INamedTypeSymbol;
|
||||
return (true, elementType);
|
||||
}
|
||||
}
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
private static string GenerateDynamicQueryMessages(List<DynamicQueryInfo> dynamicQueries, string rootNamespace)
|
||||
@ -2232,6 +2660,7 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
properties.Add(new PropertyInfo
|
||||
{
|
||||
Name = prop.Name,
|
||||
ProtoPropertyName = ToPascalCaseHelper(ToSnakeCaseHelper(prop.Name)),
|
||||
Type = propType,
|
||||
FullyQualifiedType = propType,
|
||||
ProtoType = protoType,
|
||||
@ -2382,8 +2811,27 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
{
|
||||
if (string.IsNullOrEmpty(snakeCase)) return snakeCase;
|
||||
var parts = snakeCase.Split('_');
|
||||
return string.Join("", parts.Select(p =>
|
||||
p.Length > 0 ? char.ToUpperInvariant(p[0]) + p.Substring(1).ToLowerInvariant() : ""));
|
||||
var result = new StringBuilder();
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (part.Length == 0) continue;
|
||||
// Capitalize the first letter
|
||||
result.Append(char.ToUpperInvariant(part[0]));
|
||||
// Process remaining characters - capitalize letters that follow digits
|
||||
for (int i = 1; i < part.Length; i++)
|
||||
{
|
||||
char c = part[i];
|
||||
if (i > 0 && char.IsLetter(c) && char.IsDigit(part[i - 1]))
|
||||
{
|
||||
result.Append(char.ToUpperInvariant(c));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append(char.ToLowerInvariant(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,11 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
|
||||
public class PropertyInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
/// <summary>
|
||||
/// The property name as generated by Grpc.Tools from the proto field name.
|
||||
/// This may differ from the C# property name due to casing differences.
|
||||
/// </summary>
|
||||
public string ProtoPropertyName { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string FullyQualifiedType { get; set; }
|
||||
public string ProtoType { get; set; }
|
||||
@ -51,6 +56,7 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
|
||||
public PropertyInfo()
|
||||
{
|
||||
Name = string.Empty;
|
||||
ProtoPropertyName = string.Empty;
|
||||
Type = string.Empty;
|
||||
FullyQualifiedType = string.Empty;
|
||||
ProtoType = string.Empty;
|
||||
|
||||
@ -13,6 +13,26 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
|
||||
public string HandlerInterfaceName { get; set; }
|
||||
public List<PropertyInfo> ResultProperties { get; set; }
|
||||
public bool IsResultPrimitiveType { get; set; }
|
||||
/// <summary>
|
||||
/// True if the result type is a collection (List, IEnumerable, etc.)
|
||||
/// </summary>
|
||||
public bool IsResultCollection { get; set; }
|
||||
/// <summary>
|
||||
/// The element type name if IsResultCollection is true
|
||||
/// </summary>
|
||||
public string ResultElementType { get; set; }
|
||||
/// <summary>
|
||||
/// The fully qualified element type name if IsResultCollection is true
|
||||
/// </summary>
|
||||
public string ResultElementTypeFullyQualified { get; set; }
|
||||
/// <summary>
|
||||
/// True if the result type is nullable (ends with ? or is Nullable<T>)
|
||||
/// </summary>
|
||||
public bool IsResultNullable { get; set; }
|
||||
/// <summary>
|
||||
/// The result type name without the nullable annotation (e.g., "CnfFoodDetailItem" instead of "CnfFoodDetailItem?")
|
||||
/// </summary>
|
||||
public string ResultTypeWithoutNullable { get; set; }
|
||||
|
||||
public QueryInfo()
|
||||
{
|
||||
@ -25,6 +45,11 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
|
||||
HandlerInterfaceName = string.Empty;
|
||||
ResultProperties = new List<PropertyInfo>();
|
||||
IsResultPrimitiveType = false;
|
||||
IsResultCollection = false;
|
||||
ResultElementType = string.Empty;
|
||||
ResultElementTypeFullyQualified = string.Empty;
|
||||
IsResultNullable = false;
|
||||
ResultTypeWithoutNullable = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -403,9 +403,14 @@ internal class ProtoFileGenerator
|
||||
_messagesBuilder.AppendLine();
|
||||
|
||||
// Generate complex type message after closing the response message
|
||||
if (resultType != null && IsComplexType(resultType))
|
||||
// Use GetElementOrUnderlyingType to extract element type from collections (e.g., List<CnfFoodItem> -> CnfFoodItem)
|
||||
if (resultType != null)
|
||||
{
|
||||
GenerateComplexTypeMessage(resultType as INamedTypeSymbol);
|
||||
var underlyingType = ProtoFileTypeMapper.GetElementOrUnderlyingType(resultType);
|
||||
if (IsComplexType(underlyingType) && underlyingType is INamedTypeSymbol namedType)
|
||||
{
|
||||
GenerateComplexTypeMessage(namedType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,5 +2,15 @@
|
||||
<PropertyGroup>
|
||||
<!-- Marker to indicate Svrnty.CQRS.Grpc.Generators is referenced -->
|
||||
<SvrntyCqrsGrpcGeneratorsVersion>$(SvrntyCqrsGrpcGeneratorsVersion)</SvrntyCqrsGrpcGeneratorsVersion>
|
||||
|
||||
<!-- Path resolution for both NuGet package and project reference -->
|
||||
<_SvrntyCqrsGrpcGeneratorsPath Condition="Exists('$(MSBuildThisFileDirectory)..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll</_SvrntyCqrsGrpcGeneratorsPath>
|
||||
<_SvrntyCqrsGrpcGeneratorsPath Condition="'$(_SvrntyCqrsGrpcGeneratorsPath)' == '' AND Exists('$(MSBuildThisFileDirectory)..\bin\Debug\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)..\bin\Debug\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll</_SvrntyCqrsGrpcGeneratorsPath>
|
||||
<_SvrntyCqrsGrpcGeneratorsPath Condition="'$(_SvrntyCqrsGrpcGeneratorsPath)' == '' AND Exists('$(MSBuildThisFileDirectory)..\bin\Release\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)..\bin\Release\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll</_SvrntyCqrsGrpcGeneratorsPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Explicitly add the generator to the Analyzer ItemGroup -->
|
||||
<ItemGroup Condition="'$(_SvrntyCqrsGrpcGeneratorsPath)' != ''">
|
||||
<Analyzer Include="$(_SvrntyCqrsGrpcGeneratorsPath)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user