From bd43bc9bde37cc93eabd6142778f685d43c18bab Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 6 Jan 2026 23:25:01 -0500 Subject: [PATCH] Fix gRPC source generator for complex nested types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add DateTime/Timestamp conversion in nested property mapping - Add IsReadOnly property detection to skip computed properties - Extract ElementNestedProperties for complex list element types - Skip read-only properties in GenerateComplexObjectMapping and GenerateComplexListMapping 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs | 254 +++++++++++++++++- .../Models/CommandInfo.cs | 6 + .../ProtoTypeMapper.cs | 47 +++- Svrnty.Sample/Protos/cqrs_services.proto | 108 +------- 4 files changed, 298 insertions(+), 117 deletions(-) diff --git a/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs b/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs index 7a5c07d..8f45d62 100644 --- a/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs +++ b/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs @@ -363,6 +363,8 @@ namespace Svrnty.CQRS.Grpc.Generators 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 @@ -482,6 +484,24 @@ namespace Svrnty.CQRS.Grpc.Generators 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) @@ -582,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 { @@ -591,6 +614,7 @@ 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), @@ -599,6 +623,8 @@ namespace Svrnty.CQRS.Grpc.Generators 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 @@ -610,6 +636,13 @@ 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(); + ExtractNestedPropertiesWithTypeInfo(namedElementType, propInfo.ElementNestedProperties); + } } } // Recursively extract nested properties for complex types @@ -716,6 +749,29 @@ namespace Svrnty.CQRS.Grpc.Generators } } + // Handle binary types (proto ByteString -> C# byte[]/Stream) + if (prop.IsBinaryType) + { + if (prop.IsStream) + { + // ByteString -> MemoryStream + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}?.IsEmpty == false ? new System.IO.MemoryStream({source}.ToByteArray()) : null,"; + } + return $"{indent}{prop.Name} = new System.IO.MemoryStream({source}.ToByteArray()),"; + } + else + { + // ByteString -> byte[] + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source}?.IsEmpty == false ? {source}.ToByteArray() : null,"; + } + return $"{indent}{prop.Name} = {source}.ToByteArray(),"; + } + } + // Handle complex types (single objects) if (prop.IsComplexType) { @@ -734,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); @@ -751,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); } @@ -783,6 +845,19 @@ namespace Svrnty.CQRS.Grpc.Generators } } + // Handle DateTime (proto Timestamp -> C# DateTime) + if (prop.IsDateTime) + { + if (prop.IsNullable) + { + return $"{indent}{prop.Name} = {source} == null ? (System.DateTime?)null : {source}.ToDateTime(),"; + } + else + { + return $"{indent}{prop.Name} = {source}.ToDateTime(),"; + } + } + // Handle Guid if (prop.IsGuid) { @@ -796,9 +871,13 @@ namespace Svrnty.CQRS.Grpc.Generators } } - // Handle lists + // Handle lists with complex element types if (prop.IsList) { + if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any()) + { + return GenerateComplexListMapping(prop, source, indent); + } return $"{indent}{prop.Name} = {source}?.ToList(),"; } @@ -897,6 +976,29 @@ namespace Svrnty.CQRS.Grpc.Generators 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) { @@ -959,6 +1061,32 @@ namespace Svrnty.CQRS.Grpc.Generators { 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) { @@ -988,9 +1116,34 @@ namespace Svrnty.CQRS.Grpc.Generators // 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) { @@ -1000,10 +1153,18 @@ namespace Svrnty.CQRS.Grpc.Generators } 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}>() }},"; + // 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} }},"; } @@ -1012,7 +1173,12 @@ namespace Svrnty.CQRS.Grpc.Generators if (prop.IsComplexType) { var typeName = prop.Type.Split('.').Last().Replace("?", ""); - if (prop.IsNullable || prop.Type.EndsWith("?")) + if (prop.NestedProperties != null && prop.NestedProperties.Any()) + { + // Use recursive mapping for nested properties + return GenerateResultComplexObjectMapping(prop, source, indent); + } + else if (prop.IsNullable || prop.Type.EndsWith("?")) { return $"{indent}{prop.Name} = {source} != null ? new {typeName}() : null,"; } @@ -1028,6 +1194,12 @@ namespace Svrnty.CQRS.Grpc.Generators 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},"; } @@ -1079,6 +1251,8 @@ namespace Svrnty.CQRS.Grpc.Generators 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 @@ -1090,6 +1264,13 @@ 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(); + ExtractNestedPropertiesWithTypeInfo(namedElementType, propInfo.ElementNestedProperties); + } } } // If it's a complex type (not list), extract nested properties @@ -1133,6 +1314,8 @@ namespace Svrnty.CQRS.Grpc.Generators 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 @@ -2528,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(" {"); @@ -2552,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\", "); diff --git a/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs b/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs index 36e1943..9a11ec4 100644 --- a/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs +++ b/Svrnty.CQRS.Grpc.Generators/Models/CommandInfo.cs @@ -46,6 +46,9 @@ namespace Svrnty.CQRS.Grpc.Generators.Models 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; } @@ -66,6 +69,9 @@ namespace Svrnty.CQRS.Grpc.Generators.Models IsDateTime = false; IsGuid = false; IsJsonElement = false; + IsBinaryType = false; + IsStream = false; + IsReadOnly = false; IsElementComplexType = false; IsElementGuid = false; } diff --git a/Svrnty.CQRS.Grpc.Generators/ProtoTypeMapper.cs b/Svrnty.CQRS.Grpc.Generators/ProtoTypeMapper.cs index 3e8ab62..cdce5cf 100644 --- a/Svrnty.CQRS.Grpc.Generators/ProtoTypeMapper.cs +++ b/Svrnty.CQRS.Grpc.Generators/ProtoTypeMapper.cs @@ -68,6 +68,20 @@ internal static class ProtoFileTypeMapper } } + // Handle byte[] array type (check before switch since it's an array) + if (typeSymbol is IArrayTypeSymbol arrayType && arrayType.ElementType.SpecialType == SpecialType.System_Byte) + { + return "bytes"; + } + + // Handle Stream types -> bytes + if (fullTypeName.Contains("System.IO.Stream") || + fullTypeName.Contains("System.IO.MemoryStream") || + fullTypeName.Contains("System.IO.FileStream")) + { + return "bytes"; + } + // Basic types switch (typeName) { @@ -97,6 +111,10 @@ internal static class ProtoFileTypeMapper return "double"; case "Byte[]": return "bytes"; + case "Stream": + case "MemoryStream": + case "FileStream": + return "bytes"; case "Guid": // Guid serialized as string return "string"; @@ -184,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") || @@ -198,6 +216,31 @@ internal static class ProtoFileTypeMapper return false; } + /// + /// Checks if a type is a Stream or byte array type (for special ByteString handling) + /// + 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"; + } + /// /// Gets the element type from a collection type, or returns the type itself if not a collection. /// Also unwraps Nullable types. diff --git a/Svrnty.Sample/Protos/cqrs_services.proto b/Svrnty.Sample/Protos/cqrs_services.proto index 10bad1c..f5eaed2 100644 --- a/Svrnty.Sample/Protos/cqrs_services.proto +++ b/Svrnty.Sample/Protos/cqrs_services.proto @@ -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