2390 lines
122 KiB
C#
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() : ""));
|
|
}
|
|
}
|
|
}
|