Compare commits
4 Commits
feat/nutri
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bd43bc9bde | |||
| 661f5b4b1c | |||
| 99aebcf314 | |||
|
|
f76dbb1a97 |
@ -360,7 +360,11 @@ 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),
|
||||
IsBinaryType = IsBinaryType(property.Type),
|
||||
IsStream = IsStreamType(property.Type),
|
||||
};
|
||||
|
||||
// If it's a list, extract element type info
|
||||
@ -371,6 +375,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 +472,36 @@ 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 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)
|
||||
{
|
||||
if (type is IArrayTypeSymbol)
|
||||
@ -495,6 +530,41 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the value expression for converting from proto type to C# type
|
||||
/// </summary>
|
||||
private static string GetProtoToCSharpConversion(PropertyInfo prop, string sourceExpr)
|
||||
{
|
||||
if (prop.IsGuid)
|
||||
{
|
||||
if (prop.IsNullable)
|
||||
return $"string.IsNullOrEmpty({sourceExpr}) ? null : System.Guid.Parse({sourceExpr})";
|
||||
return $"System.Guid.Parse({sourceExpr})";
|
||||
}
|
||||
if (prop.IsEnum)
|
||||
{
|
||||
// Enum is already handled correctly in proto - values match
|
||||
return $"{sourceExpr}";
|
||||
}
|
||||
// Default: direct assignment
|
||||
return $"{sourceExpr}!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates the value expression for converting from C# type to proto type
|
||||
/// </summary>
|
||||
private static string GetCSharpToProtoConversion(PropertyInfo prop, string sourceExpr)
|
||||
{
|
||||
if (prop.IsGuid)
|
||||
{
|
||||
if (prop.IsNullable)
|
||||
return $"{sourceExpr}?.ToString() ?? \"\"";
|
||||
return $"{sourceExpr}.ToString()";
|
||||
}
|
||||
// Default: direct assignment
|
||||
return sourceExpr;
|
||||
}
|
||||
|
||||
private static void ExtractNestedProperties(INamedTypeSymbol type, List<PropertyInfo> nestedProperties)
|
||||
{
|
||||
var properties = type.GetMembers().OfType<IPropertySymbol>()
|
||||
@ -532,6 +602,9 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
|
||||
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 propInfo = new PropertyInfo
|
||||
{
|
||||
@ -541,12 +614,17 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
ProtoType = string.Empty,
|
||||
FieldNumber = 0,
|
||||
IsComplexType = IsUserDefinedComplexType(property.Type),
|
||||
IsReadOnly = isReadOnly,
|
||||
// Type metadata
|
||||
IsNullable = IsNullableType(property.Type),
|
||||
IsEnum = IsEnumType(property.Type),
|
||||
IsDecimal = IsDecimalType(property.Type),
|
||||
IsDateTime = IsDateTimeType(property.Type),
|
||||
IsGuid = IsGuidType(property.Type),
|
||||
IsJsonElement = IsJsonElementType(property.Type),
|
||||
IsList = IsListOrCollection(property.Type),
|
||||
IsBinaryType = IsBinaryType(property.Type),
|
||||
IsStream = IsStreamType(property.Type),
|
||||
};
|
||||
|
||||
// If it's a list, extract element type info
|
||||
@ -557,6 +635,14 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
{
|
||||
propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
propInfo.IsElementComplexType = IsUserDefinedComplexType(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
|
||||
@ -606,6 +692,11 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
// Complex list: map each element
|
||||
return GenerateComplexListMapping(prop, source, indent);
|
||||
}
|
||||
else if (prop.IsElementGuid)
|
||||
{
|
||||
// List<string> from proto -> List<Guid> in C#
|
||||
return $"{indent}{prop.Name} = {source}?.Select(x => System.Guid.Parse(x)).ToList(),";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Primitive list: just ToList()
|
||||
@ -645,6 +736,42 @@ 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 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)
|
||||
if (prop.IsComplexType)
|
||||
{
|
||||
@ -663,6 +790,9 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
|
||||
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 nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, "x", indent + " ");
|
||||
sb.AppendLine(nestedAssignment);
|
||||
@ -680,6 +810,9 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
|
||||
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 + " ");
|
||||
sb.AppendLine(nestedAssignment);
|
||||
}
|
||||
@ -712,9 +845,39 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
}
|
||||
}
|
||||
|
||||
// Handle lists
|
||||
// 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
|
||||
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 with complex element types
|
||||
if (prop.IsList)
|
||||
{
|
||||
if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any())
|
||||
{
|
||||
return GenerateComplexListMapping(prop, source, indent);
|
||||
}
|
||||
return $"{indent}{prop.Name} = {source}?.ToList(),";
|
||||
}
|
||||
|
||||
@ -728,6 +891,319 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
return $"{indent}{prop.Name} = {source},";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates C# to proto property mapping (reverse of GeneratePropertyAssignment)
|
||||
/// </summary>
|
||||
private static string GenerateResultPropertyMapping(PropertyInfo prop, string sourceVar, string indent)
|
||||
{
|
||||
var source = $"{sourceVar}.{prop.Name}";
|
||||
|
||||
// Handle lists
|
||||
if (prop.IsList)
|
||||
{
|
||||
if (prop.IsElementComplexType)
|
||||
{
|
||||
// Complex list: map each element to proto type
|
||||
return GenerateResultComplexListMapping(prop, source, indent);
|
||||
}
|
||||
else if (prop.IsElementGuid)
|
||||
{
|
||||
// List<Guid> -> repeated string
|
||||
return $"{indent}{prop.Name} = {{ {source}?.Select(x => x.ToString()) ?? Enumerable.Empty<string>() }},";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Primitive list: just copy
|
||||
return $"{indent}{prop.Name} = {{ {source} ?? Enumerable.Empty<{prop.Type.Replace("System.Collections.Generic.List<", "").Replace(">", "").Replace("?", "")}>() }},";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Guid (C# Guid -> proto string)
|
||||
if (prop.IsGuid)
|
||||
{
|
||||
if (prop.IsNullable)
|
||||
{
|
||||
return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{indent}{prop.Name} = {source}.ToString(),";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle decimals (C# decimal -> proto string)
|
||||
if (prop.IsDecimal)
|
||||
{
|
||||
if (prop.IsNullable)
|
||||
{
|
||||
return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{indent}{prop.Name} = {source}.ToString(),";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle DateTime (C# DateTime -> proto Timestamp)
|
||||
if (prop.IsDateTime)
|
||||
{
|
||||
if (prop.IsNullable)
|
||||
{
|
||||
return $"{indent}{prop.Name} = {source}.HasValue ? Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(System.DateTime.SpecifyKind({source}.Value, System.DateTimeKind.Utc)) : null,";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{indent}{prop.Name} = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(System.DateTime.SpecifyKind({source}, System.DateTimeKind.Utc)),";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle TimeSpan (C# TimeSpan -> proto Duration)
|
||||
if (prop.FullyQualifiedType.Contains("System.TimeSpan"))
|
||||
{
|
||||
if (prop.IsNullable)
|
||||
{
|
||||
return $"{indent}{prop.Name} = {source}.HasValue ? Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan({source}.Value) : null,";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{indent}{prop.Name} = Google.Protobuf.WellKnownTypes.Duration.FromTimeSpan({source}),";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle enums (C# enum -> proto int32)
|
||||
if (prop.IsEnum)
|
||||
{
|
||||
return $"{indent}{prop.Name} = (int){source},";
|
||||
}
|
||||
|
||||
// Handle 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)
|
||||
if (prop.IsComplexType)
|
||||
{
|
||||
return GenerateResultComplexObjectMapping(prop, source, indent);
|
||||
}
|
||||
|
||||
// Default: direct assignment (strings, ints, bools, etc.)
|
||||
if (prop.IsNullable)
|
||||
{
|
||||
if (prop.Type.Contains("string"))
|
||||
{
|
||||
return $"{indent}{prop.Name} = {source} ?? string.Empty,";
|
||||
}
|
||||
// Handle nullable primitives (long?, int?, etc.) - use default value
|
||||
return $"{indent}{prop.Name} = {source} ?? default,";
|
||||
}
|
||||
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 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
|
||||
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)
|
||||
{
|
||||
if (prop.IsNullable)
|
||||
{
|
||||
return $"{indent}{prop.Name} = (int)({source} ?? default),";
|
||||
}
|
||||
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
|
||||
if (prop.IsList)
|
||||
{
|
||||
if (prop.IsElementGuid)
|
||||
{
|
||||
return $"{indent}{prop.Name} = {{ {source}?.Select(x => x.ToString()) ?? Enumerable.Empty<string>() }},";
|
||||
}
|
||||
else if (prop.IsElementComplexType)
|
||||
{
|
||||
// Complex list elements need mapping
|
||||
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)
|
||||
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.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,";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{indent}{prop.Name} = new {typeName}(),";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle nullable strings
|
||||
if (prop.IsNullable && prop.Type.Contains("string"))
|
||||
{
|
||||
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
|
||||
return $"{indent}{prop.Name} = {source},";
|
||||
}
|
||||
|
||||
private static QueryInfo? ExtractQueryInfo(INamedTypeSymbol queryType, INamedTypeSymbol resultType)
|
||||
{
|
||||
var queryInfo = new QueryInfo
|
||||
@ -759,14 +1235,55 @@ 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),
|
||||
IsBinaryType = IsBinaryType(property.Type),
|
||||
IsStream = IsStreamType(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);
|
||||
|
||||
// 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
|
||||
else if (propInfo.IsComplexType)
|
||||
{
|
||||
var unwrapped = UnwrapNullableType(property.Type);
|
||||
if (unwrapped is INamedTypeSymbol namedType)
|
||||
{
|
||||
ExtractNestedPropertiesWithTypeInfo(namedType, propInfo.NestedProperties);
|
||||
}
|
||||
}
|
||||
|
||||
queryInfo.ResultProperties.Add(propInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@ -781,13 +1298,48 @@ 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),
|
||||
IsBinaryType = IsBinaryType(property.Type),
|
||||
IsStream = IsStreamType(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 +1584,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 +1601,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 +1668,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 +2109,39 @@ 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 }};");
|
||||
|
||||
// Generate response with mapping if complex type
|
||||
if (command.IsResultPrimitiveType)
|
||||
{
|
||||
// Handle primitive type result conversion (e.g., Guid.ToString())
|
||||
if (command.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 {command.ResultType}");
|
||||
sb.AppendLine(" {");
|
||||
foreach (var prop in command.ResultProperties)
|
||||
{
|
||||
var assignment = GenerateResultPropertyMapping(prop, "result", " ");
|
||||
sb.AppendLine(assignment);
|
||||
}
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" };");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -1617,7 +2204,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 +2213,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 +2273,23 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
"System.Guid"
|
||||
};
|
||||
|
||||
return primitiveTypes.Contains(typeName) ||
|
||||
typeName.StartsWith("System.Nullable<") ||
|
||||
typeName.EndsWith("?");
|
||||
if (primitiveTypes.Contains(typeName))
|
||||
return true;
|
||||
|
||||
// Handle nullable types - check if the underlying type is primitive
|
||||
if (typeName.EndsWith("?"))
|
||||
{
|
||||
var underlyingType = typeName.Substring(0, typeName.Length - 1);
|
||||
return IsPrimitiveType(underlyingType);
|
||||
}
|
||||
|
||||
if (typeName.StartsWith("System.Nullable<") && typeName.EndsWith(">"))
|
||||
{
|
||||
var underlyingType = typeName.Substring("System.Nullable<".Length, typeName.Length - "System.Nullable<".Length - 1);
|
||||
return IsPrimitiveType(underlyingType);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string GenerateDynamicQueryMessages(List<DynamicQueryInfo> dynamicQueries, string rootNamespace)
|
||||
@ -1910,10 +2525,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();
|
||||
|
||||
@ -2096,7 +2711,8 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
sb.AppendLine(" var domainElementType = domainProp.PropertyType.IsArray");
|
||||
sb.AppendLine(" ? domainProp.PropertyType.GetElementType()");
|
||||
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(" foreach (var item in enumerable)");
|
||||
sb.AppendLine(" {");
|
||||
@ -2120,14 +2736,66 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
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(" else if (!domainProp.PropertyType.IsPrimitive && ");
|
||||
sb.AppendLine(" domainProp.PropertyType != typeof(string) && ");
|
||||
sb.AppendLine(" !domainProp.PropertyType.IsEnum &&");
|
||||
sb.AppendLine(" !domainProp.PropertyType.IsValueType)");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" // Get the proto field type and recursively map");
|
||||
sb.AppendLine(" var protoFieldType = protoAccessor.GetValue(proto)?.GetType() ?? protoField.MessageType?.ClrType;");
|
||||
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.FieldType == Google.Protobuf.Reflection.FieldType.Message ? protoField.MessageType?.ClrType : null);");
|
||||
sb.AppendLine(" if (protoFieldType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoFieldType))");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" var mapMethod = typeof(DynamicQueryServiceImpl).GetMethod(\"MapToProtoModel\", ");
|
||||
@ -2143,6 +2811,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 +2925,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 +3016,10 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
{
|
||||
sb.AppendLine($" {protoFieldName} = domain.{prop.Name}.ToString(),");
|
||||
}
|
||||
else if (prop.IsGuid)
|
||||
{
|
||||
sb.AppendLine($" {protoFieldName} = domain.{prop.Name}.ToString(),");
|
||||
}
|
||||
else if (prop.IsEnum)
|
||||
{
|
||||
// Map domain enum to proto enum - get simple type name
|
||||
|
||||
@ -44,8 +44,14 @@ 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 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 bool IsElementComplexType { get; set; }
|
||||
public bool IsElementGuid { get; set; }
|
||||
public List<PropertyInfo>? ElementNestedProperties { get; set; }
|
||||
|
||||
public PropertyInfo()
|
||||
@ -61,7 +67,13 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
|
||||
IsNullable = false;
|
||||
IsDecimal = false;
|
||||
IsDateTime = false;
|
||||
IsGuid = false;
|
||||
IsJsonElement = false;
|
||||
IsBinaryType = false;
|
||||
IsStream = false;
|
||||
IsReadOnly = false;
|
||||
IsElementComplexType = false;
|
||||
IsElementGuid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,7 +320,9 @@ internal class ProtoFileGenerator
|
||||
|
||||
var properties = type.GetMembers()
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public)
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public &&
|
||||
!p.IsIndexer &&
|
||||
!ProtoFileTypeMapper.IsCollectionInternalProperty(p.Name))
|
||||
.ToList();
|
||||
|
||||
// Collect nested complex types to generate after closing this message
|
||||
@ -423,48 +425,73 @@ internal class ProtoFileGenerator
|
||||
_messagesBuilder.AppendLine($"// {type.Name} entity");
|
||||
_messagesBuilder.AppendLine($"message {type.Name} {{");
|
||||
|
||||
var properties = type.GetMembers()
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public)
|
||||
.ToList();
|
||||
|
||||
// Collect nested complex types to generate after closing this message
|
||||
var nestedComplexTypes = new List<INamedTypeSymbol>();
|
||||
|
||||
int fieldNumber = 1;
|
||||
foreach (var prop in properties)
|
||||
// Check if this type is a collection (implements IList<T>, ICollection<T>, etc.)
|
||||
var collectionElementType = ProtoFileTypeMapper.GetCollectionElementTypeByInterface(type);
|
||||
if (collectionElementType != null)
|
||||
{
|
||||
if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type))
|
||||
{
|
||||
_messagesBuilder.AppendLine($" // Skipped: {prop.Name} - unsupported type {prop.Type.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var protoType = ProtoFileTypeMapper.MapType(prop.Type, out var needsImport, out var importPath);
|
||||
// This type is a collection - generate a single repeated field for items
|
||||
var protoElementType = ProtoFileTypeMapper.MapType(collectionElementType, out var needsImport, out var importPath);
|
||||
if (needsImport && importPath != null)
|
||||
{
|
||||
_requiredImports.Add(importPath);
|
||||
}
|
||||
|
||||
var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name);
|
||||
_messagesBuilder.AppendLine($" {protoType} {fieldName} = {fieldNumber};");
|
||||
_messagesBuilder.AppendLine($" repeated {protoElementType} items = 1;");
|
||||
|
||||
// Track enums for later generation
|
||||
var enumType = ProtoFileTypeMapper.GetEnumType(prop.Type);
|
||||
if (enumType != null)
|
||||
// Track the element type for nested generation
|
||||
if (IsComplexType(collectionElementType) && collectionElementType is INamedTypeSymbol elementNamedType)
|
||||
{
|
||||
TrackEnumType(enumType);
|
||||
nestedComplexTypes.Add(elementNamedType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not a collection - generate properties as usual
|
||||
var properties = type.GetMembers()
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public &&
|
||||
!p.IsIndexer &&
|
||||
!ProtoFileTypeMapper.IsCollectionInternalProperty(p.Name))
|
||||
.ToList();
|
||||
|
||||
// Collect complex types to generate after this message is closed
|
||||
// Use GetElementOrUnderlyingType to extract element type from collections
|
||||
var underlyingType = ProtoFileTypeMapper.GetElementOrUnderlyingType(prop.Type);
|
||||
if (IsComplexType(underlyingType) && underlyingType is INamedTypeSymbol namedType)
|
||||
int fieldNumber = 1;
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
nestedComplexTypes.Add(namedType);
|
||||
}
|
||||
if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type))
|
||||
{
|
||||
_messagesBuilder.AppendLine($" // Skipped: {prop.Name} - unsupported type {prop.Type.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
fieldNumber++;
|
||||
var protoType = ProtoFileTypeMapper.MapType(prop.Type, out var needsImport, out var importPath);
|
||||
if (needsImport && importPath != null)
|
||||
{
|
||||
_requiredImports.Add(importPath);
|
||||
}
|
||||
|
||||
var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name);
|
||||
_messagesBuilder.AppendLine($" {protoType} {fieldName} = {fieldNumber};");
|
||||
|
||||
// Track enums for later generation
|
||||
var enumType = ProtoFileTypeMapper.GetEnumType(prop.Type);
|
||||
if (enumType != null)
|
||||
{
|
||||
TrackEnumType(enumType);
|
||||
}
|
||||
|
||||
// Collect complex types to generate after this message is closed
|
||||
// Use GetElementOrUnderlyingType to extract element type from collections
|
||||
var underlyingType = ProtoFileTypeMapper.GetElementOrUnderlyingType(prop.Type);
|
||||
if (IsComplexType(underlyingType) && underlyingType is INamedTypeSymbol namedType)
|
||||
{
|
||||
nestedComplexTypes.Add(namedType);
|
||||
}
|
||||
|
||||
fieldNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
_messagesBuilder.AppendLine("}");
|
||||
@ -737,7 +764,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
|
||||
});
|
||||
@ -755,7 +782,9 @@ internal class ProtoFileGenerator
|
||||
int fieldNumber = 1;
|
||||
|
||||
foreach (var prop in type.GetMembers().OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public))
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public &&
|
||||
!p.IsIndexer &&
|
||||
!ProtoFileTypeMapper.IsCollectionInternalProperty(p.Name)))
|
||||
{
|
||||
if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type))
|
||||
continue;
|
||||
@ -817,14 +846,16 @@ internal class ProtoFileGenerator
|
||||
|
||||
foreach (var prop in notification.Properties)
|
||||
{
|
||||
var protoType = ProtoFileTypeMapper.MapType(
|
||||
_compilation.GetTypeByMetadataName(prop.FullyQualifiedType) ??
|
||||
GetTypeFromName(prop.FullyQualifiedType),
|
||||
out var needsImport, out var importPath);
|
||||
var typeSymbol = _compilation.GetTypeByMetadataName(prop.FullyQualifiedType) ??
|
||||
GetTypeFromName(prop.FullyQualifiedType);
|
||||
|
||||
if (needsImport && importPath != null)
|
||||
if (typeSymbol != null)
|
||||
{
|
||||
_requiredImports.Add(importPath);
|
||||
ProtoFileTypeMapper.MapType(typeSymbol, out var needsImport, out var importPath);
|
||||
if (needsImport && importPath != null)
|
||||
{
|
||||
_requiredImports.Add(importPath);
|
||||
}
|
||||
}
|
||||
|
||||
var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name);
|
||||
|
||||
@ -20,6 +20,68 @@ internal static class ProtoFileTypeMapper
|
||||
// Note: NullableAnnotation.Annotated is for reference type nullability (List<T>?, string?, etc.)
|
||||
// We don't unwrap these - just use the underlying type. Nullable<T> value types are handled later.
|
||||
|
||||
// Handle Nullable<T> value types (e.g., int?, decimal?, enum?) FIRST
|
||||
if (typeSymbol is INamedTypeSymbol nullableType &&
|
||||
nullableType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T &&
|
||||
nullableType.TypeArguments.Length == 1)
|
||||
{
|
||||
// Unwrap the nullable and map the inner type
|
||||
return MapType(nullableType.TypeArguments[0], out needsImport, out importPath);
|
||||
}
|
||||
|
||||
// Handle collections BEFORE basic type checks (to avoid matching List<Guid> as Guid)
|
||||
if (typeSymbol is INamedTypeSymbol collectionType)
|
||||
{
|
||||
// List, IEnumerable, Array, ICollection etc. (but not Nullable<T>)
|
||||
var collectionTypeName = collectionType.Name;
|
||||
if (collectionType.TypeArguments.Length == 1 &&
|
||||
(collectionTypeName.Contains("List") || collectionTypeName.Contains("Collection") ||
|
||||
collectionTypeName.Contains("Enumerable") || collectionTypeName.Contains("Array") ||
|
||||
collectionTypeName.Contains("Set") || collectionTypeName.Contains("IList") ||
|
||||
collectionTypeName.Contains("ICollection") || collectionTypeName.Contains("IEnumerable")))
|
||||
{
|
||||
var elementType = collectionType.TypeArguments[0];
|
||||
var protoElementType = MapType(elementType, out needsImport, out importPath);
|
||||
return $"repeated {protoElementType}";
|
||||
}
|
||||
|
||||
// Dictionary<K, V>
|
||||
if (collectionType.TypeArguments.Length == 2 &&
|
||||
(collectionTypeName.Contains("Dictionary") || collectionTypeName.Contains("IDictionary")))
|
||||
{
|
||||
var keyType = MapType(collectionType.TypeArguments[0], out var keyNeedsImport, out var keyImportPath);
|
||||
var valueType = MapType(collectionType.TypeArguments[1], out var valueNeedsImport, out var valueImportPath);
|
||||
|
||||
// Set import flags if either key or value needs imports
|
||||
if (keyNeedsImport)
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = keyImportPath;
|
||||
}
|
||||
if (valueNeedsImport)
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = valueImportPath; // Note: This only captures last import, may need improvement
|
||||
}
|
||||
|
||||
return $"map<{keyType}, {valueType}>";
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
switch (typeName)
|
||||
{
|
||||
@ -49,81 +111,35 @@ internal static class ProtoFileTypeMapper
|
||||
return "double";
|
||||
case "Byte[]":
|
||||
return "bytes";
|
||||
}
|
||||
|
||||
// Special types that need imports
|
||||
if (fullTypeName.Contains("System.DateTime"))
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = "google/protobuf/timestamp.proto";
|
||||
return "google.protobuf.Timestamp";
|
||||
}
|
||||
|
||||
if (fullTypeName.Contains("System.TimeSpan"))
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = "google/protobuf/duration.proto";
|
||||
return "google.protobuf.Duration";
|
||||
}
|
||||
|
||||
if (fullTypeName.Contains("System.Guid"))
|
||||
{
|
||||
// Guid serialized as string
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (fullTypeName.Contains("System.Decimal") || typeName == "Decimal" || fullTypeName == "decimal")
|
||||
{
|
||||
// Decimal serialized as string (no native decimal in proto)
|
||||
return "string";
|
||||
}
|
||||
|
||||
// Handle Nullable<T> value types (e.g., int?, decimal?, enum?)
|
||||
if (typeSymbol is INamedTypeSymbol nullableType &&
|
||||
nullableType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T &&
|
||||
nullableType.TypeArguments.Length == 1)
|
||||
{
|
||||
// Unwrap the nullable and map the inner type
|
||||
return MapType(nullableType.TypeArguments[0], out needsImport, out importPath);
|
||||
}
|
||||
|
||||
// Collections
|
||||
if (typeSymbol is INamedTypeSymbol collectionType)
|
||||
{
|
||||
// List, IEnumerable, Array, ICollection etc. (but not Nullable<T>)
|
||||
var typeName2 = collectionType.Name;
|
||||
if (collectionType.TypeArguments.Length == 1 &&
|
||||
(typeName2.Contains("List") || typeName2.Contains("Collection") ||
|
||||
typeName2.Contains("Enumerable") || typeName2.Contains("Array") ||
|
||||
typeName2.Contains("Set") || typeName2.Contains("IList") ||
|
||||
typeName2.Contains("ICollection") || typeName2.Contains("IEnumerable")))
|
||||
{
|
||||
var elementType = collectionType.TypeArguments[0];
|
||||
var protoElementType = MapType(elementType, out needsImport, out importPath);
|
||||
return $"repeated {protoElementType}";
|
||||
}
|
||||
|
||||
// Dictionary<K, V>
|
||||
if (collectionType.TypeArguments.Length == 2 &&
|
||||
(typeName.Contains("Dictionary") || typeName.Contains("IDictionary")))
|
||||
{
|
||||
var keyType = MapType(collectionType.TypeArguments[0], out var keyNeedsImport, out var keyImportPath);
|
||||
var valueType = MapType(collectionType.TypeArguments[1], out var valueNeedsImport, out var valueImportPath);
|
||||
|
||||
// Set import flags if either key or value needs imports
|
||||
if (keyNeedsImport)
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = keyImportPath;
|
||||
}
|
||||
if (valueNeedsImport)
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = valueImportPath; // Note: This only captures last import, may need improvement
|
||||
}
|
||||
|
||||
return $"map<{keyType}, {valueType}>";
|
||||
}
|
||||
case "Stream":
|
||||
case "MemoryStream":
|
||||
case "FileStream":
|
||||
return "bytes";
|
||||
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 "DateOnly":
|
||||
// DateOnly serialized as string (YYYY-MM-DD format)
|
||||
return "string";
|
||||
case "TimeOnly":
|
||||
// TimeOnly serialized as string (HH:mm:ss format)
|
||||
return "string";
|
||||
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
|
||||
@ -186,8 +202,8 @@ internal static class ProtoFileTypeMapper
|
||||
var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
|
||||
// Skip these types - they should trigger a warning/error
|
||||
if (fullTypeName.Contains("System.IO.Stream") ||
|
||||
fullTypeName.Contains("System.Threading.CancellationToken") ||
|
||||
// Note: Stream types are now supported (mapped to bytes)
|
||||
if (fullTypeName.Contains("System.Threading.CancellationToken") ||
|
||||
fullTypeName.Contains("System.Threading.Tasks.Task") ||
|
||||
fullTypeName.Contains("System.Collections.Generic.IAsyncEnumerable") ||
|
||||
fullTypeName.Contains("System.Func") ||
|
||||
@ -200,6 +216,31 @@ internal static class ProtoFileTypeMapper
|
||||
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>
|
||||
/// Gets the element type from a collection type, or returns the type itself if not a collection.
|
||||
/// Also unwraps Nullable types.
|
||||
@ -251,4 +292,97 @@ internal static class ProtoFileTypeMapper
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a type is a collection by checking if it implements IList{T}, ICollection{T}, or IEnumerable{T}
|
||||
/// This handles types like NpgsqlPolygon that implement IList{NpgsqlPoint} but aren't named "List"
|
||||
/// </summary>
|
||||
public static bool IsCollectionTypeByInterface(ITypeSymbol typeSymbol)
|
||||
{
|
||||
if (typeSymbol is not INamedTypeSymbol namedType)
|
||||
return false;
|
||||
|
||||
// Skip string (implements IEnumerable<char>)
|
||||
if (namedType.SpecialType == SpecialType.System_String)
|
||||
return false;
|
||||
|
||||
// Check all interfaces for IList<T>, ICollection<T>, or IEnumerable<T>
|
||||
foreach (var iface in namedType.AllInterfaces)
|
||||
{
|
||||
if (iface.IsGenericType && iface.TypeArguments.Length == 1)
|
||||
{
|
||||
var ifaceName = iface.OriginalDefinition.ToDisplayString();
|
||||
if (ifaceName == "System.Collections.Generic.IList<T>" ||
|
||||
ifaceName == "System.Collections.Generic.ICollection<T>" ||
|
||||
ifaceName == "System.Collections.Generic.IEnumerable<T>" ||
|
||||
ifaceName == "System.Collections.Generic.IReadOnlyList<T>" ||
|
||||
ifaceName == "System.Collections.Generic.IReadOnlyCollection<T>")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the element type from a collection that implements IList{T}, ICollection{T}, or IEnumerable{T}
|
||||
/// Returns null if the type is not a collection
|
||||
/// </summary>
|
||||
public static ITypeSymbol? GetCollectionElementTypeByInterface(ITypeSymbol typeSymbol)
|
||||
{
|
||||
if (typeSymbol is not INamedTypeSymbol namedType)
|
||||
return null;
|
||||
|
||||
// Skip string
|
||||
if (namedType.SpecialType == SpecialType.System_String)
|
||||
return null;
|
||||
|
||||
// Prefer IList<T> over ICollection<T> over IEnumerable<T>
|
||||
ITypeSymbol? elementType = null;
|
||||
int priority = 0;
|
||||
|
||||
foreach (var iface in namedType.AllInterfaces)
|
||||
{
|
||||
if (iface.IsGenericType && iface.TypeArguments.Length == 1)
|
||||
{
|
||||
var ifaceName = iface.OriginalDefinition.ToDisplayString();
|
||||
int currentPriority = 0;
|
||||
|
||||
if (ifaceName == "System.Collections.Generic.IList<T>" ||
|
||||
ifaceName == "System.Collections.Generic.IReadOnlyList<T>")
|
||||
currentPriority = 3;
|
||||
else if (ifaceName == "System.Collections.Generic.ICollection<T>" ||
|
||||
ifaceName == "System.Collections.Generic.IReadOnlyCollection<T>")
|
||||
currentPriority = 2;
|
||||
else if (ifaceName == "System.Collections.Generic.IEnumerable<T>")
|
||||
currentPriority = 1;
|
||||
|
||||
if (currentPriority > priority)
|
||||
{
|
||||
priority = currentPriority;
|
||||
elementType = iface.TypeArguments[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return elementType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection-internal properties that should be skipped when generating proto messages
|
||||
/// </summary>
|
||||
private static readonly System.Collections.Generic.HashSet<string> CollectionInternalProperties = new()
|
||||
{
|
||||
"Count", "Capacity", "IsReadOnly", "IsSynchronized", "SyncRoot", "Keys", "Values"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a property name is a collection-internal property that should be skipped
|
||||
/// </summary>
|
||||
public static bool IsCollectionInternalProperty(string propertyName)
|
||||
{
|
||||
return CollectionInternalProperties.Contains(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,111 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "Svrnty.Sample.Grpc";
|
||||
option csharp_namespace = "Generated.Grpc";
|
||||
|
||||
package cqrs;
|
||||
|
||||
// Command service for CQRS operations
|
||||
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;
|
||||
}
|
||||
|
||||
// Placeholder proto file - will be regenerated on next build
|
||||
|
||||
Loading…
Reference in New Issue
Block a user