Compare commits

..

2 Commits

Author SHA1 Message Date
46e739eead fix: handle nullable numeric types in gRPC request mapping
When a C# property is nullable (int?, long?, etc.), protobuf3 defaults
to 0 when the field is not set. The generator now treats 0 as null
for nullable numeric properties, allowing proper optional field semantics.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-25 11:30:01 -05:00
179b06374d fix: improve gRPC source generator type mapping and property naming
- Add ProtoPropertyName to PropertyInfo for correct proto property naming
- Fix ToPascalCaseHelper to match Grpc.Tools naming (e.g., value_per100g → ValuePer100G)
- Add IsResultNullable and ResultTypeWithoutNullable to QueryInfo
- Fix IsPrimitiveType to correctly handle nullable complex types
- Add GetCollectionElementType helper (excludes strings from collection detection)
- Use AddRange pattern for repeated/collection fields in proto messages
- Add explicit Analyzer reference in props for reliable source generator loading
- Handle null cases in single complex type response mapping
- Fix collection properties in complex results with proper nested type mapping

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-25 10:47:05 -05:00
7 changed files with 757 additions and 1013 deletions

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,11 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
public class PropertyInfo
{
public string Name { get; set; }
/// <summary>
/// The property name as generated by Grpc.Tools from the proto field name.
/// This may differ from the C# property name due to casing differences.
/// </summary>
public string ProtoPropertyName { get; set; }
public string Type { get; set; }
public string FullyQualifiedType { get; set; }
public string ProtoType { get; set; }
@ -44,19 +49,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()
{
Name = string.Empty;
ProtoPropertyName = string.Empty;
Type = string.Empty;
FullyQualifiedType = string.Empty;
ProtoType = string.Empty;
@ -67,13 +67,7 @@ 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;
}
}
}

View File

@ -13,6 +13,26 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
public string HandlerInterfaceName { get; set; }
public List<PropertyInfo> ResultProperties { get; set; }
public bool IsResultPrimitiveType { get; set; }
/// <summary>
/// True if the result type is a collection (List, IEnumerable, etc.)
/// </summary>
public bool IsResultCollection { get; set; }
/// <summary>
/// The element type name if IsResultCollection is true
/// </summary>
public string ResultElementType { get; set; }
/// <summary>
/// The fully qualified element type name if IsResultCollection is true
/// </summary>
public string ResultElementTypeFullyQualified { get; set; }
/// <summary>
/// True if the result type is nullable (ends with ? or is Nullable<T>)
/// </summary>
public bool IsResultNullable { get; set; }
/// <summary>
/// The result type name without the nullable annotation (e.g., "CnfFoodDetailItem" instead of "CnfFoodDetailItem?")
/// </summary>
public string ResultTypeWithoutNullable { get; set; }
public QueryInfo()
{
@ -25,6 +45,11 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
HandlerInterfaceName = string.Empty;
ResultProperties = new List<PropertyInfo>();
IsResultPrimitiveType = false;
IsResultCollection = false;
ResultElementType = string.Empty;
ResultElementTypeFullyQualified = string.Empty;
IsResultNullable = false;
ResultTypeWithoutNullable = string.Empty;
}
}
}

View File

