Handle generic types in proto message name generation
All checks were successful
Publish NuGets / build (release) Successful in 39s

Generic types like Translation<T> now produce qualified message names
(e.g. TranslationOfFaqTranslationQueryItem) to avoid duplicate message
definitions in generated .proto files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
David Nguyen 2026-02-19 18:56:37 -05:00
parent b372805c4e
commit 6aece5a769
Signed by: david.nguyen
GPG Key ID: D5FB5A5715829326
2 changed files with 24 additions and 5 deletions

View File

@ -413,17 +413,21 @@ internal class ProtoFileGenerator
private void GenerateComplexTypeMessage(INamedTypeSymbol? type) private void GenerateComplexTypeMessage(INamedTypeSymbol? type)
{ {
if (type == null || _generatedMessages.Contains(type.Name)) if (type == null)
return;
var messageName = ProtoFileTypeMapper.GetProtoMessageName(type);
if (_generatedMessages.Contains(messageName))
return; return;
// Don't generate messages for system types or primitives // Don't generate messages for system types or primitives
if (type.ContainingNamespace?.ToString().StartsWith("System") == true) if (type.ContainingNamespace?.ToString().StartsWith("System") == true)
return; return;
_generatedMessages.Add(type.Name); _generatedMessages.Add(messageName);
_messagesBuilder.AppendLine($"// {type.Name} entity"); _messagesBuilder.AppendLine($"// {messageName} entity");
_messagesBuilder.AppendLine($"message {type.Name} {{"); _messagesBuilder.AppendLine($"message {messageName} {{");
// Collect nested complex types to generate after closing this message // Collect nested complex types to generate after closing this message
var nestedComplexTypes = new List<INamedTypeSymbol>(); var nestedComplexTypes = new List<INamedTypeSymbol>();

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
namespace Svrnty.CQRS.Grpc.Generators; namespace Svrnty.CQRS.Grpc.Generators;
@ -151,13 +152,27 @@ internal static class ProtoFileTypeMapper
// Complex types (classes/records) become message types // Complex types (classes/records) become message types
if (typeSymbol.TypeKind == TypeKind.Class || typeSymbol.TypeKind == TypeKind.Struct) if (typeSymbol.TypeKind == TypeKind.Class || typeSymbol.TypeKind == TypeKind.Struct)
{ {
return typeName; // Reference the message type by name return GetProtoMessageName(typeSymbol); // Reference the message type by name (handles generics)
} }
// Fallback // Fallback
return "string"; // Default to string for unknown types return "string"; // Default to string for unknown types
} }
/// <summary>
/// Gets the proto message name for a type, handling generic types by qualifying
/// with type arguments. e.g. Translation&lt;FaqTranslationQueryItem&gt; becomes TranslationOfFaqTranslationQueryItem.
/// </summary>
public static string GetProtoMessageName(ITypeSymbol typeSymbol)
{
if (typeSymbol is INamedTypeSymbol namedType && namedType.IsGenericType && namedType.TypeArguments.Length > 0)
{
var typeArgs = string.Join("And", namedType.TypeArguments.Select(t => GetProtoMessageName(t)));
return $"{namedType.Name}Of{typeArgs}";
}
return typeSymbol.Name;
}
/// <summary> /// <summary>
/// Converts C# PascalCase property name to proto snake_case field name. /// Converts C# PascalCase property name to proto snake_case field name.
/// Uses simple conversion: add underscore before each uppercase letter (except first). /// Uses simple conversion: add underscore before each uppercase letter (except first).