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