Compare commits

..

4 Commits

Author SHA1 Message Date
bd43bc9bde
Fix gRPC source generator for complex nested types
- 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 <noreply@anthropic.com>
2026-01-06 23:25:01 -05:00
661f5b4b1c
Fix GrpcGenerator type mapping for commands and nullable primitives
- Add proper complex type mapping for command results (same as queries already had)
- Handle nullable primitives (long?, int?, etc.) with default value fallback
- Fixes CS0029 and CS0266 compilation errors in generated gRPC service implementations

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 11:29:32 -05:00
99aebcf314
Fix proto generation for collection types (NpgsqlPolygon, etc.)
- Add IsCollectionTypeByInterface() to detect types implementing IList<T>, ICollection<T>, IEnumerable<T>
- Add GetCollectionElementTypeByInterface() to extract element type from collection interfaces
- Add IsCollectionInternalProperty() to filter out Count, Capacity, IsReadOnly, etc.
- Update GenerateComplexTypeMessage to generate `repeated T items` for collection types
- Filter out indexers (!p.IsIndexer) and collection-internal properties from all property extraction

This fixes the invalid proto syntax where C# indexers (this[]) were being generated
as proto fields, causing proto compilation errors.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 10:33:55 -05:00
Mathias Beaulieu-Duncan
f76dbb1a97 fix: add Guid to string conversion in gRPC source generator
The MapToProtoModel function was silently failing when mapping Guid
properties to proto string fields, causing IDs to be empty in gRPC
responses. Added explicit Guid → string conversion handling.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 19:06:18 -05:00
7 changed files with 1011 additions and 755 deletions

File diff suppressed because it is too large Load Diff

View File

@ -31,11 +31,6 @@ 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; }
@ -49,14 +44,19 @@ 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,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;
}
}
}

View File

@ -13,26 +13,6 @@ 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()
{
@ -45,11 +25,6 @@ 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,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
@ -403,14 +405,9 @@ internal class ProtoFileGenerator
_messagesBuilder.AppendLine();
// Generate complex type message after closing the response message
// Use GetElementOrUnderlyingType to extract element type from collections (e.g., List<CnfFoodItem> -> CnfFoodItem)
if (resultType != null)
if (resultType != null && IsComplexType(resultType))
{
var underlyingType = ProtoFileTypeMapper.GetElementOrUnderlyingType(resultType);
if (IsComplexType(underlyingType) && underlyingType is INamedTypeSymbol namedType)
{
GenerateComplexTypeMessage(namedType);
}
GenerateComplexTypeMessage(resultType as INamedTypeSymbol);
}
}
@ -428,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("}");
@ -742,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
});
@ -760,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;
@ -822,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);

View File

@ -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);
}
}

View File

@ -2,15 +2,5 @@
<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,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