dotnet-cqrs/Svrnty.CQRS.Grpc.Generators/GrpcGenerator.cs

2390 lines
122 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),
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);
// 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 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;
}
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),
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);
}
}
// 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
{
// 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 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 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},";
}
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)
{
queryInfo.ResultProperties.Add(new PropertyInfo
{
Name = property.Name,
Type = property.Type.ToDisplayString(),
FullyQualifiedType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
ProtoType = string.Empty, // Not needed for result mapping
FieldNumber = 0 // Not needed for result mapping
});
}
}
// 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);
queryInfo.Properties.Add(new PropertyInfo
{
Name = property.Name,
Type = propertyType,
ProtoType = protoType,
FieldNumber = fieldNumber++
});
}
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)
{
sb.AppendLine($" {prop.Name} = request.{prop.Name}!,");
}
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)
{
sb.AppendLine($" {prop.Name} = request.{prop.Name}!,");
}
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)
{
sb.AppendLine($" {prop.Name} = request.{prop.Name}!,");
}
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);");
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)
{
sb.AppendLine($" {prop.Name} = request.{char.ToUpper(prop.Name[0]) + prop.Name.Substring(1)},");
}
sb.AppendLine(" };");
sb.AppendLine(" var result = await handler.HandleAsync(query, context.CancellationToken);");
// Generate response with mapping if complex type
if (query.IsResultPrimitiveType)
{
sb.AppendLine($" return new {responseType} {{ Result = result }};");
}
else
{
// Complex type - need to map from C# type to proto type
sb.AppendLine($" return new {responseType}");
sb.AppendLine(" {");
sb.AppendLine($" Result = new {query.ResultType}");
sb.AppendLine(" {");
foreach (var prop in query.ResultProperties)
{
sb.AppendLine($" {prop.Name} = result.{prop.Name},");
}
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"
};
return primitiveTypes.Contains(typeName) ||
typeName.StartsWith("System.Nullable<") ||
typeName.EndsWith("?");
}
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),");
sb.AppendLine(" Sorts = ConvertSorts(request.Sorts),");
sb.AppendLine(" Groups = ConvertGroups(request.Groups),");
sb.AppendLine(" Aggregates = ConvertAggregates(request.Aggregates)");
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(" 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,
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.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() : ""));
}
}
}