dotnet-cqrs/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs
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

2802 lines
139 KiB
C#

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Svrnty.CQRS.Grpc.Generators.Helpers;
using Svrnty.CQRS.Grpc.Generators.Models;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Svrnty.CQRS.Grpc.Generators
{
[Generator]
public class GrpcGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Find all types that might be commands or queries from source
var typeDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => node is TypeDeclarationSyntax,
transform: static (ctx, _) => GetTypeSymbol(ctx))
.Where(static symbol => symbol is not null);
// Combine with compilation
var compilationAndTypes = context.CompilationProvider.Combine(typeDeclarations.Collect());
// Register source output
context.RegisterSourceOutput(compilationAndTypes, static (spc, source) => Execute(source.Left, source.Right!, spc));
}
private static INamedTypeSymbol? GetTypeSymbol(GeneratorSyntaxContext context)
{
var typeDeclaration = (TypeDeclarationSyntax)context.Node;
var symbol = context.SemanticModel.GetDeclaredSymbol(typeDeclaration);
return symbol as INamedTypeSymbol;
}
/// <summary>
/// Collects all types from the compilation and all referenced assemblies
/// </summary>
private static IEnumerable<INamedTypeSymbol> GetAllTypesFromCompilation(Compilation compilation)
{
var types = new List<INamedTypeSymbol>();
// Get types from the current assembly
CollectTypesFromNamespace(compilation.Assembly.GlobalNamespace, types);
// Get types from all referenced assemblies
foreach (var reference in compilation.References)
{
var assemblySymbol = compilation.GetAssemblyOrModuleSymbol(reference) as IAssemblySymbol;
if (assemblySymbol != null)
{
CollectTypesFromNamespace(assemblySymbol.GlobalNamespace, types);
}
}
return types;
}
private static void CollectTypesFromNamespace(INamespaceSymbol ns, List<INamedTypeSymbol> types)
{
foreach (var type in ns.GetTypeMembers())
{
types.Add(type);
// Also collect nested types
CollectNestedTypes(type, types);
}
foreach (var nestedNs in ns.GetNamespaceMembers())
{
CollectTypesFromNamespace(nestedNs, types);
}
}
private static void CollectNestedTypes(INamedTypeSymbol type, List<INamedTypeSymbol> types)
{
foreach (var nestedType in type.GetTypeMembers())
{
types.Add(nestedType);
CollectNestedTypes(nestedType, types);
}
}
private static void Execute(Compilation compilation, IEnumerable<INamedTypeSymbol?> sourceTypes, SourceProductionContext context)
{
// Get the expected namespace for proto-generated types
var rootNamespace = compilation.AssemblyName ?? "Generated";
var grpcNamespace = $"{rootNamespace}.Grpc";
// Check if proto types are available (from Grpc.Tools compilation of .proto file)
// If not, skip generation - this happens on first build before proto file is compiled
var commandServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.CommandService+CommandServiceBase");
var queryServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.QueryService+QueryServiceBase");
var dynamicQueryServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.DynamicQueryService+DynamicQueryServiceBase");
var notificationServiceBase = compilation.GetTypeByMetadataName($"{grpcNamespace}.NotificationService+NotificationServiceBase");
// If none of the service bases exist, the proto hasn't been compiled yet - skip generation
if (commandServiceBase == null && queryServiceBase == null && dynamicQueryServiceBase == null && notificationServiceBase == null)
{
// Report diagnostic for first build
var descriptor = new DiagnosticDescriptor(
"CQRSGRPC003",
"Proto types not yet available",
"gRPC service implementations will be generated on second build after proto file is compiled",
"Svrnty.CQRS.Grpc",
DiagnosticSeverity.Info,
isEnabledByDefault: true);
context.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None));
return;
}
var grpcIgnoreAttribute = compilation.GetTypeByMetadataName("Svrnty.CQRS.Grpc.Abstractions.Attributes.GrpcIgnoreAttribute");
var commandHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`1");
var commandHandlerWithResultInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.ICommandHandler`2");
var queryHandlerInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.Abstractions.IQueryHandler`2");
var dynamicQueryInterface2 = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IDynamicQuery`2");
var dynamicQueryInterface3 = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IDynamicQuery`3");
var queryableProviderInterface = compilation.GetTypeByMetadataName("Svrnty.CQRS.DynamicQuery.Abstractions.IQueryableProvider`1");
if (commandHandlerInterface == null || queryHandlerInterface == null)
{
return; // Handler interfaces not found
}
var commandMap = new Dictionary<INamedTypeSymbol, INamedTypeSymbol?>(SymbolEqualityComparer.Default); // Command -> Result type (null if no result)
var queryMap = new Dictionary<INamedTypeSymbol, INamedTypeSymbol>(SymbolEqualityComparer.Default); // Query -> Result type
var dynamicQueryMap = new List<(INamedTypeSymbol SourceType, INamedTypeSymbol DestinationType, INamedTypeSymbol? ParamsType)>(); // List of (Source, Destination, Params?)
// Get all types from the compilation and referenced assemblies
var allTypes = GetAllTypesFromCompilation(compilation);
// Find all command and query types by looking at handler implementations
foreach (var typeSymbol in allTypes)
{
if (typeSymbol.IsAbstract || typeSymbol.IsStatic)
continue;
// Check if this type implements ICommandHandler<T> or ICommandHandler<T, TResult>
foreach (var iface in typeSymbol.AllInterfaces)
{
if (iface.IsGenericType)
{
// Check for ICommandHandler<TCommand>
if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerInterface) && iface.TypeArguments.Length == 1)
{
var commandType = iface.TypeArguments[0] as INamedTypeSymbol;
if (commandType != null && !commandMap.ContainsKey(commandType))
commandMap[commandType] = null; // No result type
}
// Check for ICommandHandler<TCommand, TResult>
else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, commandHandlerWithResultInterface) && iface.TypeArguments.Length == 2)
{
var commandType = iface.TypeArguments[0] as INamedTypeSymbol;
var resultType = iface.TypeArguments[1] as INamedTypeSymbol;
if (commandType != null && resultType != null)
commandMap[commandType] = resultType;
}
// Check for IQueryHandler<TQuery, TResult>
else if (SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryHandlerInterface) && iface.TypeArguments.Length == 2)
{
var queryType = iface.TypeArguments[0] as INamedTypeSymbol;
var resultType = iface.TypeArguments[1] as INamedTypeSymbol;
if (queryType != null && resultType != null)
{
// Check if this is a dynamic query handler
if (queryType.IsGenericType &&
(SymbolEqualityComparer.Default.Equals(queryType.OriginalDefinition, dynamicQueryInterface2) ||
SymbolEqualityComparer.Default.Equals(queryType.OriginalDefinition, dynamicQueryInterface3)))
{
// Extract source, destination, and optional params types
var sourceType = queryType.TypeArguments[0] as INamedTypeSymbol;
var destinationType = queryType.TypeArguments[1] as INamedTypeSymbol;
INamedTypeSymbol? paramsType = null;
if (queryType.TypeArguments.Length == 3)
{
paramsType = queryType.TypeArguments[2] as INamedTypeSymbol;
}
if (sourceType != null && destinationType != null)
{
// Check if already added (avoid duplicates)
var exists = dynamicQueryMap.Any(dq =>
SymbolEqualityComparer.Default.Equals(dq.SourceType, sourceType) &&
SymbolEqualityComparer.Default.Equals(dq.DestinationType, destinationType) &&
(dq.ParamsType == null && paramsType == null ||
dq.ParamsType != null && paramsType != null && SymbolEqualityComparer.Default.Equals(dq.ParamsType, paramsType)));
if (!exists)
{
dynamicQueryMap.Add((sourceType, destinationType, paramsType));
}
}
}
else
{
queryMap[queryType] = resultType;
}
}
}
}
}
// Check if this type implements IQueryableProvider<T> - this indicates a dynamic query
if (queryableProviderInterface != null)
{
foreach (var iface in typeSymbol.AllInterfaces)
{
if (iface.IsGenericType && SymbolEqualityComparer.Default.Equals(iface.OriginalDefinition, queryableProviderInterface))
{
// Extract source type from IQueryableProvider<TSource>
var sourceType = iface.TypeArguments[0] as INamedTypeSymbol;
if (sourceType != null)
{
// For IQueryableProvider, we assume TSource = TDestination (no params)
var exists = dynamicQueryMap.Any(dq =>
SymbolEqualityComparer.Default.Equals(dq.SourceType, sourceType) &&
SymbolEqualityComparer.Default.Equals(dq.DestinationType, sourceType) &&
dq.ParamsType == null);
if (!exists)
{
dynamicQueryMap.Add((sourceType, sourceType, null));
}
}
}
}
}
}
var commands = new List<CommandInfo>();
var queries = new List<QueryInfo>();
// Process discovered command types
foreach (var kvp in commandMap)
{
var commandType = kvp.Key;
var resultType = kvp.Value;
// Skip if marked with [GrpcIgnore]
if (HasGrpcIgnoreAttribute(commandType))
continue;
var commandInfo = ExtractCommandInfo(commandType, resultType);
if (commandInfo != null)
commands.Add(commandInfo);
}
// Process discovered query types
foreach (var kvp in queryMap)
{
var queryType = kvp.Key;
var resultType = kvp.Value;
// Skip if marked with [GrpcIgnore]
if (HasGrpcIgnoreAttribute(queryType))
continue;
var queryInfo = ExtractQueryInfo(queryType, resultType);
if (queryInfo != null)
queries.Add(queryInfo);
}
// Process discovered dynamic query types
var dynamicQueries = new List<DynamicQueryInfo>();
foreach (var (sourceType, destinationType, paramsType) in dynamicQueryMap)
{
var dynamicQueryInfo = ExtractDynamicQueryInfo(sourceType, destinationType, paramsType);
if (dynamicQueryInfo != null)
dynamicQueries.Add(dynamicQueryInfo);
}
// Process discovered notification types (marked with [StreamingNotification])
var notifications = DiscoverNotifications(allTypes, compilation);
// Generate services if we found any commands, queries, dynamic queries, or notifications
if (commands.Any() || queries.Any() || dynamicQueries.Any() || notifications.Any())
{
GenerateProtoAndServices(context, commands, queries, dynamicQueries, notifications, compilation);
}
}
private static bool HasAttribute(INamedTypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol)
{
return typeSymbol.GetAttributes().Any(attr =>
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeSymbol));
}
private static bool HasGrpcIgnoreAttribute(INamedTypeSymbol typeSymbol)
{
return typeSymbol.GetAttributes().Any(attr =>
attr.AttributeClass?.Name == "GrpcIgnoreAttribute");
}
private static bool ImplementsInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? interfaceSymbol)
{
if (interfaceSymbol == null)
return false;
return typeSymbol.AllInterfaces.Any(i =>
SymbolEqualityComparer.Default.Equals(i, interfaceSymbol));
}
private static bool ImplementsGenericInterface(INamedTypeSymbol typeSymbol, INamedTypeSymbol? genericInterfaceSymbol)
{
if (genericInterfaceSymbol == null)
return false;
return typeSymbol.AllInterfaces.Any(i =>
i.IsGenericType && SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, genericInterfaceSymbol));
}
private static CommandInfo? ExtractCommandInfo(INamedTypeSymbol commandType, INamedTypeSymbol? resultType)
{
var commandInfo = new CommandInfo
{
Name = commandType.Name,
FullyQualifiedName = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
Namespace = commandType.ContainingNamespace.ToDisplayString(),
Properties = new List<PropertyInfo>()
};
// Set result type if provided
if (resultType != null)
{
commandInfo.ResultType = resultType.Name;
commandInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
// Use fully qualified names to avoid ambiguity with proto-generated types
var commandTypeFullyQualified = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var resultTypeFullyQualified = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandTypeFullyQualified}, {resultTypeFullyQualified}>";
}
else
{
var commandTypeFullyQualified = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandTypeFullyQualified}>";
}
// Extract properties
var properties = commandType.GetMembers().OfType<IPropertySymbol>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
.ToList();
int fieldNumber = 1;
foreach (var property in properties)
{
var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional);
var propInfo = new PropertyInfo
{
Name = property.Name,
Type = propertyType,
FullyQualifiedType = propertyType,
ProtoType = protoType,
FieldNumber = fieldNumber++,
IsComplexType = IsUserDefinedComplexType(property.Type),
// New type metadata fields
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),
};
// 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 element is complex, extract nested properties
if (propInfo.IsElementComplexType)
{
var unwrappedElement = UnwrapNullableType(elementType);
if (unwrappedElement 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);
}
}
commandInfo.Properties.Add(propInfo);
}
return commandInfo;
}
private static bool IsUserDefinedComplexType(ITypeSymbol type)
{
if (type == null)
return false;
// Unwrap nullable first
var unwrapped = UnwrapNullableType(type);
if (unwrapped.TypeKind != TypeKind.Class && unwrapped.TypeKind != TypeKind.Struct)
return false;
var fullName = unwrapped.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
// Exclude system types and primitives
if (fullName.StartsWith("global::System."))
return false;
if (IsPrimitiveType(fullName))
return false;
return true;
}
private static ITypeSymbol UnwrapNullableType(ITypeSymbol type)
{
// Handle Nullable<T> (value type nullability)
if (type is INamedTypeSymbol namedType &&
namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T &&
namedType.TypeArguments.Length == 1)
{
return namedType.TypeArguments[0];
}
return type;
}
private static bool IsNullableType(ITypeSymbol type)
{
// Check for Nullable<T> (value type nullability)
if (type is INamedTypeSymbol namedType &&
namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T)
{
return true;
}
// Check for reference type nullability (C# 8.0+)
if (type.NullableAnnotation == NullableAnnotation.Annotated)
{
return true;
}
return false;
}
private static bool IsDecimalType(ITypeSymbol type)
{
var unwrapped = UnwrapNullableType(type);
return unwrapped.SpecialType == SpecialType.System_Decimal;
}
private static bool IsDateTimeType(ITypeSymbol type)
{
var unwrapped = UnwrapNullableType(type);
return unwrapped.SpecialType == SpecialType.System_DateTime;
}
private static bool IsEnumType(ITypeSymbol type)
{
var unwrapped = UnwrapNullableType(type);
return unwrapped.TypeKind == TypeKind.Enum;
}
private static bool IsGuidType(ITypeSymbol type)
{
var unwrapped = UnwrapNullableType(type);
return unwrapped.ToDisplayString() == "System.Guid";
}
private static bool IsJsonElementType(ITypeSymbol type)
{
var unwrapped = UnwrapNullableType(type);
return unwrapped.ToDisplayString() == "System.Text.Json.JsonElement";
}
private static bool IsListOrCollection(ITypeSymbol type)
{
if (type is IArrayTypeSymbol)
return true;
if (type is INamedTypeSymbol namedType && namedType.IsGenericType)
{
var typeName = namedType.OriginalDefinition.ToDisplayString();
return typeName.StartsWith("System.Collections.Generic.List<") ||
typeName.StartsWith("System.Collections.Generic.IList<") ||
typeName.StartsWith("System.Collections.Generic.ICollection<") ||
typeName.StartsWith("System.Collections.Generic.IEnumerable<");
}
return false;
}
private static ITypeSymbol? GetListElementType(ITypeSymbol type)
{
if (type is IArrayTypeSymbol arrayType)
return arrayType.ElementType;
if (type is INamedTypeSymbol namedType && namedType.IsGenericType && namedType.TypeArguments.Length > 0)
{
return namedType.TypeArguments[0];
}
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>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
.ToList();
foreach (var property in properties)
{
var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var propInfo = new PropertyInfo
{
Name = property.Name,
Type = propertyType,
FullyQualifiedType = propertyType,
ProtoType = string.Empty,
FieldNumber = 0,
IsComplexType = IsUserDefinedComplexType(property.Type),
};
// Recursively extract nested properties for complex types
if (propInfo.IsComplexType && property.Type is INamedTypeSymbol namedType)
{
ExtractNestedProperties(namedType, propInfo.NestedProperties);
}
nestedProperties.Add(propInfo);
}
}
private static void ExtractNestedPropertiesWithTypeInfo(INamedTypeSymbol type, List<PropertyInfo> nestedProperties)
{
var properties = type.GetMembers().OfType<IPropertySymbol>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
.ToList();
foreach (var property in properties)
{
var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var propInfo = new PropertyInfo
{
Name = property.Name,
Type = propertyType,
FullyQualifiedType = propertyType,
ProtoType = string.Empty,
FieldNumber = 0,
IsComplexType = IsUserDefinedComplexType(property.Type),
// 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),
};
// 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);
}
}
// Recursively extract nested properties for complex types
else if (propInfo.IsComplexType)
{
var unwrapped = UnwrapNullableType(property.Type);
if (unwrapped is INamedTypeSymbol namedType)
{
ExtractNestedPropertiesWithTypeInfo(namedType, propInfo.NestedProperties);
}
}
nestedProperties.Add(propInfo);
}
}
private static void GenerateNestedPropertyMapping(StringBuilder sb, List<PropertyInfo> properties, string sourcePrefix, string indent)
{
foreach (var prop in properties)
{
var sourcePropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1);
if (prop.IsComplexType)
{
// Generate nested object mapping
sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{sourcePropName} != null ? new {prop.FullyQualifiedType}");
sb.AppendLine($"{indent}{{");
GenerateNestedPropertyMapping(sb, prop.NestedProperties, $"{sourcePrefix}.{sourcePropName}", indent + " ");
sb.AppendLine($"{indent}}} : null!,");
}
else
{
sb.AppendLine($"{indent}{prop.Name} = {sourcePrefix}.{sourcePropName},");
}
}
}
private static string GeneratePropertyAssignment(PropertyInfo prop, string requestVar, string indent)
{
var requestPropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1);
var source = $"{requestVar}.{requestPropName}";
// Handle lists
if (prop.IsList)
{
if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any())
{
// 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()
return $"{indent}{prop.Name} = {source}?.ToList(),";
}
}
// Handle enums (proto int32 -> C# enum)
if (prop.IsEnum)
{
return $"{indent}{prop.Name} = ({prop.FullyQualifiedType}){source},";
}
// Handle decimals (proto string -> C# decimal)
if (prop.IsDecimal)
{
if (prop.IsNullable)
{
return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : decimal.Parse({source}),";
}
else
{
return $"{indent}{prop.Name} = decimal.Parse({source}),";
}
}
// 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 (proto string -> C# Guid)
if (prop.IsGuid)
{
if (prop.IsNullable)
{
return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : System.Guid.Parse({source}),";
}
else
{
return $"{indent}{prop.Name} = System.Guid.Parse({source}),";
}
}
// Handle complex types (single objects)
if (prop.IsComplexType)
{
return GenerateComplexObjectMapping(prop, source, indent);
}
// Default: direct assignment
return $"{indent}{prop.Name} = {source},";
}
private static string GenerateComplexListMapping(PropertyInfo prop, string source, string indent)
{
var sb = new StringBuilder();
sb.AppendLine($"{indent}{prop.Name} = {source}?.Select(x => new {prop.ElementType}");
sb.AppendLine($"{indent}{{");
foreach (var nestedProp in prop.ElementNestedProperties!)
{
var nestedSourcePropName = char.ToUpper(nestedProp.Name[0]) + nestedProp.Name.Substring(1);
var nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, "x", indent + " ");
sb.AppendLine(nestedAssignment);
}
sb.Append($"{indent}}}).ToList(),");
return sb.ToString();
}
private static string GenerateComplexObjectMapping(PropertyInfo prop, string source, string indent)
{
var sb = new StringBuilder();
sb.AppendLine($"{indent}{prop.Name} = {source} != null ? new {prop.FullyQualifiedType}");
sb.AppendLine($"{indent}{{");
foreach (var nestedProp in prop.NestedProperties)
{
var nestedAssignment = GenerateNestedPropertyAssignment(nestedProp, source, indent + " ");
sb.AppendLine(nestedAssignment);
}
sb.Append($"{indent}}} : null!,");
return sb.ToString();
}
private static string GenerateNestedPropertyAssignment(PropertyInfo prop, string sourceVar, string indent)
{
var sourcePropName = char.ToUpper(prop.Name[0]) + prop.Name.Substring(1);
var source = $"{sourceVar}.{sourcePropName}";
// Handle enums
if (prop.IsEnum)
{
return $"{indent}{prop.Name} = ({prop.FullyQualifiedType}){source},";
}
// Handle decimals
if (prop.IsDecimal)
{
if (prop.IsNullable)
{
return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : decimal.Parse({source}),";
}
else
{
return $"{indent}{prop.Name} = decimal.Parse({source}),";
}
}
// Handle Guid
if (prop.IsGuid)
{
if (prop.IsNullable)
{
return $"{indent}{prop.Name} = string.IsNullOrEmpty({source}) ? null : System.Guid.Parse({source}),";
}
else
{
return $"{indent}{prop.Name} = System.Guid.Parse({source}),";
}
}
// Handle lists
if (prop.IsList)
{
return $"{indent}{prop.Name} = {source}?.ToList(),";
}
// Handle complex types
if (prop.IsComplexType && prop.NestedProperties.Any())
{
return GenerateComplexObjectMapping(prop, source, indent);
}
// Default: direct assignment
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 complex types (single objects)
if (prop.IsComplexType)
{
return GenerateResultComplexObjectMapping(prop, source, indent);
}
// Default: direct assignment (strings, ints, bools, etc.)
if (prop.IsNullable && prop.Type.Contains("string"))
{
return $"{indent}{prop.Name} = {source} ?? string.Empty,";
}
return $"{indent}{prop.Name} = {source},";
}
private static string GenerateResultComplexListMapping(PropertyInfo prop, string source, string indent)
{
var sb = new StringBuilder();
var protoElementType = prop.ElementType?.Split('.').Last() ?? prop.Type;
sb.AppendLine($"{indent}{prop.Name} = {{");
sb.AppendLine($"{indent} {source}?.Select(x => new {protoElementType}");
sb.AppendLine($"{indent} {{");
if (prop.ElementNestedProperties != null)
{
foreach (var nestedProp in prop.ElementNestedProperties)
{
var nestedAssignment = GenerateResultNestedPropertyMapping(nestedProp, "x", indent + " ");
sb.AppendLine(nestedAssignment);
}
}
sb.AppendLine($"{indent} }}) ?? Enumerable.Empty<{protoElementType}>()");
sb.Append($"{indent}}},");
return sb.ToString();
}
private static string GenerateResultComplexObjectMapping(PropertyInfo prop, string source, string indent)
{
var sb = new StringBuilder();
var protoType = prop.Type.Split('.').Last().Replace("?", "");
sb.AppendLine($"{indent}{prop.Name} = {source} != null ? new {protoType}");
sb.AppendLine($"{indent}{{");
foreach (var nestedProp in prop.NestedProperties)
{
var nestedAssignment = GenerateResultNestedPropertyMapping(nestedProp, source, indent + " ");
sb.AppendLine(nestedAssignment);
}
sb.Append($"{indent}}} : null,");
return sb.ToString();
}
private static string GenerateResultNestedPropertyMapping(PropertyInfo prop, string sourceVar, string indent)
{
var source = $"{sourceVar}.{prop.Name}";
// Handle Guid
if (prop.IsGuid)
{
if (prop.IsNullable)
{
return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,";
}
else
{
return $"{indent}{prop.Name} = {source}.ToString(),";
}
}
// Handle decimals
if (prop.IsDecimal)
{
if (prop.IsNullable)
{
return $"{indent}{prop.Name} = {source}?.ToString() ?? string.Empty,";
}
else
{
return $"{indent}{prop.Name} = {source}.ToString(),";
}
}
// Handle enums
if (prop.IsEnum)
{
return $"{indent}{prop.Name} = (int){source},";
}
// Handle lists
if (prop.IsList)
{
if (prop.IsElementGuid)
{
return $"{indent}{prop.Name} = {{ {source}?.Select(x => x.ToString()) ?? Enumerable.Empty<string>() }},";
}
else if (prop.IsElementComplexType)
{
// Complex list elements need mapping - but we don't have nested property info here
// Fall back to creating empty proto objects (the user needs to ensure types are compatible)
var elementTypeName = prop.ElementType?.Split('.').Last() ?? "object";
return $"{indent}{prop.Name} = {{ {source}?.Select(x => new {elementTypeName}()) ?? Enumerable.Empty<{elementTypeName}>() }},";
}
return $"{indent}{prop.Name} = {{ {source} }},";
}
// Handle complex types (non-list)
if (prop.IsComplexType)
{
var typeName = prop.Type.Split('.').Last().Replace("?", "");
if (prop.IsNullable || prop.Type.EndsWith("?"))
{
return $"{indent}{prop.Name} = {source} != null ? new {typeName}() : null,";
}
else
{
return $"{indent}{prop.Name} = new {typeName}(),";
}
}
// Handle nullable strings
if (prop.IsNullable && prop.Type.Contains("string"))
{
return $"{indent}{prop.Name} = {source} ?? string.Empty,";
}
// Default: direct assignment
return $"{indent}{prop.Name} = {source},";
}
private static QueryInfo? ExtractQueryInfo(INamedTypeSymbol queryType, INamedTypeSymbol resultType)
{
var queryInfo = new QueryInfo
{
Name = queryType.Name,
FullyQualifiedName = queryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
Namespace = queryType.ContainingNamespace.ToDisplayString(),
Properties = new List<PropertyInfo>()
};
// Set result type
queryInfo.ResultType = resultType.Name;
queryInfo.ResultFullyQualifiedName = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
// Use fully qualified names to avoid ambiguity with proto-generated types
var queryTypeFullyQualified = queryType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var resultTypeFullyQualified = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
queryInfo.HandlerInterfaceName = $"IQueryHandler<{queryTypeFullyQualified}, {resultTypeFullyQualified}>";
// Check if result type is primitive
var resultTypeString = resultType.ToDisplayString();
queryInfo.IsResultPrimitiveType = IsPrimitiveType(resultTypeString);
// Extract result type properties if it's a complex type
if (!queryInfo.IsResultPrimitiveType)
{
var resultProperties = resultType.GetMembers().OfType<IPropertySymbol>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
.ToList();
foreach (var property in resultProperties)
{
var propInfo = new PropertyInfo
{
Name = property.Name,
Type = property.Type.ToDisplayString(),
FullyQualifiedType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
ProtoType = string.Empty,
FieldNumber = 0,
IsNullable = property.Type.NullableAnnotation == NullableAnnotation.Annotated ||
(property.Type is INamedTypeSymbol nt && nt.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T),
IsEnum = IsEnumType(property.Type),
IsDecimal = IsDecimalType(property.Type),
IsDateTime = IsDateTimeType(property.Type),
IsGuid = IsGuidType(property.Type),
IsJsonElement = IsJsonElementType(property.Type),
IsList = IsListOrCollection(property.Type),
IsComplexType = IsUserDefinedComplexType(property.Type),
};
// If it's a list, extract element type info
if (propInfo.IsList)
{
var elementType = GetListElementType(property.Type);
if (elementType != null)
{
propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType);
propInfo.IsElementGuid = IsGuidType(elementType);
}
}
// If it's a complex type (not list), extract nested properties
else if (propInfo.IsComplexType)
{
var unwrapped = UnwrapNullableType(property.Type);
if (unwrapped is INamedTypeSymbol namedType)
{
ExtractNestedPropertiesWithTypeInfo(namedType, propInfo.NestedProperties);
}
}
queryInfo.ResultProperties.Add(propInfo);
}
}
// Extract properties
var properties = queryType.GetMembers().OfType<IPropertySymbol>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
.ToList();
int fieldNumber = 1;
foreach (var property in properties)
{
var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var protoType = ProtoTypeMapper.MapToProtoType(propertyType, out bool isRepeated, out bool isOptional);
var propInfo = new PropertyInfo
{
Name = property.Name,
Type = propertyType,
FullyQualifiedType = propertyType,
ProtoType = protoType,
FieldNumber = fieldNumber++,
IsNullable = property.Type.NullableAnnotation == NullableAnnotation.Annotated ||
(property.Type is INamedTypeSymbol nt && nt.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T),
IsEnum = IsEnumType(property.Type),
IsDecimal = IsDecimalType(property.Type),
IsDateTime = IsDateTimeType(property.Type),
IsGuid = IsGuidType(property.Type),
IsJsonElement = IsJsonElementType(property.Type),
IsList = IsListOrCollection(property.Type),
IsComplexType = IsUserDefinedComplexType(property.Type),
};
// If it's a list, extract element type info
if (propInfo.IsList)
{
var elementType = GetListElementType(property.Type);
if (elementType != null)
{
propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType);
propInfo.IsElementGuid = IsGuidType(elementType);
}
}
// If it's a complex type (not list), extract nested properties
else if (propInfo.IsComplexType)
{
var unwrapped = UnwrapNullableType(property.Type);
if (unwrapped is INamedTypeSymbol namedType)
{
ExtractNestedPropertiesWithTypeInfo(namedType, propInfo.NestedProperties);
}
}
queryInfo.Properties.Add(propInfo);
}
return queryInfo;
}
private static DynamicQueryInfo? ExtractDynamicQueryInfo(INamedTypeSymbol sourceType, INamedTypeSymbol destinationType, INamedTypeSymbol? paramsType)
{
var dynamicQueryInfo = new DynamicQueryInfo
{
SourceType = sourceType.Name,
SourceTypeFullyQualified = sourceType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
DestinationType = destinationType.Name,
DestinationTypeFullyQualified = destinationType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
HasParams = paramsType != null
};
if (paramsType != null)
{
dynamicQueryInfo.ParamsType = paramsType.Name;
dynamicQueryInfo.ParamsTypeFullyQualified = paramsType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
}
// Pluralize destination type for naming (e.g., User -> Users)
dynamicQueryInfo.Name = Pluralize(destinationType.Name);
// Build interface names
if (paramsType != null)
{
dynamicQueryInfo.QueryInterfaceName = $"IDynamicQuery<{dynamicQueryInfo.SourceTypeFullyQualified}, {dynamicQueryInfo.DestinationTypeFullyQualified}, {dynamicQueryInfo.ParamsTypeFullyQualified}>";
dynamicQueryInfo.HandlerInterfaceName = $"IQueryHandler<{dynamicQueryInfo.QueryInterfaceName}, IQueryExecutionResult<{dynamicQueryInfo.DestinationTypeFullyQualified}>>";
}
else
{
dynamicQueryInfo.QueryInterfaceName = $"IDynamicQuery<{dynamicQueryInfo.SourceTypeFullyQualified}, {dynamicQueryInfo.DestinationTypeFullyQualified}>";
dynamicQueryInfo.HandlerInterfaceName = $"IQueryHandler<{dynamicQueryInfo.QueryInterfaceName}, IQueryExecutionResult<{dynamicQueryInfo.DestinationTypeFullyQualified}>>";
}
return dynamicQueryInfo;
}
private static string Pluralize(string word)
{
// Simple pluralization logic - can be enhanced with Pluralize.NET if needed
if (word.EndsWith("y") && word.Length > 1 && !"aeiou".Contains(word[word.Length - 2]))
return word.Substring(0, word.Length - 1) + "ies";
if (word.EndsWith("s") || word.EndsWith("x") || word.EndsWith("z") || word.EndsWith("ch") || word.EndsWith("sh"))
return word + "es";
return word + "s";
}
private static void GenerateProtoAndServices(SourceProductionContext context, List<CommandInfo> commands, List<QueryInfo> queries, List<DynamicQueryInfo> dynamicQueries, List<NotificationInfo> notifications, Compilation compilation)
{
// Get root namespace from compilation
var rootNamespace = compilation.AssemblyName ?? "Application";
// Generate service implementations for commands
if (commands.Any())
{
var commandService = GenerateCommandServiceImpl(commands, rootNamespace);
context.AddSource("CommandServiceImpl.g.cs", commandService);
}
// Generate service implementations for queries
if (queries.Any())
{
var queryService = GenerateQueryServiceImpl(queries, rootNamespace);
context.AddSource("QueryServiceImpl.g.cs", queryService);
}
// Generate service implementations for dynamic queries
if (dynamicQueries.Any())
{
var dynamicQueryService = GenerateDynamicQueryServiceImpl(dynamicQueries, rootNamespace);
context.AddSource("DynamicQueryServiceImpl.g.cs", dynamicQueryService);
}
// Generate service implementations for notifications (streaming)
if (notifications.Any())
{
var notificationService = GenerateNotificationServiceImpl(notifications, rootNamespace);
context.AddSource("NotificationServiceImpl.g.cs", notificationService);
}
// Generate registration extensions
var registrationExtensions = GenerateRegistrationExtensions(commands.Any(), queries.Any(), dynamicQueries.Any(), notifications.Any(), rootNamespace);
context.AddSource("GrpcServiceRegistration.g.cs", registrationExtensions);
}
private static string GenerateCommandMessages(List<CommandInfo> commands, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using System.Runtime.Serialization;");
sb.AppendLine("using ProtoBuf;");
sb.AppendLine();
sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages");
sb.AppendLine("{");
foreach (var command in commands)
{
// Generate command DTO
sb.AppendLine(" [ProtoContract]");
sb.AppendLine(" [DataContract]");
sb.AppendLine($" public sealed class {command.Name}Dto");
sb.AppendLine(" {");
foreach (var prop in command.Properties)
{
sb.AppendLine($" [ProtoMember({prop.FieldNumber})]");
sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]");
sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine();
// Generate result DTO if command has a result
if (command.HasResult)
{
sb.AppendLine(" [ProtoContract]");
sb.AppendLine(" [DataContract]");
sb.AppendLine($" public sealed class {command.Name}ResultDto");
sb.AppendLine(" {");
sb.AppendLine(" [ProtoMember(1)]");
sb.AppendLine(" [DataMember(Order = 1)]");
sb.AppendLine($" public {command.ResultFullyQualifiedName} Result {{ get; set; }}");
sb.AppendLine(" }");
sb.AppendLine();
}
}
sb.AppendLine("}");
return sb.ToString();
}
private static string GenerateQueryMessages(List<QueryInfo> queries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using System.Runtime.Serialization;");
sb.AppendLine("using ProtoBuf;");
sb.AppendLine();
sb.AppendLine($"namespace {rootNamespace}.Grpc.Messages");
sb.AppendLine("{");
foreach (var query in queries)
{
// Generate query DTO
sb.AppendLine(" [ProtoContract]");
sb.AppendLine(" [DataContract]");
sb.AppendLine($" public sealed class {query.Name}Dto");
sb.AppendLine(" {");
foreach (var prop in query.Properties)
{
sb.AppendLine($" [ProtoMember({prop.FieldNumber})]");
sb.AppendLine(" [DataMember(Order = " + prop.FieldNumber + ")]");
sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine();
// Generate result DTO
sb.AppendLine(" [ProtoContract]");
sb.AppendLine(" [DataContract]");
sb.AppendLine($" public sealed class {query.Name}ResultDto");
sb.AppendLine(" {");
sb.AppendLine(" [ProtoMember(1)]");
sb.AppendLine(" [DataMember(Order = 1)]");
sb.AppendLine($" public {query.ResultFullyQualifiedName} Result {{ get; set; }}");
sb.AppendLine(" }");
sb.AppendLine();
}
sb.AppendLine("}");
return sb.ToString();
}
private static string GenerateCommandService(List<CommandInfo> commands, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using System.ServiceModel;");
sb.AppendLine("using System.Threading;");
sb.AppendLine("using System.Threading.Tasks;");
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
sb.AppendLine($"using {rootNamespace}.Grpc.Messages;");
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
sb.AppendLine("using ProtoBuf.Grpc;");
sb.AppendLine();
// Generate service interface
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
sb.AppendLine("{");
sb.AppendLine(" [ServiceContract]");
sb.AppendLine(" public interface ICommandService");
sb.AppendLine(" {");
foreach (var command in commands)
{
if (command.HasResult)
{
sb.AppendLine($" [OperationContract]");
sb.AppendLine($" Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);");
}
else
{
sb.AppendLine($" [OperationContract]");
sb.AppendLine($" Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default);");
}
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine();
// Generate service implementation
sb.AppendLine(" public sealed class CommandService : ICommandService");
sb.AppendLine(" {");
sb.AppendLine(" private readonly IServiceProvider _serviceProvider;");
sb.AppendLine();
sb.AppendLine(" public CommandService(IServiceProvider serviceProvider)");
sb.AppendLine(" {");
sb.AppendLine(" _serviceProvider = serviceProvider;");
sb.AppendLine(" }");
sb.AppendLine();
foreach (var command in commands)
{
if (command.HasResult)
{
sb.AppendLine($" public async Task<{command.Name}ResultDto> Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)");
sb.AppendLine(" {");
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();");
sb.AppendLine($" var command = new {command.FullyQualifiedName}");
sb.AppendLine(" {");
foreach (var prop in command.Properties)
{
var conversion = GetProtoToCSharpConversion(prop, $"request.{prop.Name}");
sb.AppendLine($" {prop.Name} = {conversion},");
}
sb.AppendLine(" };");
sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);");
sb.AppendLine($" return new {command.Name}ResultDto {{ Result = result }};");
sb.AppendLine(" }");
}
else
{
sb.AppendLine($" public async Task Execute{command.Name}Async({command.Name}Dto request, CallContext context = default)");
sb.AppendLine(" {");
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();");
sb.AppendLine($" var command = new {command.FullyQualifiedName}");
sb.AppendLine(" {");
foreach (var prop in command.Properties)
{
var conversion = GetProtoToCSharpConversion(prop, $"request.{prop.Name}");
sb.AppendLine($" {prop.Name} = {conversion},");
}
sb.AppendLine(" };");
sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);");
sb.AppendLine(" }");
}
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static string GenerateQueryService(List<QueryInfo> queries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using System.ServiceModel;");
sb.AppendLine("using System.Threading;");
sb.AppendLine("using System.Threading.Tasks;");
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
sb.AppendLine($"using {rootNamespace}.Grpc.Messages;");
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
sb.AppendLine("using ProtoBuf.Grpc;");
sb.AppendLine();
// Generate service interface
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
sb.AppendLine("{");
sb.AppendLine(" [ServiceContract]");
sb.AppendLine(" public interface IQueryService");
sb.AppendLine(" {");
foreach (var query in queries)
{
sb.AppendLine($" [OperationContract]");
sb.AppendLine($" Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default);");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine();
// Generate service implementation
sb.AppendLine(" public sealed class QueryService : IQueryService");
sb.AppendLine(" {");
sb.AppendLine(" private readonly IServiceProvider _serviceProvider;");
sb.AppendLine();
sb.AppendLine(" public QueryService(IServiceProvider serviceProvider)");
sb.AppendLine(" {");
sb.AppendLine(" _serviceProvider = serviceProvider;");
sb.AppendLine(" }");
sb.AppendLine();
foreach (var query in queries)
{
sb.AppendLine($" public async Task<{query.Name}ResultDto> Execute{query.Name}Async({query.Name}Dto request, CallContext context = default)");
sb.AppendLine(" {");
sb.AppendLine($" var handler = _serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();");
sb.AppendLine($" var query = new {query.FullyQualifiedName}");
sb.AppendLine(" {");
foreach (var prop in query.Properties)
{
var conversion = GetProtoToCSharpConversion(prop, $"request.{prop.Name}");
sb.AppendLine($" {prop.Name} = {conversion},");
}
sb.AppendLine(" };");
sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);");
sb.AppendLine($" return new {query.Name}ResultDto {{ Result = result }};");
sb.AppendLine(" }");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static string GenerateRegistrationExtensions(bool hasCommands, bool hasQueries, bool hasDynamicQueries, bool hasNotifications, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("using Microsoft.AspNetCore.Builder;");
sb.AppendLine("using Microsoft.AspNetCore.Routing;");
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
sb.AppendLine($"using {rootNamespace}.Grpc.Services;");
if (hasNotifications)
{
sb.AppendLine("using Svrnty.CQRS.Notifications.Grpc;");
}
sb.AppendLine();
sb.AppendLine($"namespace {rootNamespace}.Grpc.Extensions");
sb.AppendLine("{");
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Auto-generated extension methods for registering and mapping gRPC services");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static class GrpcServiceRegistrationExtensions");
sb.AppendLine(" {");
if (hasCommands)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers the auto-generated Command gRPC service");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IServiceCollection AddGrpcCommandService(this IServiceCollection services)");
sb.AppendLine(" {");
sb.AppendLine(" services.AddGrpc();");
sb.AppendLine(" services.AddSingleton<CommandServiceImpl>();");
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps the auto-generated Command gRPC service endpoints");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommands(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
sb.AppendLine(" endpoints.MapGrpcService<CommandServiceImpl>();");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
sb.AppendLine();
}
if (hasQueries)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers the auto-generated Query gRPC service");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IServiceCollection AddGrpcQueryService(this IServiceCollection services)");
sb.AppendLine(" {");
sb.AppendLine(" services.AddGrpc();");
sb.AppendLine(" services.AddSingleton<QueryServiceImpl>();");
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps the auto-generated Query gRPC service endpoints");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcQueries(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
sb.AppendLine(" endpoints.MapGrpcService<QueryServiceImpl>();");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
sb.AppendLine();
}
if (hasDynamicQueries)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers the auto-generated DynamicQuery gRPC service");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IServiceCollection AddGrpcDynamicQueryService(this IServiceCollection services)");
sb.AppendLine(" {");
sb.AppendLine(" services.AddGrpc();");
sb.AppendLine(" services.AddSingleton<DynamicQueryServiceImpl>();");
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps the auto-generated DynamicQuery gRPC service endpoints");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcDynamicQueries(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
sb.AppendLine(" endpoints.MapGrpcService<DynamicQueryServiceImpl>();");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
sb.AppendLine();
}
if (hasNotifications)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers the auto-generated Notification streaming gRPC service");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IServiceCollection AddGrpcNotificationService(this IServiceCollection services)");
sb.AppendLine(" {");
sb.AppendLine(" services.AddGrpc();");
sb.AppendLine(" services.AddStreamingNotifications();");
sb.AppendLine(" services.AddSingleton<NotificationServiceImpl>();");
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps the auto-generated Notification streaming gRPC service endpoints");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcNotifications(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
sb.AppendLine(" endpoints.MapGrpcService<NotificationServiceImpl>();");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
sb.AppendLine();
}
if (hasCommands || hasQueries || hasDynamicQueries || hasNotifications)
{
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers all auto-generated gRPC services (Commands, Queries, DynamicQueries, and Notifications)");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IServiceCollection AddGrpcCommandsAndQueries(this IServiceCollection services)");
sb.AppendLine(" {");
sb.AppendLine(" services.AddGrpc();");
sb.AppendLine(" services.AddGrpcReflection();");
if (hasCommands)
sb.AppendLine(" services.AddSingleton<CommandServiceImpl>();");
if (hasQueries)
sb.AppendLine(" services.AddSingleton<QueryServiceImpl>();");
if (hasDynamicQueries)
sb.AppendLine(" services.AddSingleton<DynamicQueryServiceImpl>();");
if (hasNotifications)
{
sb.AppendLine(" services.AddStreamingNotifications();");
sb.AppendLine(" services.AddSingleton<NotificationServiceImpl>();");
}
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps all auto-generated gRPC service endpoints (Commands, Queries, DynamicQueries, and Notifications)");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcCommandsAndQueries(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
if (hasCommands)
sb.AppendLine(" endpoints.MapGrpcService<CommandServiceImpl>();");
if (hasQueries)
sb.AppendLine(" endpoints.MapGrpcService<QueryServiceImpl>();");
if (hasDynamicQueries)
sb.AppendLine(" endpoints.MapGrpcService<DynamicQueryServiceImpl>();");
if (hasNotifications)
sb.AppendLine(" endpoints.MapGrpcService<NotificationServiceImpl>();");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
sb.AppendLine();
// Add configuration-based methods
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Registers gRPC services based on configuration");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IServiceCollection AddGrpcFromConfiguration(this IServiceCollection services)");
sb.AppendLine(" {");
sb.AppendLine(" var config = services.BuildServiceProvider().GetService<Svrnty.CQRS.Configuration.CqrsConfiguration>();");
sb.AppendLine(" var grpcOptions = config?.GetConfiguration<Svrnty.CQRS.Grpc.GrpcCqrsOptions>();");
sb.AppendLine(" if (grpcOptions != null)");
sb.AppendLine(" {");
sb.AppendLine(" services.AddGrpc();");
sb.AppendLine(" if (grpcOptions.ShouldEnableReflection)");
sb.AppendLine(" services.AddGrpcReflection();");
sb.AppendLine();
if (hasCommands)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapCommands())");
sb.AppendLine(" services.AddSingleton<CommandServiceImpl>();");
}
if (hasQueries)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())");
sb.AppendLine(" services.AddSingleton<QueryServiceImpl>();");
}
if (hasDynamicQueries)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())");
sb.AppendLine(" services.AddSingleton<DynamicQueryServiceImpl>();");
}
if (hasNotifications)
{
sb.AppendLine(" // Always register notification service if it exists");
sb.AppendLine(" services.AddStreamingNotifications();");
sb.AppendLine(" services.AddSingleton<NotificationServiceImpl>();");
}
sb.AppendLine(" }");
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Maps gRPC service endpoints based on configuration");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public static IEndpointRouteBuilder MapGrpcFromConfiguration(this IEndpointRouteBuilder endpoints)");
sb.AppendLine(" {");
sb.AppendLine(" var config = endpoints.ServiceProvider.GetService<Svrnty.CQRS.Configuration.CqrsConfiguration>();");
sb.AppendLine(" var grpcOptions = config?.GetConfiguration<Svrnty.CQRS.Grpc.GrpcCqrsOptions>();");
sb.AppendLine(" if (grpcOptions != null)");
sb.AppendLine(" {");
if (hasCommands)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapCommands())");
sb.AppendLine(" endpoints.MapGrpcService<CommandServiceImpl>();");
}
if (hasQueries)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())");
sb.AppendLine(" endpoints.MapGrpcService<QueryServiceImpl>();");
}
if (hasDynamicQueries)
{
sb.AppendLine(" if (grpcOptions.GetShouldMapQueries())");
sb.AppendLine(" endpoints.MapGrpcService<DynamicQueryServiceImpl>();");
}
if (hasNotifications)
{
sb.AppendLine(" // Always map notification service if it exists");
sb.AppendLine(" endpoints.MapGrpcService<NotificationServiceImpl>();");
}
sb.AppendLine();
sb.AppendLine(" if (grpcOptions.ShouldEnableReflection)");
sb.AppendLine(" endpoints.MapGrpcReflectionService();");
sb.AppendLine(" }");
sb.AppendLine(" return endpoints;");
sb.AppendLine(" }");
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static string ToCamelCase(string str)
{
if (string.IsNullOrEmpty(str) || char.IsLower(str[0]))
return str;
return char.ToLowerInvariant(str[0]) + str.Substring(1);
}
// New methods for standard gRPC generation
private static string GenerateCommandsProto(List<CommandInfo> commands, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("syntax = \"proto3\";");
sb.AppendLine();
sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";");
sb.AppendLine();
sb.AppendLine("package cqrs;");
sb.AppendLine();
sb.AppendLine("// Command service for CQRS operations");
sb.AppendLine("service CommandService {");
foreach (var command in commands)
{
var methodName = command.Name.Replace("Command", "");
sb.AppendLine($" // {command.Name}");
sb.AppendLine($" rpc {methodName} ({command.Name}Request) returns ({command.Name}Response);");
}
sb.AppendLine("}");
sb.AppendLine();
// Generate message types
foreach (var command in commands)
{
// Request message
sb.AppendLine($"message {command.Name}Request {{");
foreach (var prop in command.Properties)
{
sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};");
}
sb.AppendLine("}");
sb.AppendLine();
// Response message
sb.AppendLine($"message {command.Name}Response {{");
if (command.HasResult)
{
sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(command.ResultFullyQualifiedName!, out _, out _)} result = 1;");
}
sb.AppendLine("}");
sb.AppendLine();
}
return sb.ToString();
}
private static string GenerateQueriesProto(List<QueryInfo> queries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("syntax = \"proto3\";");
sb.AppendLine();
sb.AppendLine($"option csharp_namespace = \"{rootNamespace}.Grpc\";");
sb.AppendLine();
sb.AppendLine("package cqrs;");
sb.AppendLine();
sb.AppendLine("// Query service for CQRS operations");
sb.AppendLine("service QueryService {");
foreach (var query in queries)
{
var methodName = query.Name.Replace("Query", "");
sb.AppendLine($" // {query.Name}");
sb.AppendLine($" rpc {methodName} ({query.Name}Request) returns ({query.Name}Response);");
}
sb.AppendLine("}");
sb.AppendLine();
// Generate message types
foreach (var query in queries)
{
// Request message
sb.AppendLine($"message {query.Name}Request {{");
foreach (var prop in query.Properties)
{
sb.AppendLine($" {prop.ProtoType} {ToCamelCase(prop.Name)} = {prop.FieldNumber};");
}
sb.AppendLine("}");
sb.AppendLine();
// Response message
sb.AppendLine($"message {query.Name}Response {{");
sb.AppendLine($" {ProtoTypeMapper.MapToProtoType(query.ResultFullyQualifiedName, out _, out _)} result = 1;");
sb.AppendLine("}");
sb.AppendLine();
}
return sb.ToString();
}
private static string GenerateCommandServiceImpl(List<CommandInfo> commands, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using Grpc.Core;");
sb.AppendLine("using System.Threading.Tasks;");
sb.AppendLine("using System.Linq;");
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
sb.AppendLine("using FluentValidation;");
sb.AppendLine("using Google.Rpc;");
sb.AppendLine("using Google.Protobuf.WellKnownTypes;");
sb.AppendLine($"using {rootNamespace}.Grpc;");
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
sb.AppendLine();
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
sb.AppendLine("{");
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Auto-generated gRPC service implementation for Commands");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public sealed class CommandServiceImpl : CommandService.CommandServiceBase");
sb.AppendLine(" {");
sb.AppendLine(" private readonly IServiceScopeFactory _scopeFactory;");
sb.AppendLine();
sb.AppendLine(" public CommandServiceImpl(IServiceScopeFactory scopeFactory)");
sb.AppendLine(" {");
sb.AppendLine(" _scopeFactory = scopeFactory;");
sb.AppendLine(" }");
sb.AppendLine();
foreach (var command in commands)
{
var methodName = command.Name.Replace("Command", "");
var requestType = $"{command.Name}Request";
var responseType = $"{command.Name}Response";
sb.AppendLine($" public override async Task<{responseType}> {methodName}(");
sb.AppendLine($" {requestType} request,");
sb.AppendLine(" ServerCallContext context)");
sb.AppendLine(" {");
sb.AppendLine(" using var scope = _scopeFactory.CreateScope();");
sb.AppendLine(" var serviceProvider = scope.ServiceProvider;");
sb.AppendLine();
sb.AppendLine($" var command = new {command.FullyQualifiedName}");
sb.AppendLine(" {");
foreach (var prop in command.Properties)
{
var assignment = GeneratePropertyAssignment(prop, "request", " ");
sb.AppendLine(assignment);
}
sb.AppendLine(" };");
sb.AppendLine();
sb.AppendLine(" // Validate command if validator is registered");
sb.AppendLine($" var validator = serviceProvider.GetService<IValidator<{command.FullyQualifiedName}>>();");
sb.AppendLine(" if (validator != null)");
sb.AppendLine(" {");
sb.AppendLine(" var validationResult = await validator.ValidateAsync(command, context.CancellationToken);");
sb.AppendLine(" if (!validationResult.IsValid)");
sb.AppendLine(" {");
sb.AppendLine(" // Create Rich Error Model with structured field violations");
sb.AppendLine(" var badRequest = new BadRequest();");
sb.AppendLine(" foreach (var error in validationResult.Errors)");
sb.AppendLine(" {");
sb.AppendLine(" badRequest.FieldViolations.Add(new BadRequest.Types.FieldViolation");
sb.AppendLine(" {");
sb.AppendLine(" Field = error.PropertyName,");
sb.AppendLine(" Description = error.ErrorMessage");
sb.AppendLine(" });");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" var status = new Google.Rpc.Status");
sb.AppendLine(" {");
sb.AppendLine(" Code = (int)Code.InvalidArgument,");
sb.AppendLine(" Message = \"Validation failed\",");
sb.AppendLine(" Details = { Any.Pack(badRequest) }");
sb.AppendLine(" };");
sb.AppendLine();
sb.AppendLine(" throw status.ToRpcException();");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine($" var handler = serviceProvider.GetRequiredService<{command.HandlerInterfaceName}>();");
if (command.HasResult)
{
sb.AppendLine(" var result = await handler.HandleAsync(command, context.CancellationToken);");
// Handle Guid result conversion
if (command.ResultFullyQualifiedName?.Contains("System.Guid") == true)
{
sb.AppendLine($" return new {responseType} {{ Result = result.ToString() }};");
}
else
{
sb.AppendLine($" return new {responseType} {{ Result = result }};");
}
}
else
{
sb.AppendLine(" await handler.HandleAsync(command, context.CancellationToken);");
sb.AppendLine($" return new {responseType}();");
}
sb.AppendLine(" }");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static string GenerateQueryServiceImpl(List<QueryInfo> queries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using Grpc.Core;");
sb.AppendLine("using System.Threading.Tasks;");
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
sb.AppendLine($"using {rootNamespace}.Grpc;");
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
sb.AppendLine();
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
sb.AppendLine("{");
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Auto-generated gRPC service implementation for Queries");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public sealed class QueryServiceImpl : QueryService.QueryServiceBase");
sb.AppendLine(" {");
sb.AppendLine(" private readonly IServiceScopeFactory _scopeFactory;");
sb.AppendLine();
sb.AppendLine(" public QueryServiceImpl(IServiceScopeFactory scopeFactory)");
sb.AppendLine(" {");
sb.AppendLine(" _scopeFactory = scopeFactory;");
sb.AppendLine(" }");
sb.AppendLine();
foreach (var query in queries)
{
var methodName = query.Name.Replace("Query", "");
var requestType = $"{query.Name}Request";
var responseType = $"{query.Name}Response";
sb.AppendLine($" public override async Task<{responseType}> {methodName}(");
sb.AppendLine($" {requestType} request,");
sb.AppendLine(" ServerCallContext context)");
sb.AppendLine(" {");
sb.AppendLine(" using var scope = _scopeFactory.CreateScope();");
sb.AppendLine(" var serviceProvider = scope.ServiceProvider;");
sb.AppendLine();
sb.AppendLine($" var handler = serviceProvider.GetRequiredService<{query.HandlerInterfaceName}>();");
sb.AppendLine($" var query = new {query.FullyQualifiedName}");
sb.AppendLine(" {");
foreach (var prop in query.Properties)
{
var assignment = GeneratePropertyAssignment(prop, "request", " ");
sb.AppendLine(assignment);
}
sb.AppendLine(" };");
sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);");
// Generate response with mapping if complex type
if (query.IsResultPrimitiveType)
{
// 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)
{
var assignment = GenerateResultPropertyMapping(prop, "result", " ");
sb.AppendLine(assignment);
}
sb.AppendLine(" }");
sb.AppendLine(" };");
}
sb.AppendLine(" }");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static bool IsPrimitiveType(string typeName)
{
// Check for common primitive and built-in types
var primitiveTypes = new[]
{
"int", "System.Int32",
"long", "System.Int64",
"short", "System.Int16",
"byte", "System.Byte",
"bool", "System.Boolean",
"float", "System.Single",
"double", "System.Double",
"decimal", "System.Decimal",
"string", "System.String",
"System.DateTime",
"System.DateTimeOffset",
"System.TimeSpan",
"System.Guid"
};
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)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using System.Collections.Generic;");
sb.AppendLine("using System.ServiceModel;");
sb.AppendLine("using System.Runtime.Serialization;");
sb.AppendLine("using ProtoBuf;");
sb.AppendLine("using ProtoBuf.Grpc;");
sb.AppendLine();
sb.AppendLine($"namespace {rootNamespace}.Grpc.DynamicQuery");
sb.AppendLine("{");
// Common message types
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Dynamic query filter with support for nested AND/OR logic");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" [ProtoContract]");
sb.AppendLine(" [DataContract]");
sb.AppendLine(" public sealed class DynamicQueryFilter");
sb.AppendLine(" {");
sb.AppendLine(" [ProtoMember(1)]");
sb.AppendLine(" [DataMember(Order = 1)]");
sb.AppendLine(" public string Path { get; set; } = string.Empty;");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(2)]");
sb.AppendLine(" [DataMember(Order = 2)]");
sb.AppendLine(" public int Type { get; set; } // Maps to PoweredSoft.DynamicQuery.Core.FilterType");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(3)]");
sb.AppendLine(" [DataMember(Order = 3)]");
sb.AppendLine(" public string Value { get; set; } = string.Empty;");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(4)]");
sb.AppendLine(" [DataMember(Order = 4)]");
sb.AppendLine(" public List<DynamicQueryFilter>? And { get; set; }");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(5)]");
sb.AppendLine(" [DataMember(Order = 5)]");
sb.AppendLine(" public List<DynamicQueryFilter>? Or { get; set; }");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Dynamic query sort");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" [ProtoContract]");
sb.AppendLine(" [DataContract]");
sb.AppendLine(" public sealed class DynamicQuerySort");
sb.AppendLine(" {");
sb.AppendLine(" [ProtoMember(1)]");
sb.AppendLine(" [DataMember(Order = 1)]");
sb.AppendLine(" public string Path { get; set; } = string.Empty;");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(2)]");
sb.AppendLine(" [DataMember(Order = 2)]");
sb.AppendLine(" public bool Ascending { get; set; } = true;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Dynamic query group");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" [ProtoContract]");
sb.AppendLine(" [DataContract]");
sb.AppendLine(" public sealed class DynamicQueryGroup");
sb.AppendLine(" {");
sb.AppendLine(" [ProtoMember(1)]");
sb.AppendLine(" [DataMember(Order = 1)]");
sb.AppendLine(" public string Path { get; set; } = string.Empty;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Dynamic query aggregate");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" [ProtoContract]");
sb.AppendLine(" [DataContract]");
sb.AppendLine(" public sealed class DynamicQueryAggregate");
sb.AppendLine(" {");
sb.AppendLine(" [ProtoMember(1)]");
sb.AppendLine(" [DataMember(Order = 1)]");
sb.AppendLine(" public string Path { get; set; } = string.Empty;");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(2)]");
sb.AppendLine(" [DataMember(Order = 2)]");
sb.AppendLine(" public int Type { get; set; } // Maps to PoweredSoft.DynamicQuery.Core.AggregateType");
sb.AppendLine(" }");
sb.AppendLine();
// Generate request/response messages for each dynamic query
foreach (var dynamicQuery in dynamicQueries)
{
// Request message
sb.AppendLine($" /// <summary>");
sb.AppendLine($" /// Request message for dynamic query on {dynamicQuery.Name}");
sb.AppendLine($" /// </summary>");
sb.AppendLine(" [ProtoContract]");
sb.AppendLine(" [DataContract]");
sb.AppendLine($" public sealed class DynamicQuery{dynamicQuery.Name}Request");
sb.AppendLine(" {");
sb.AppendLine(" [ProtoMember(1)]");
sb.AppendLine(" [DataMember(Order = 1)]");
sb.AppendLine(" public int Page { get; set; }");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(2)]");
sb.AppendLine(" [DataMember(Order = 2)]");
sb.AppendLine(" public int PageSize { get; set; }");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(3)]");
sb.AppendLine(" [DataMember(Order = 3)]");
sb.AppendLine(" public List<DynamicQueryFilter> Filters { get; set; } = new();");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(4)]");
sb.AppendLine(" [DataMember(Order = 4)]");
sb.AppendLine(" public List<DynamicQuerySort> Sorts { get; set; } = new();");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(5)]");
sb.AppendLine(" [DataMember(Order = 5)]");
sb.AppendLine(" public List<DynamicQueryGroup> Groups { get; set; } = new();");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(6)]");
sb.AppendLine(" [DataMember(Order = 6)]");
sb.AppendLine(" public List<DynamicQueryAggregate> Aggregates { get; set; } = new();");
sb.AppendLine(" }");
sb.AppendLine();
// Response message
sb.AppendLine($" /// <summary>");
sb.AppendLine($" /// Response message for dynamic query on {dynamicQuery.Name}");
sb.AppendLine($" /// </summary>");
sb.AppendLine(" [ProtoContract]");
sb.AppendLine(" [DataContract]");
sb.AppendLine($" public sealed class DynamicQuery{dynamicQuery.Name}Response");
sb.AppendLine(" {");
sb.AppendLine(" [ProtoMember(1)]");
sb.AppendLine(" [DataMember(Order = 1)]");
sb.AppendLine($" public List<{dynamicQuery.DestinationTypeFullyQualified}> Data {{ get; set; }} = new();");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(2)]");
sb.AppendLine(" [DataMember(Order = 2)]");
sb.AppendLine(" public long TotalCount { get; set; }");
sb.AppendLine();
sb.AppendLine(" [ProtoMember(3)]");
sb.AppendLine(" [DataMember(Order = 3)]");
sb.AppendLine(" public int NumberOfPages { get; set; }");
sb.AppendLine(" }");
sb.AppendLine();
}
// Generate service interface
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// gRPC service interface for DynamicQueries");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" [ServiceContract]");
sb.AppendLine(" public interface IDynamicQueryService");
sb.AppendLine(" {");
foreach (var dynamicQuery in dynamicQueries)
{
var methodName = $"Query{dynamicQuery.Name}";
sb.AppendLine($" /// <summary>");
sb.AppendLine($" /// Execute dynamic query on {dynamicQuery.Name}");
sb.AppendLine($" /// </summary>");
sb.AppendLine(" [OperationContract]");
sb.AppendLine($" System.Threading.Tasks.Task<DynamicQuery{dynamicQuery.Name}Response> {methodName}Async(DynamicQuery{dynamicQuery.Name}Request request, CallContext context = default);");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static string GenerateDynamicQueryServiceImpl(List<DynamicQueryInfo> dynamicQueries, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using Grpc.Core;");
sb.AppendLine("using System.Threading.Tasks;");
sb.AppendLine("using System.Collections.Generic;");
sb.AppendLine("using System.Linq;");
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
sb.AppendLine($"using {rootNamespace}.Grpc;");
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
sb.AppendLine("using Svrnty.CQRS.DynamicQuery.Abstractions;");
sb.AppendLine("using PoweredSoft.DynamicQuery.Core;");
sb.AppendLine();
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
sb.AppendLine("{");
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Auto-generated gRPC service implementation for DynamicQueries");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public sealed class DynamicQueryServiceImpl : DynamicQueryService.DynamicQueryServiceBase");
sb.AppendLine(" {");
sb.AppendLine(" private readonly IServiceScopeFactory _scopeFactory;");
sb.AppendLine();
sb.AppendLine(" public DynamicQueryServiceImpl(IServiceScopeFactory scopeFactory)");
sb.AppendLine(" {");
sb.AppendLine(" _scopeFactory = scopeFactory;");
sb.AppendLine(" }");
sb.AppendLine();
foreach (var dynamicQuery in dynamicQueries)
{
var methodName = $"Query{dynamicQuery.Name}";
var requestType = $"DynamicQuery{dynamicQuery.Name}Request";
var responseType = $"DynamicQuery{dynamicQuery.Name}Response";
sb.AppendLine($" public override async Task<{responseType}> {methodName}(");
sb.AppendLine($" {requestType} request,");
sb.AppendLine(" ServerCallContext context)");
sb.AppendLine(" {");
sb.AppendLine(" using var scope = _scopeFactory.CreateScope();");
sb.AppendLine(" var serviceProvider = scope.ServiceProvider;");
sb.AppendLine();
// Build the dynamic query object
if (dynamicQuery.HasParams)
{
sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}, {dynamicQuery.ParamsTypeFullyQualified}>");
}
else
{
sb.AppendLine($" var query = new Svrnty.CQRS.DynamicQuery.DynamicQuery<{dynamicQuery.SourceTypeFullyQualified}, {dynamicQuery.DestinationTypeFullyQualified}>");
}
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) ?? 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();
// Get the handler and execute
sb.AppendLine($" var handler = serviceProvider.GetRequiredService<IQueryHandler<{dynamicQuery.QueryInterfaceName}, IQueryExecutionResult<{dynamicQuery.DestinationTypeFullyQualified}>>>();");
sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);");
sb.AppendLine();
// Build response
sb.AppendLine($" var response = new {responseType}");
sb.AppendLine(" {");
sb.AppendLine(" TotalRecords = result.TotalRecords,");
sb.AppendLine(" NumberOfPages = (int)(result.NumberOfPages ?? 0)");
sb.AppendLine(" };");
sb.AppendLine();
sb.AppendLine(" if (result.Data != null)");
sb.AppendLine(" {");
sb.AppendLine(" foreach (var item in result.Data)");
sb.AppendLine(" {");
sb.AppendLine($" response.Data.Add(MapTo{dynamicQuery.Name}ProtoModel(item));");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" // TODO: Add aggregates and groups to response if needed");
sb.AppendLine();
sb.AppendLine(" return response;");
sb.AppendLine(" }");
sb.AppendLine();
}
// Add helper methods for converting proto messages to AspNetCore types
sb.AppendLine(" private static List<Svrnty.CQRS.DynamicQuery.DynamicQueryFilter>? ConvertFilters(Google.Protobuf.Collections.RepeatedField<DynamicQueryFilter> protoFilters)");
sb.AppendLine(" {");
sb.AppendLine(" if (protoFilters == null || protoFilters.Count == 0)");
sb.AppendLine(" return null;");
sb.AppendLine();
sb.AppendLine(" var filters = new List<Svrnty.CQRS.DynamicQuery.DynamicQueryFilter>();");
sb.AppendLine(" foreach (var protoFilter in protoFilters)");
sb.AppendLine(" {");
sb.AppendLine(" var filter = new Svrnty.CQRS.DynamicQuery.DynamicQueryFilter");
sb.AppendLine(" {");
sb.AppendLine(" Path = protoFilter.Path,");
sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.FilterType)protoFilter.Type).ToString(),");
sb.AppendLine(" Value = protoFilter.Value");
sb.AppendLine(" };");
sb.AppendLine();
sb.AppendLine(" // Handle nested AND filters");
sb.AppendLine(" if (protoFilter.And != null && protoFilter.And.Count > 0)");
sb.AppendLine(" {");
sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(protoFilter.And);");
sb.AppendLine(" filter.And = true;");
sb.AppendLine(" }");
sb.AppendLine(" // Handle nested OR filters");
sb.AppendLine(" else if (protoFilter.Or != null && protoFilter.Or.Count > 0)");
sb.AppendLine(" {");
sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(protoFilter.Or);");
sb.AppendLine(" filter.And = false;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" filters.Add(filter);");
sb.AppendLine(" }");
sb.AppendLine(" return filters;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" private static List<Svrnty.CQRS.DynamicQuery.DynamicQueryFilter> ConvertProtoFiltersToList(Google.Protobuf.Collections.RepeatedField<DynamicQueryFilter> protoFilters)");
sb.AppendLine(" {");
sb.AppendLine(" var result = new List<Svrnty.CQRS.DynamicQuery.DynamicQueryFilter>();");
sb.AppendLine(" foreach (var pf in protoFilters)");
sb.AppendLine(" {");
sb.AppendLine(" var filter = new Svrnty.CQRS.DynamicQuery.DynamicQueryFilter");
sb.AppendLine(" {");
sb.AppendLine(" Path = pf.Path,");
sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.FilterType)pf.Type).ToString(),");
sb.AppendLine(" Value = pf.Value");
sb.AppendLine(" };");
sb.AppendLine(" if (pf.And != null && pf.And.Count > 0)");
sb.AppendLine(" {");
sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(pf.And);");
sb.AppendLine(" filter.And = true;");
sb.AppendLine(" }");
sb.AppendLine(" else if (pf.Or != null && pf.Or.Count > 0)");
sb.AppendLine(" {");
sb.AppendLine(" filter.Filters = ConvertProtoFiltersToList(pf.Or);");
sb.AppendLine(" filter.And = false;");
sb.AppendLine(" }");
sb.AppendLine(" result.Add(filter);");
sb.AppendLine(" }");
sb.AppendLine(" return result;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" private static List<PoweredSoft.DynamicQuery.Sort>? ConvertSorts(Google.Protobuf.Collections.RepeatedField<DynamicQuerySort> protoSorts)");
sb.AppendLine(" {");
sb.AppendLine(" if (protoSorts == null || protoSorts.Count == 0)");
sb.AppendLine(" return null;");
sb.AppendLine();
sb.AppendLine(" return protoSorts.Select(s => new PoweredSoft.DynamicQuery.Sort");
sb.AppendLine(" {");
sb.AppendLine(" Path = s.Path,");
sb.AppendLine(" Ascending = s.Ascending");
sb.AppendLine(" }).ToList();");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" private static List<PoweredSoft.DynamicQuery.Group>? ConvertGroups(Google.Protobuf.Collections.RepeatedField<DynamicQueryGroup> protoGroups)");
sb.AppendLine(" {");
sb.AppendLine(" if (protoGroups == null || protoGroups.Count == 0)");
sb.AppendLine(" return null;");
sb.AppendLine();
sb.AppendLine(" return protoGroups.Select(g => new PoweredSoft.DynamicQuery.Group");
sb.AppendLine(" {");
sb.AppendLine(" Path = g.Path");
sb.AppendLine(" }).ToList();");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" private static List<Svrnty.CQRS.DynamicQuery.DynamicQueryAggregate>? ConvertAggregates(Google.Protobuf.Collections.RepeatedField<DynamicQueryAggregate> protoAggregates)");
sb.AppendLine(" {");
sb.AppendLine(" if (protoAggregates == null || protoAggregates.Count == 0)");
sb.AppendLine(" return null;");
sb.AppendLine();
sb.AppendLine(" return protoAggregates.Select(a => new Svrnty.CQRS.DynamicQuery.DynamicQueryAggregate");
sb.AppendLine(" {");
sb.AppendLine(" Path = a.Path,");
sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.AggregateType)a.Type).ToString()");
sb.AppendLine(" }).ToList();");
sb.AppendLine(" }");
sb.AppendLine();
// Add generic reflection-based mapper helper
sb.AppendLine(" private static TProto MapToProtoModel<TDomain, TProto>(TDomain domainModel) where TProto : Google.Protobuf.IMessage, new()");
sb.AppendLine(" {");
sb.AppendLine(" if (domainModel == null) return new TProto();");
sb.AppendLine(" var proto = new TProto();");
sb.AppendLine(" var domainProps = typeof(TDomain).GetProperties();");
sb.AppendLine(" var protoDesc = proto.Descriptor;");
sb.AppendLine();
sb.AppendLine(" foreach (var domainProp in domainProps)");
sb.AppendLine(" {");
sb.AppendLine(" // Convert property name to proto field name (PascalCase to snake_case)");
sb.AppendLine(" var protoFieldName = ToSnakeCase(domainProp.Name);");
sb.AppendLine(" var protoField = protoDesc.FindFieldByName(protoFieldName);");
sb.AppendLine(" if (protoField == null) continue;");
sb.AppendLine();
sb.AppendLine(" var domainValue = domainProp.GetValue(domainModel);");
sb.AppendLine(" if (domainValue == null) continue;");
sb.AppendLine();
sb.AppendLine(" var protoAccessor = protoField.Accessor;");
sb.AppendLine();
sb.AppendLine(" // Handle DateTime -> Timestamp conversion");
sb.AppendLine(" if (domainProp.PropertyType == typeof(DateTime) || domainProp.PropertyType == typeof(DateTime?))");
sb.AppendLine(" {");
sb.AppendLine(" var dateTime = (DateTime)domainValue;");
sb.AppendLine(" // Ensure UTC for Timestamp conversion");
sb.AppendLine(" if (dateTime.Kind != DateTimeKind.Utc)");
sb.AppendLine(" dateTime = dateTime.ToUniversalTime();");
sb.AppendLine(" protoAccessor.SetValue(proto, Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(dateTime));");
sb.AppendLine(" }");
sb.AppendLine(" else if (domainProp.PropertyType == typeof(DateTimeOffset) || domainProp.PropertyType == typeof(DateTimeOffset?))");
sb.AppendLine(" {");
sb.AppendLine(" var dateTimeOffset = (DateTimeOffset)domainValue;");
sb.AppendLine(" protoAccessor.SetValue(proto, Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset(dateTimeOffset));");
sb.AppendLine(" }");
sb.AppendLine(" // Handle collections (List<T>, IList<T>, etc.) - must check before complex types");
sb.AppendLine(" else if (protoField.IsRepeated && domainValue is System.Collections.IEnumerable enumerable && domainProp.PropertyType != typeof(string))");
sb.AppendLine(" {");
sb.AppendLine(" var repeatedField = protoAccessor.GetValue(proto);");
sb.AppendLine(" if (repeatedField == null) continue;");
sb.AppendLine();
sb.AppendLine(" // Get the element type of the RepeatedField<T>");
sb.AppendLine(" var repeatedFieldType = repeatedField.GetType();");
sb.AppendLine(" var repeatedElementType = repeatedFieldType.IsGenericType ? repeatedFieldType.GetGenericArguments()[0] : null;");
sb.AppendLine(" if (repeatedElementType == null) continue;");
sb.AppendLine();
sb.AppendLine(" // Get Add(T) method with specific parameter type to avoid ambiguity");
sb.AppendLine(" var addMethod = repeatedFieldType.GetMethod(\"Add\", new[] { repeatedElementType });");
sb.AppendLine(" if (addMethod == null) continue;");
sb.AppendLine();
sb.AppendLine(" // Get element types");
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();
sb.AppendLine(" foreach (var item in enumerable)");
sb.AppendLine(" {");
sb.AppendLine(" if (item == null) continue;");
sb.AppendLine();
sb.AppendLine(" // Check if elements need mapping (complex types)");
sb.AppendLine(" if (protoElementType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoElementType) && domainElementType != null)");
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(" else");
sb.AppendLine(" {");
sb.AppendLine(" // Primitive types, enums, strings - add directly");
sb.AppendLine(" try { addMethod.Invoke(repeatedField, new[] { item }); }");
sb.AppendLine(" catch { /* Type mismatch, skip */ }");
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(" if (protoFieldType != null && typeof(Google.Protobuf.IMessage).IsAssignableFrom(protoFieldType))");
sb.AppendLine(" {");
sb.AppendLine(" var mapMethod = typeof(DynamicQueryServiceImpl).GetMethod(\"MapToProtoModel\", ");
sb.AppendLine(" System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!");
sb.AppendLine(" .MakeGenericMethod(domainProp.PropertyType, protoFieldType);");
sb.AppendLine(" var nestedProto = mapMethod.Invoke(null, new[] { domainValue });");
sb.AppendLine(" if (nestedProto != null)");
sb.AppendLine(" protoAccessor.SetValue(proto, nestedProto);");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine(" // Handle decimal -> string conversion");
sb.AppendLine(" else if (domainProp.PropertyType == typeof(decimal) || domainProp.PropertyType == typeof(decimal?))");
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");
sb.AppendLine(" try { protoAccessor.SetValue(proto, domainValue); }");
sb.AppendLine(" catch { /* Type mismatch, skip */ }");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine(" return proto;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" private static string ToSnakeCase(string str)");
sb.AppendLine(" {");
sb.AppendLine(" if (string.IsNullOrEmpty(str)) return str;");
sb.AppendLine(" var result = new System.Text.StringBuilder();");
sb.AppendLine(" for (int i = 0; i < str.Length; i++)");
sb.AppendLine(" {");
sb.AppendLine(" if (i > 0 && char.IsUpper(str[i]))");
sb.AppendLine(" result.Append('_');");
sb.AppendLine(" result.Append(char.ToLowerInvariant(str[i]));");
sb.AppendLine(" }");
sb.AppendLine(" return result.ToString();");
sb.AppendLine(" }");
sb.AppendLine();
// Add mapper methods for each entity type
foreach (var dynamicQuery in dynamicQueries)
{
var entityName = dynamicQuery.Name;
var protoTypeName = $"{entityName.TrimEnd('s')}"; // User from Users
sb.AppendLine($" private static {protoTypeName} MapTo{entityName}ProtoModel({dynamicQuery.DestinationTypeFullyQualified} domainModel)");
sb.AppendLine(" {");
sb.AppendLine($" return MapToProtoModel<{dynamicQuery.DestinationTypeFullyQualified}, {protoTypeName}>(domainModel);");
sb.AppendLine(" }");
sb.AppendLine();
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
/// <summary>
/// Discovers types marked with [StreamingNotification] attribute
/// </summary>
private static List<NotificationInfo> DiscoverNotifications(IEnumerable<INamedTypeSymbol> allTypes, Compilation compilation)
{
var streamingNotificationAttribute = compilation.GetTypeByMetadataName(
"Svrnty.CQRS.Notifications.Abstractions.StreamingNotificationAttribute");
if (streamingNotificationAttribute == null)
return new List<NotificationInfo>();
var notifications = new List<NotificationInfo>();
foreach (var type in allTypes)
{
if (type.IsAbstract || type.IsStatic)
continue;
var attr = type.GetAttributes()
.FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(
a.AttributeClass, streamingNotificationAttribute));
if (attr == null)
continue;
// Extract SubscriptionKey from attribute
var subscriptionKeyArg = attr.NamedArguments
.FirstOrDefault(a => a.Key == "SubscriptionKey");
var subscriptionKeyProp = subscriptionKeyArg.Value.Value as string;
if (string.IsNullOrEmpty(subscriptionKeyProp))
continue;
// Get all properties of the notification type
var properties = new List<PropertyInfo>();
int fieldNumber = 1;
foreach (var prop in type.GetMembers().OfType<IPropertySymbol>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public))
{
var propType = prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var protoType = ProtoTypeMapper.MapToProtoType(propType, out _, out _);
properties.Add(new PropertyInfo
{
Name = prop.Name,
Type = propType,
FullyQualifiedType = propType,
ProtoType = protoType,
FieldNumber = fieldNumber++,
IsEnum = prop.Type.TypeKind == TypeKind.Enum,
IsDecimal = propType.Contains("decimal") || propType.Contains("Decimal"),
IsDateTime = propType.Contains("DateTime")
});
}
// Find the subscription key property info
var keyPropInfo = properties.FirstOrDefault(p => p.Name == subscriptionKeyProp);
if (keyPropInfo == null)
continue;
notifications.Add(new NotificationInfo
{
Name = type.Name,
FullyQualifiedName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
Namespace = type.ContainingNamespace?.ToDisplayString() ?? "",
SubscriptionKeyProperty = subscriptionKeyProp!, // Already validated as non-null above
SubscriptionKeyInfo = keyPropInfo,
Properties = properties
});
}
return notifications;
}
/// <summary>
/// Generates the NotificationServiceImpl class for streaming notifications
/// </summary>
private static string GenerateNotificationServiceImpl(List<NotificationInfo> notifications, string rootNamespace)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("#nullable enable");
sb.AppendLine("using Grpc.Core;");
sb.AppendLine("using System.Threading.Tasks;");
sb.AppendLine("using System.Threading;");
sb.AppendLine("using Google.Protobuf.WellKnownTypes;");
sb.AppendLine($"using {rootNamespace}.Grpc;");
sb.AppendLine("using Svrnty.CQRS.Notifications.Grpc;");
sb.AppendLine();
sb.AppendLine($"namespace {rootNamespace}.Grpc.Services");
sb.AppendLine("{");
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Auto-generated gRPC service implementation for streaming Notifications");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" public sealed class NotificationServiceImpl : NotificationService.NotificationServiceBase");
sb.AppendLine(" {");
sb.AppendLine(" private readonly NotificationSubscriptionManager _subscriptionManager;");
sb.AppendLine();
sb.AppendLine(" public NotificationServiceImpl(NotificationSubscriptionManager subscriptionManager)");
sb.AppendLine(" {");
sb.AppendLine(" _subscriptionManager = subscriptionManager;");
sb.AppendLine(" }");
foreach (var notification in notifications)
{
var methodName = $"SubscribeTo{notification.Name}";
var requestType = $"SubscribeTo{notification.Name}Request";
var keyPropName = notification.SubscriptionKeyProperty;
// Proto uses PascalCase for C# properties
var keyPropPascal = ToPascalCaseHelper(ToSnakeCaseHelper(keyPropName));
sb.AppendLine();
sb.AppendLine($" public override async Task {methodName}(");
sb.AppendLine($" {requestType} request,");
sb.AppendLine($" IServerStreamWriter<{notification.Name}> responseStream,");
sb.AppendLine(" ServerCallContext context)");
sb.AppendLine(" {");
sb.AppendLine($" // Subscribe with mapper from domain notification to proto message");
sb.AppendLine($" using var subscription = _subscriptionManager.Subscribe<{notification.FullyQualifiedName}, {notification.Name}>(");
sb.AppendLine($" request.{keyPropPascal},");
sb.AppendLine($" responseStream,");
sb.AppendLine($" domainNotification => Map{notification.Name}(domainNotification));");
sb.AppendLine();
sb.AppendLine(" // Keep the stream alive until client disconnects");
sb.AppendLine(" try");
sb.AppendLine(" {");
sb.AppendLine(" await Task.Delay(Timeout.Infinite, context.CancellationToken);");
sb.AppendLine(" }");
sb.AppendLine(" catch (OperationCanceledException)");
sb.AppendLine(" {");
sb.AppendLine(" // Client disconnected - normal behavior");
sb.AppendLine(" }");
sb.AppendLine(" }");
}
// Generate mapper methods
foreach (var notification in notifications)
{
sb.AppendLine();
sb.AppendLine($" private static {notification.Name} Map{notification.Name}({notification.FullyQualifiedName} domain)");
sb.AppendLine(" {");
sb.AppendLine($" return new {notification.Name}");
sb.AppendLine(" {");
foreach (var prop in notification.Properties)
{
var protoFieldName = ToPascalCaseHelper(ToSnakeCaseHelper(prop.Name));
if (prop.IsDateTime)
{
sb.AppendLine($" {protoFieldName} = Timestamp.FromDateTime(domain.{prop.Name}.ToUniversalTime()),");
}
else if (prop.IsDecimal)
{
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
var simpleTypeName = prop.Type.Replace("global::", "").Split('.').Last();
sb.AppendLine($" {protoFieldName} = ({simpleTypeName})((int)domain.{prop.Name}),");
}
else
{
sb.AppendLine($" {protoFieldName} = domain.{prop.Name},");
}
}
sb.AppendLine(" };");
sb.AppendLine(" }");
}
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
private static string ToSnakeCaseHelper(string str)
{
if (string.IsNullOrEmpty(str)) return str;
var result = new StringBuilder();
for (int i = 0; i < str.Length; i++)
{
if (i > 0 && char.IsUpper(str[i]))
result.Append('_');
result.Append(char.ToLowerInvariant(str[i]));
}
return result.ToString();
}
private static string ToPascalCaseHelper(string snakeCase)
{
if (string.IsNullOrEmpty(snakeCase)) return snakeCase;
var parts = snakeCase.Split('_');
return string.Join("", parts.Select(p =>
p.Length > 0 ? char.ToUpperInvariant(p[0]) + p.Substring(1).ToLowerInvariant() : ""));
}
}
}