@ -320,9 +320,7 @@ internal class ProtoFileGenerator
var properties = type.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public &&
!p.IsIndexer &&
!ProtoFileTypeMapper.IsCollectionInternalProperty(p.Name))
.Where(p => p.DeclaredAccessibility == Accessibility.Public)
.ToList();
// Collect nested complex types to generate after closing this message
@ -405,9 +403,14 @@ internal class ProtoFileGenerator
_messagesBuilder.AppendLine();
// Generate complex type message after closing the response message
if (resultType != null && IsComplexType(resultType))
// Use GetElementOrUnderlyingType to extract element type from collections (e.g., List<CnfFoodItem> -> CnfFoodItem)
if (resultType != null)
{
GenerateComplexTypeMessage(resultType as INamedTypeSymbol);
var underlyingType = ProtoFileTypeMapper.GetElementOrUnderlyingType(resultType);
if (IsComplexType(underlyingType) && underlyingType is INamedTypeSymbol namedType)
{
GenerateComplexTypeMessage(namedType);
}
}
}
@ -425,73 +428,48 @@ 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>();
// Check if this type is a collection (implements IList<T>, ICollection<T>, etc.)
var collectionElementType = ProtoFileTypeMapper.GetCollectionElementTypeByInterface(type);
if (collectionElementType != null)
int fieldNumber = 1;
foreach (var prop in properties)
{
// This type is a collection - generate a single repeated field for items
var protoElementType = ProtoFileTypeMapper.MapType(collectionElementType, out var needsImport, out var importPath);
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);
if (needsImport && importPath != null)
{
_requiredImports.Add(importPath);
}
_messagesBuilder.AppendLine($" repeated {protoElementType} items = 1;");
var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name);
_messagesBuilder.AppendLine($" {protoType} {fieldName} = {fieldNumber};");
// Track the element type for nested generation
if (IsComplexType(collectionElementType) && collectionElementType is INamedTypeSymbol elementNamedType)
// Track enums for later generation
var enumType = ProtoFileTypeMapper.GetEnumType(prop.Type);
if (enumType != null)
{
nestedComplexTypes.Add(elementNamedType);
TrackEnumType(enumType);
}
}
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();
int fieldNumber = 1;
foreach (var prop in properties)
// 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)
{
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);
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++;
nestedComplexTypes.Add(namedType);
}
fieldNumber++;
}
_messagesBuilder.AppendLine("}");
@ -764,7 +742,7 @@ internal class ProtoFileGenerator
FullyQualifiedName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
.Replace("global::", ""),
Namespace = type.ContainingNamespace?.ToDisplayString() ?? "",
SubscriptionKeyProperty = subscriptionKeyProp!, // Already validated as non-null above
SubscriptionKeyProperty = subscriptionKeyProp,
SubscriptionKeyInfo = keyPropInfo,
Properties = properties
});
@ -782,9 +760,7 @@ internal class ProtoFileGenerator
int fieldNumber = 1;
foreach (var prop in type.GetMembers().OfType<IPropertySymbol>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public &&
!p.IsIndexer &&
!ProtoFileTypeMapper.IsCollectionInternalProperty(p.Name)))
.Where(p => p.DeclaredAccessibility == Accessibility.Public))
{
if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type))
continue;
@ -846,16 +822,14 @@ internal class ProtoFileGenerator
foreach (var prop in notification.Properties)
{
var typeSymbol = _compilation.GetTypeByMetadataName(prop.FullyQualifiedType) ??
GetTypeFromName(prop.FullyQualifiedType);
var protoType = ProtoFileTypeMapper.MapType(
_compilation.GetTypeByMetadataName(prop.FullyQualifiedType) ??
GetTypeFromName(prop.FullyQualifiedType),
out var needsImport, out var importPath);
if (typeSymbol != null)
if (needsImport && importPath != null)
{
ProtoFileTypeMapper.MapType(typeSymbol, out var needsImport, out var importPath);
if (needsImport && importPath != null)
{
_requiredImports.Add(importPath);
}
_requiredImports.Add(importPath);
}
var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name);

View File

@ -20,68 +20,6 @@ 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)
{
@ -111,35 +49,81 @@ 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";
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";
}
// 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}>";
}
}
// Enums
@ -202,8 +186,8 @@ internal static class ProtoFileTypeMapper
var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
// Skip these types - they should trigger a warning/error
// Note: Stream types are now supported (mapped to bytes)
if (fullTypeName.Contains("System.Threading.CancellationToken") ||
if (fullTypeName.Contains("System.IO.Stream") ||
fullTypeName.Contains("System.Threading.CancellationToken") ||
fullTypeName.Contains("System.Threading.Tasks.Task") ||
fullTypeName.Contains("System.Collections.Generic.IAsyncEnumerable") ||
fullTypeName.Contains("System.Func") ||
@ -216,31 +200,6 @@ 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.
@ -292,97 +251,4 @@ 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);
}
}

View File

@ -2,5 +2,15 @@
<PropertyGroup>
<!-- Marker to indicate Svrnty.CQRS.Grpc.Generators is referenced -->
<SvrntyCqrsGrpcGeneratorsVersion>$(SvrntyCqrsGrpcGeneratorsVersion)</SvrntyCqrsGrpcGeneratorsVersion>
<!-- Path resolution for both NuGet package and project reference -->
<_SvrntyCqrsGrpcGeneratorsPath Condition="Exists('$(MSBuildThisFileDirectory)..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll</_SvrntyCqrsGrpcGeneratorsPath>
<_SvrntyCqrsGrpcGeneratorsPath Condition="'$(_SvrntyCqrsGrpcGeneratorsPath)' == '' AND Exists('$(MSBuildThisFileDirectory)..\bin\Debug\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)..\bin\Debug\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll</_SvrntyCqrsGrpcGeneratorsPath>
<_SvrntyCqrsGrpcGeneratorsPath Condition="'$(_SvrntyCqrsGrpcGeneratorsPath)' == '' AND Exists('$(MSBuildThisFileDirectory)..\bin\Release\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)..\bin\Release\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll</_SvrntyCqrsGrpcGeneratorsPath>
</PropertyGroup>
<!-- Explicitly add the generator to the Analyzer ItemGroup -->
<ItemGroup Condition="'$(_SvrntyCqrsGrpcGeneratorsPath)' != ''">
<Analyzer Include="$(_SvrntyCqrsGrpcGeneratorsPath)" />
</ItemGroup>
</Project>

View File

@ -1,7 +1,111 @@
syntax = "proto3";
option csharp_namespace = "Generated.Grpc";
option csharp_namespace = "Svrnty.Sample.Grpc";
package cqrs;
// Placeholder proto file - will be regenerated on next build
// 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;
}