872 lines
41 KiB
C#
872 lines
41 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
|
|
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;
|
|
}
|
|
|
|
private static void Execute(Compilation compilation, IEnumerable<INamedTypeSymbol?> types, SourceProductionContext context)
|
|
{
|
|
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");
|
|
|
|
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
|
|
|
|
// Find all command and query types by looking at handler implementations
|
|
foreach (var typeSymbol in types)
|
|
{
|
|
if (typeSymbol == null || 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)
|
|
queryMap[queryType] = resultType;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 (grpcIgnoreAttribute != null && HasAttribute(commandType, grpcIgnoreAttribute))
|
|
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 (grpcIgnoreAttribute != null && HasAttribute(queryType, grpcIgnoreAttribute))
|
|
continue;
|
|
|
|
var queryInfo = ExtractQueryInfo(queryType, resultType);
|
|
if (queryInfo != null)
|
|
queries.Add(queryInfo);
|
|
}
|
|
|
|
// Generate services if we found any commands or queries
|
|
if (commands.Any() || queries.Any())
|
|
{
|
|
GenerateProtoAndServices(context, commands, queries, compilation);
|
|
}
|
|
}
|
|
|
|
private static bool HasAttribute(INamedTypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol)
|
|
{
|
|
return typeSymbol.GetAttributes().Any(attr =>
|
|
SymbolEqualityComparer.Default.Equals(attr.AttributeClass, attributeSymbol));
|
|
}
|
|
|
|
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);
|
|
commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandType.Name}, {resultType.Name}>";
|
|
}
|
|
else
|
|
{
|
|
commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandType.Name}>";
|
|
}
|
|
|
|
// 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);
|
|
|
|
commandInfo.Properties.Add(new PropertyInfo
|
|
{
|
|
Name = property.Name,
|
|
Type = propertyType,
|
|
ProtoType = protoType,
|
|
FieldNumber = fieldNumber++
|
|
});
|
|
}
|
|
|
|
return commandInfo;
|
|
}
|
|
|
|
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);
|
|
queryInfo.HandlerInterfaceName = $"IQueryHandler<{queryType.Name}, {resultType.Name}>";
|
|
|
|
// 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 void GenerateProtoAndServices(SourceProductionContext context, List<CommandInfo> commands, List<QueryInfo> queries, 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 registration extensions
|
|
var registrationExtensions = GenerateRegistrationExtensions(commands.Any(), queries.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, 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;");
|
|
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 (hasCommands && hasQueries)
|
|
{
|
|
sb.AppendLine(" /// <summary>");
|
|
sb.AppendLine(" /// Registers both Command and Query gRPC services");
|
|
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>();");
|
|
sb.AppendLine(" return services;");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" /// <summary>");
|
|
sb.AppendLine(" /// Maps both Command and Query gRPC service endpoints");
|
|
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>();");
|
|
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 IServiceProvider _serviceProvider;");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" public CommandServiceImpl(IServiceProvider serviceProvider)");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" _serviceProvider = serviceProvider;");
|
|
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($" var command = new {command.FullyQualifiedName}");
|
|
sb.AppendLine(" {");
|
|
foreach (var prop in command.Properties)
|
|
{
|
|
sb.AppendLine($" {prop.Name} = request.{char.ToUpper(prop.Name[0]) + prop.Name.Substring(1)},");
|
|
}
|
|
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 IServiceProvider _serviceProvider;");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" public QueryServiceImpl(IServiceProvider serviceProvider)");
|
|
sb.AppendLine(" {");
|
|
sb.AppendLine(" _serviceProvider = serviceProvider;");
|
|
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($" 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);");
|
|
sb.AppendLine($" return new {responseType} {{ Result = result }};");
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
}
|
|
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine("}");
|
|
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
}
|