Compare commits
4 Commits
feat/nutri
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bd43bc9bde | |||
| 661f5b4b1c | |||
| 99aebcf314 | |||
|
|
f76dbb1a97 |
File diff suppressed because it is too large
Load Diff
@ -31,11 +31,6 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
|
||||
public class PropertyInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
/// <summary>
|
||||
/// The property name as generated by Grpc.Tools from the proto field name.
|
||||
/// This may differ from the C# property name due to casing differences.
|
||||
/// </summary>
|
||||
public string ProtoPropertyName { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string FullyQualifiedType { get; set; }
|
||||
public string ProtoType { get; set; }
|
||||
@ -49,14 +44,19 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
|
||||
public bool IsNullable { get; set; }
|
||||
public bool IsDecimal { get; set; }
|
||||
public bool IsDateTime { get; set; }
|
||||
public bool IsGuid { get; set; }
|
||||
public bool IsJsonElement { get; set; }
|
||||
public bool IsBinaryType { get; set; } // Stream, byte[], MemoryStream
|
||||
public bool IsStream { get; set; } // Specifically Stream types (not byte[])
|
||||
public bool IsReadOnly { get; set; } // Read-only/computed properties should be skipped
|
||||
public string? ElementType { get; set; }
|
||||
public bool IsElementComplexType { get; set; }
|
||||
public bool IsElementGuid { get; set; }
|
||||
public List<PropertyInfo>? ElementNestedProperties { get; set; }
|
||||
|
||||
public PropertyInfo()
|
||||
{
|
||||
Name = string.Empty;
|
||||
ProtoPropertyName = string.Empty;
|
||||
Type = string.Empty;
|
||||
FullyQualifiedType = string.Empty;
|
||||
ProtoType = string.Empty;
|
||||
@ -67,7 +67,13 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
|
||||
IsNullable = false;
|
||||
IsDecimal = false;
|
||||
IsDateTime = false;
|
||||
IsGuid = false;
|
||||
IsJsonElement = false;
|
||||
IsBinaryType = false;
|
||||
IsStream = false;
|
||||
IsReadOnly = false;
|
||||
IsElementComplexType = false;
|
||||
IsElementGuid = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,26 +13,6 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
|
||||
public string HandlerInterfaceName { get; set; }
|
||||
public List<PropertyInfo> ResultProperties { get; set; }
|
||||
public bool IsResultPrimitiveType { get; set; }
|
||||
/// <summary>
|
||||
/// True if the result type is a collection (List, IEnumerable, etc.)
|
||||
/// </summary>
|
||||
public bool IsResultCollection { get; set; }
|
||||
/// <summary>
|
||||
/// The element type name if IsResultCollection is true
|
||||
/// </summary>
|
||||
public string ResultElementType { get; set; }
|
||||
/// <summary>
|
||||
/// The fully qualified element type name if IsResultCollection is true
|
||||
/// </summary>
|
||||
public string ResultElementTypeFullyQualified { get; set; }
|
||||
/// <summary>
|
||||
/// True if the result type is nullable (ends with ? or is Nullable<T>)
|
||||
/// </summary>
|
||||
public bool IsResultNullable { get; set; }
|
||||
/// <summary>
|
||||
/// The result type name without the nullable annotation (e.g., "CnfFoodDetailItem" instead of "CnfFoodDetailItem?")
|
||||
/// </summary>
|
||||
public string ResultTypeWithoutNullable { get; set; }
|
||||
|
||||
public QueryInfo()
|
||||
{
|
||||
@ -45,11 +25,6 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
|
||||
HandlerInterfaceName = string.Empty;
|
||||
ResultProperties = new List<PropertyInfo>();
|
||||
IsResultPrimitiveType = false;
|
||||
IsResultCollection = false;
|
||||
ResultElementType = string.Empty;
|
||||
ResultElementTypeFullyQualified = string.Empty;
|
||||
IsResultNullable = false;
|
||||
ResultTypeWithoutNullable = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,7 +320,9 @@ internal class ProtoFileGenerator
|
||||
|
||||
var properties = type.GetMembers()
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public)
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public &&
|
||||
!p.IsIndexer &&
|
||||
!ProtoFileTypeMapper.IsCollectionInternalProperty(p.Name))
|
||||
.ToList();
|
||||
|
||||
// Collect nested complex types to generate after closing this message
|
||||
@ -403,14 +405,9 @@ internal class ProtoFileGenerator
|
||||
_messagesBuilder.AppendLine();
|
||||
|
||||
// Generate complex type message after closing the response message
|
||||
// Use GetElementOrUnderlyingType to extract element type from collections (e.g., List<CnfFoodItem> -> CnfFoodItem)
|
||||
if (resultType != null)
|
||||
if (resultType != null && IsComplexType(resultType))
|
||||
{
|
||||
var underlyingType = ProtoFileTypeMapper.GetElementOrUnderlyingType(resultType);
|
||||
if (IsComplexType(underlyingType) && underlyingType is INamedTypeSymbol namedType)
|
||||
{
|
||||
GenerateComplexTypeMessage(namedType);
|
||||
}
|
||||
GenerateComplexTypeMessage(resultType as INamedTypeSymbol);
|
||||
}
|
||||
}
|
||||
|
||||
@ -428,48 +425,73 @@ internal class ProtoFileGenerator
|
||||
_messagesBuilder.AppendLine($"// {type.Name} entity");
|
||||
_messagesBuilder.AppendLine($"message {type.Name} {{");
|
||||
|
||||
var properties = type.GetMembers()
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public)
|
||||
.ToList();
|
||||
|
||||
// Collect nested complex types to generate after closing this message
|
||||
var nestedComplexTypes = new List<INamedTypeSymbol>();
|
||||
|
||||
int fieldNumber = 1;
|
||||
foreach (var prop in properties)
|
||||
// Check if this type is a collection (implements IList<T>, ICollection<T>, etc.)
|
||||
var collectionElementType = ProtoFileTypeMapper.GetCollectionElementTypeByInterface(type);
|
||||
if (collectionElementType != null)
|
||||
{
|
||||
if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type))
|
||||
{
|
||||
_messagesBuilder.AppendLine($" // Skipped: {prop.Name} - unsupported type {prop.Type.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var protoType = ProtoFileTypeMapper.MapType(prop.Type, out var needsImport, out var importPath);
|
||||
// This type is a collection - generate a single repeated field for items
|
||||
var protoElementType = ProtoFileTypeMapper.MapType(collectionElementType, out var needsImport, out var importPath);
|
||||
if (needsImport && importPath != null)
|
||||
{
|
||||
_requiredImports.Add(importPath);
|
||||
}
|
||||
|
||||
var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name);
|
||||
_messagesBuilder.AppendLine($" {protoType} {fieldName} = {fieldNumber};");
|
||||
_messagesBuilder.AppendLine($" repeated {protoElementType} items = 1;");
|
||||
|
||||
// Track enums for later generation
|
||||
var enumType = ProtoFileTypeMapper.GetEnumType(prop.Type);
|
||||
if (enumType != null)
|
||||
// Track the element type for nested generation
|
||||
if (IsComplexType(collectionElementType) && collectionElementType is INamedTypeSymbol elementNamedType)
|
||||
{
|
||||
TrackEnumType(enumType);
|
||||
nestedComplexTypes.Add(elementNamedType);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not a collection - generate properties as usual
|
||||
var properties = type.GetMembers()
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public &&
|
||||
!p.IsIndexer &&
|
||||
!ProtoFileTypeMapper.IsCollectionInternalProperty(p.Name))
|
||||
.ToList();
|
||||
|
||||
// Collect complex types to generate after this message is closed
|
||||
// Use GetElementOrUnderlyingType to extract element type from collections
|
||||
var underlyingType = ProtoFileTypeMapper.GetElementOrUnderlyingType(prop.Type);
|
||||
if (IsComplexType(underlyingType) && underlyingType is INamedTypeSymbol namedType)
|
||||
int fieldNumber = 1;
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
nestedComplexTypes.Add(namedType);
|
||||
}
|
||||
if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type))
|
||||
{
|
||||
_messagesBuilder.AppendLine($" // Skipped: {prop.Name} - unsupported type {prop.Type.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
fieldNumber++;
|
||||
var protoType = ProtoFileTypeMapper.MapType(prop.Type, out var needsImport, out var importPath);
|
||||
if (needsImport && importPath != null)
|
||||
{
|
||||
_requiredImports.Add(importPath);
|
||||
}
|
||||
|
||||
var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name);
|
||||
_messagesBuilder.AppendLine($" {protoType} {fieldName} = {fieldNumber};");
|
||||
|
||||
// Track enums for later generation
|
||||
var enumType = ProtoFileTypeMapper.GetEnumType(prop.Type);
|
||||
if (enumType != null)
|
||||
{
|
||||
TrackEnumType(enumType);
|
||||
}
|
||||
|
||||
// Collect complex types to generate after this message is closed
|
||||
// Use GetElementOrUnderlyingType to extract element type from collections
|
||||
var underlyingType = ProtoFileTypeMapper.GetElementOrUnderlyingType(prop.Type);
|
||||
if (IsComplexType(underlyingType) && underlyingType is INamedTypeSymbol namedType)
|
||||
{
|
||||
nestedComplexTypes.Add(namedType);
|
||||
}
|
||||
|
||||
fieldNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
_messagesBuilder.AppendLine("}");
|
||||
@ -742,7 +764,7 @@ internal class ProtoFileGenerator
|
||||
FullyQualifiedName = type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
|
||||
.Replace("global::", ""),
|
||||
Namespace = type.ContainingNamespace?.ToDisplayString() ?? "",
|
||||
SubscriptionKeyProperty = subscriptionKeyProp,
|
||||
SubscriptionKeyProperty = subscriptionKeyProp!, // Already validated as non-null above
|
||||
SubscriptionKeyInfo = keyPropInfo,
|
||||
Properties = properties
|
||||
});
|
||||
@ -760,7 +782,9 @@ internal class ProtoFileGenerator
|
||||
int fieldNumber = 1;
|
||||
|
||||
foreach (var prop in type.GetMembers().OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public))
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public &&
|
||||
!p.IsIndexer &&
|
||||
!ProtoFileTypeMapper.IsCollectionInternalProperty(p.Name)))
|
||||
{
|
||||
if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type))
|
||||
continue;
|
||||
@ -822,14 +846,16 @@ internal class ProtoFileGenerator
|
||||
|
||||
foreach (var prop in notification.Properties)
|
||||
{
|
||||
var protoType = ProtoFileTypeMapper.MapType(
|
||||
_compilation.GetTypeByMetadataName(prop.FullyQualifiedType) ??
|
||||
GetTypeFromName(prop.FullyQualifiedType),
|
||||
out var needsImport, out var importPath);
|
||||
var typeSymbol = _compilation.GetTypeByMetadataName(prop.FullyQualifiedType) ??
|
||||
GetTypeFromName(prop.FullyQualifiedType);
|
||||
|
||||
if (needsImport && importPath != null)
|
||||
if (typeSymbol != null)
|
||||
{
|
||||
_requiredImports.Add(importPath);
|
||||
ProtoFileTypeMapper.MapType(typeSymbol, out var needsImport, out var importPath);
|
||||
if (needsImport && importPath != null)
|
||||
{
|
||||
_requiredImports.Add(importPath);
|
||||
}
|
||||
}
|
||||
|
||||
var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name);
|
||||
|
||||
@ -20,6 +20,68 @@ internal static class ProtoFileTypeMapper
|
||||
// Note: NullableAnnotation.Annotated is for reference type nullability (List<T>?, string?, etc.)
|
||||
// We don't unwrap these - just use the underlying type. Nullable<T> value types are handled later.
|
||||
|
||||
// Handle Nullable<T> value types (e.g., int?, decimal?, enum?) FIRST
|
||||
if (typeSymbol is INamedTypeSymbol nullableType &&
|
||||
nullableType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T &&
|
||||
nullableType.TypeArguments.Length == 1)
|
||||
{
|
||||
// Unwrap the nullable and map the inner type
|
||||
return MapType(nullableType.TypeArguments[0], out needsImport, out importPath);
|
||||
}
|
||||
|
||||
// Handle collections BEFORE basic type checks (to avoid matching List<Guid> as Guid)
|
||||
if (typeSymbol is INamedTypeSymbol collectionType)
|
||||
{
|
||||
// List, IEnumerable, Array, ICollection etc. (but not Nullable<T>)
|
||||
var collectionTypeName = collectionType.Name;
|
||||
if (collectionType.TypeArguments.Length == 1 &&
|
||||
(collectionTypeName.Contains("List") || collectionTypeName.Contains("Collection") ||
|
||||
collectionTypeName.Contains("Enumerable") || collectionTypeName.Contains("Array") ||
|
||||
collectionTypeName.Contains("Set") || collectionTypeName.Contains("IList") ||
|
||||
collectionTypeName.Contains("ICollection") || collectionTypeName.Contains("IEnumerable")))
|
||||
{
|
||||
var elementType = collectionType.TypeArguments[0];
|
||||
var protoElementType = MapType(elementType, out needsImport, out importPath);
|
||||
return $"repeated {protoElementType}";
|
||||
}
|
||||
|
||||
// Dictionary<K, V>
|
||||
if (collectionType.TypeArguments.Length == 2 &&
|
||||
(collectionTypeName.Contains("Dictionary") || collectionTypeName.Contains("IDictionary")))
|
||||
{
|
||||
var keyType = MapType(collectionType.TypeArguments[0], out var keyNeedsImport, out var keyImportPath);
|
||||
var valueType = MapType(collectionType.TypeArguments[1], out var valueNeedsImport, out var valueImportPath);
|
||||
|
||||
// Set import flags if either key or value needs imports
|
||||
if (keyNeedsImport)
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = keyImportPath;
|
||||
}
|
||||
if (valueNeedsImport)
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = valueImportPath; // Note: This only captures last import, may need improvement
|
||||
}
|
||||
|
||||
return $"map<{keyType}, {valueType}>";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle byte[] array type (check before switch since it's an array)
|
||||
if (typeSymbol is IArrayTypeSymbol arrayType && arrayType.ElementType.SpecialType == SpecialType.System_Byte)
|
||||
{
|
||||
return "bytes";
|
||||
}
|
||||
|
||||
// Handle Stream types -> bytes
|
||||
if (fullTypeName.Contains("System.IO.Stream") ||
|
||||
fullTypeName.Contains("System.IO.MemoryStream") ||
|
||||
fullTypeName.Contains("System.IO.FileStream"))
|
||||
{
|
||||
return "bytes";
|
||||
}
|
||||
|
||||
// Basic types
|
||||
switch (typeName)
|
||||
{
|
||||
@ -49,81 +111,35 @@ internal static class ProtoFileTypeMapper
|
||||
return "double";
|
||||
case "Byte[]":
|
||||
return "bytes";
|
||||
}
|
||||
|
||||
// Special types that need imports
|
||||
if (fullTypeName.Contains("System.DateTime"))
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = "google/protobuf/timestamp.proto";
|
||||
return "google.protobuf.Timestamp";
|
||||
}
|
||||
|
||||
if (fullTypeName.Contains("System.TimeSpan"))
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = "google/protobuf/duration.proto";
|
||||
return "google.protobuf.Duration";
|
||||
}
|
||||
|
||||
if (fullTypeName.Contains("System.Guid"))
|
||||
{
|
||||
// Guid serialized as string
|
||||
return "string";
|
||||
}
|
||||
|
||||
if (fullTypeName.Contains("System.Decimal") || typeName == "Decimal" || fullTypeName == "decimal")
|
||||
{
|
||||
// Decimal serialized as string (no native decimal in proto)
|
||||
return "string";
|
||||
}
|
||||
|
||||
// Handle Nullable<T> value types (e.g., int?, decimal?, enum?)
|
||||
if (typeSymbol is INamedTypeSymbol nullableType &&
|
||||
nullableType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T &&
|
||||
nullableType.TypeArguments.Length == 1)
|
||||
{
|
||||
// Unwrap the nullable and map the inner type
|
||||
return MapType(nullableType.TypeArguments[0], out needsImport, out importPath);
|
||||
}
|
||||
|
||||
// Collections
|
||||
if (typeSymbol is INamedTypeSymbol collectionType)
|
||||
{
|
||||
// List, IEnumerable, Array, ICollection etc. (but not Nullable<T>)
|
||||
var typeName2 = collectionType.Name;
|
||||
if (collectionType.TypeArguments.Length == 1 &&
|
||||
(typeName2.Contains("List") || typeName2.Contains("Collection") ||
|
||||
typeName2.Contains("Enumerable") || typeName2.Contains("Array") ||
|
||||
typeName2.Contains("Set") || typeName2.Contains("IList") ||
|
||||
typeName2.Contains("ICollection") || typeName2.Contains("IEnumerable")))
|
||||
{
|
||||
var elementType = collectionType.TypeArguments[0];
|
||||
var protoElementType = MapType(elementType, out needsImport, out importPath);
|
||||
return $"repeated {protoElementType}";
|
||||
}
|
||||
|
||||
// Dictionary<K, V>
|
||||
if (collectionType.TypeArguments.Length == 2 &&
|
||||
(typeName.Contains("Dictionary") || typeName.Contains("IDictionary")))
|
||||
{
|
||||
var keyType = MapType(collectionType.TypeArguments[0], out var keyNeedsImport, out var keyImportPath);
|
||||
var valueType = MapType(collectionType.TypeArguments[1], out var valueNeedsImport, out var valueImportPath);
|
||||
|
||||
// Set import flags if either key or value needs imports
|
||||
if (keyNeedsImport)
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = keyImportPath;
|
||||
}
|
||||
if (valueNeedsImport)
|
||||
{
|
||||
needsImport = true;
|
||||
importPath = valueImportPath; // Note: This only captures last import, may need improvement
|
||||
}
|
||||
|
||||
return $"map<{keyType}, {valueType}>";
|
||||
}
|
||||
case "Stream":
|
||||
case "MemoryStream":
|
||||
case "FileStream":
|
||||
return "bytes";
|
||||
case "Guid":
|
||||
// Guid serialized as string
|
||||
return "string";
|
||||
case "Decimal":
|
||||
// Decimal serialized as string (no native decimal in proto)
|
||||
return "string";
|
||||
case "DateTime":
|
||||
case "DateTimeOffset":
|
||||
needsImport = true;
|
||||
importPath = "google/protobuf/timestamp.proto";
|
||||
return "google.protobuf.Timestamp";
|
||||
case "DateOnly":
|
||||
// DateOnly serialized as string (YYYY-MM-DD format)
|
||||
return "string";
|
||||
case "TimeOnly":
|
||||
// TimeOnly serialized as string (HH:mm:ss format)
|
||||
return "string";
|
||||
case "TimeSpan":
|
||||
needsImport = true;
|
||||
importPath = "google/protobuf/duration.proto";
|
||||
return "google.protobuf.Duration";
|
||||
case "JsonElement":
|
||||
needsImport = true;
|
||||
importPath = "google/protobuf/struct.proto";
|
||||
return "google.protobuf.Struct";
|
||||
}
|
||||
|
||||
// Enums
|
||||
@ -186,8 +202,8 @@ internal static class ProtoFileTypeMapper
|
||||
var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
|
||||
// Skip these types - they should trigger a warning/error
|
||||
if (fullTypeName.Contains("System.IO.Stream") ||
|
||||
fullTypeName.Contains("System.Threading.CancellationToken") ||
|
||||
// Note: Stream types are now supported (mapped to bytes)
|
||||
if (fullTypeName.Contains("System.Threading.CancellationToken") ||
|
||||
fullTypeName.Contains("System.Threading.Tasks.Task") ||
|
||||
fullTypeName.Contains("System.Collections.Generic.IAsyncEnumerable") ||
|
||||
fullTypeName.Contains("System.Func") ||
|
||||
@ -200,6 +216,31 @@ internal static class ProtoFileTypeMapper
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a type is a Stream or byte array type (for special ByteString handling)
|
||||
/// </summary>
|
||||
public static bool IsBinaryType(ITypeSymbol typeSymbol)
|
||||
{
|
||||
var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
|
||||
// Check for byte[]
|
||||
if (typeSymbol is IArrayTypeSymbol arrayType && arrayType.ElementType.SpecialType == SpecialType.System_Byte)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for Stream types
|
||||
if (fullTypeName.Contains("System.IO.Stream") ||
|
||||
fullTypeName.Contains("System.IO.MemoryStream") ||
|
||||
fullTypeName.Contains("System.IO.FileStream"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var typeName = typeSymbol.Name;
|
||||
return typeName == "Stream" || typeName == "MemoryStream" || typeName == "FileStream";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the element type from a collection type, or returns the type itself if not a collection.
|
||||
/// Also unwraps Nullable types.
|
||||
@ -251,4 +292,97 @@ internal static class ProtoFileTypeMapper
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a type is a collection by checking if it implements IList{T}, ICollection{T}, or IEnumerable{T}
|
||||
/// This handles types like NpgsqlPolygon that implement IList{NpgsqlPoint} but aren't named "List"
|
||||
/// </summary>
|
||||
public static bool IsCollectionTypeByInterface(ITypeSymbol typeSymbol)
|
||||
{
|
||||
if (typeSymbol is not INamedTypeSymbol namedType)
|
||||
return false;
|
||||
|
||||
// Skip string (implements IEnumerable<char>)
|
||||
if (namedType.SpecialType == SpecialType.System_String)
|
||||
return false;
|
||||
|
||||
// Check all interfaces for IList<T>, ICollection<T>, or IEnumerable<T>
|
||||
foreach (var iface in namedType.AllInterfaces)
|
||||
{
|
||||
if (iface.IsGenericType && iface.TypeArguments.Length == 1)
|
||||
{
|
||||
var ifaceName = iface.OriginalDefinition.ToDisplayString();
|
||||
if (ifaceName == "System.Collections.Generic.IList<T>" ||
|
||||
ifaceName == "System.Collections.Generic.ICollection<T>" ||
|
||||
ifaceName == "System.Collections.Generic.IEnumerable<T>" ||
|
||||
ifaceName == "System.Collections.Generic.IReadOnlyList<T>" ||
|
||||
ifaceName == "System.Collections.Generic.IReadOnlyCollection<T>")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the element type from a collection that implements IList{T}, ICollection{T}, or IEnumerable{T}
|
||||
/// Returns null if the type is not a collection
|
||||
/// </summary>
|
||||
public static ITypeSymbol? GetCollectionElementTypeByInterface(ITypeSymbol typeSymbol)
|
||||
{
|
||||
if (typeSymbol is not INamedTypeSymbol namedType)
|
||||
return null;
|
||||
|
||||
// Skip string
|
||||
if (namedType.SpecialType == SpecialType.System_String)
|
||||
return null;
|
||||
|
||||
// Prefer IList<T> over ICollection<T> over IEnumerable<T>
|
||||
ITypeSymbol? elementType = null;
|
||||
int priority = 0;
|
||||
|
||||
foreach (var iface in namedType.AllInterfaces)
|
||||
{
|
||||
if (iface.IsGenericType && iface.TypeArguments.Length == 1)
|
||||
{
|
||||
var ifaceName = iface.OriginalDefinition.ToDisplayString();
|
||||
int currentPriority = 0;
|
||||
|
||||
if (ifaceName == "System.Collections.Generic.IList<T>" ||
|
||||
ifaceName == "System.Collections.Generic.IReadOnlyList<T>")
|
||||
currentPriority = 3;
|
||||
else if (ifaceName == "System.Collections.Generic.ICollection<T>" ||
|
||||
ifaceName == "System.Collections.Generic.IReadOnlyCollection<T>")
|
||||
currentPriority = 2;
|
||||
else if (ifaceName == "System.Collections.Generic.IEnumerable<T>")
|
||||
currentPriority = 1;
|
||||
|
||||
if (currentPriority > priority)
|
||||
{
|
||||
priority = currentPriority;
|
||||
elementType = iface.TypeArguments[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return elementType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collection-internal properties that should be skipped when generating proto messages
|
||||
/// </summary>
|
||||
private static readonly System.Collections.Generic.HashSet<string> CollectionInternalProperties = new()
|
||||
{
|
||||
"Count", "Capacity", "IsReadOnly", "IsSynchronized", "SyncRoot", "Keys", "Values"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a property name is a collection-internal property that should be skipped
|
||||
/// </summary>
|
||||
public static bool IsCollectionInternalProperty(string propertyName)
|
||||
{
|
||||
return CollectionInternalProperties.Contains(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,15 +2,5 @@
|
||||
<PropertyGroup>
|
||||
<!-- Marker to indicate Svrnty.CQRS.Grpc.Generators is referenced -->
|
||||
<SvrntyCqrsGrpcGeneratorsVersion>$(SvrntyCqrsGrpcGeneratorsVersion)</SvrntyCqrsGrpcGeneratorsVersion>
|
||||
|
||||
<!-- Path resolution for both NuGet package and project reference -->
|
||||
<_SvrntyCqrsGrpcGeneratorsPath Condition="Exists('$(MSBuildThisFileDirectory)..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)..\analyzers\dotnet\cs\Svrnty.CQRS.Grpc.Generators.dll</_SvrntyCqrsGrpcGeneratorsPath>
|
||||
<_SvrntyCqrsGrpcGeneratorsPath Condition="'$(_SvrntyCqrsGrpcGeneratorsPath)' == '' AND Exists('$(MSBuildThisFileDirectory)..\bin\Debug\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)..\bin\Debug\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll</_SvrntyCqrsGrpcGeneratorsPath>
|
||||
<_SvrntyCqrsGrpcGeneratorsPath Condition="'$(_SvrntyCqrsGrpcGeneratorsPath)' == '' AND Exists('$(MSBuildThisFileDirectory)..\bin\Release\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll')">$(MSBuildThisFileDirectory)..\bin\Release\netstandard2.0\Svrnty.CQRS.Grpc.Generators.dll</_SvrntyCqrsGrpcGeneratorsPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Explicitly add the generator to the Analyzer ItemGroup -->
|
||||
<ItemGroup Condition="'$(_SvrntyCqrsGrpcGeneratorsPath)' != ''">
|
||||
<Analyzer Include="$(_SvrntyCqrsGrpcGeneratorsPath)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -1,111 +1,7 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "Svrnty.Sample.Grpc";
|
||||
option csharp_namespace = "Generated.Grpc";
|
||||
|
||||
package cqrs;
|
||||
|
||||
// Command service for CQRS operations
|
||||
service CommandService {
|
||||
// AddUserCommand operation
|
||||
rpc AddUser (AddUserCommandRequest) returns (AddUserCommandResponse);
|
||||
|
||||
// RemoveUserCommand operation
|
||||
rpc RemoveUser (RemoveUserCommandRequest) returns (RemoveUserCommandResponse);
|
||||
|
||||
}
|
||||
|
||||
// Query service for CQRS operations
|
||||
service QueryService {
|
||||
// FetchUserQuery operation
|
||||
rpc FetchUser (FetchUserQueryRequest) returns (FetchUserQueryResponse);
|
||||
|
||||
}
|
||||
|
||||
// DynamicQuery service for CQRS operations
|
||||
service DynamicQueryService {
|
||||
// Dynamic query for User
|
||||
rpc QueryUsers (DynamicQueryUsersRequest) returns (DynamicQueryUsersResponse);
|
||||
|
||||
}
|
||||
|
||||
// Request message for AddUserCommand
|
||||
message AddUserCommandRequest {
|
||||
string name = 1;
|
||||
string email = 2;
|
||||
int32 age = 3;
|
||||
}
|
||||
|
||||
// Response message for AddUserCommand
|
||||
message AddUserCommandResponse {
|
||||
int32 result = 1;
|
||||
}
|
||||
|
||||
// Request message for RemoveUserCommand
|
||||
message RemoveUserCommandRequest {
|
||||
int32 user_id = 1;
|
||||
}
|
||||
|
||||
// Response message for RemoveUserCommand
|
||||
message RemoveUserCommandResponse {
|
||||
}
|
||||
|
||||
// Request message for FetchUserQuery
|
||||
message FetchUserQueryRequest {
|
||||
int32 user_id = 1;
|
||||
}
|
||||
|
||||
// Response message for FetchUserQuery
|
||||
message FetchUserQueryResponse {
|
||||
User result = 1;
|
||||
}
|
||||
|
||||
// User entity
|
||||
message User {
|
||||
int32 id = 1;
|
||||
string name = 2;
|
||||
string email = 3;
|
||||
}
|
||||
|
||||
// Dynamic query filter with AND/OR support
|
||||
message DynamicQueryFilter {
|
||||
string path = 1;
|
||||
int32 type = 2; // PoweredSoft.DynamicQuery.Core.FilterType
|
||||
string value = 3;
|
||||
repeated DynamicQueryFilter and = 4;
|
||||
repeated DynamicQueryFilter or = 5;
|
||||
}
|
||||
|
||||
// Dynamic query sort
|
||||
message DynamicQuerySort {
|
||||
string path = 1;
|
||||
bool ascending = 2;
|
||||
}
|
||||
|
||||
// Dynamic query group
|
||||
message DynamicQueryGroup {
|
||||
string path = 1;
|
||||
}
|
||||
|
||||
// Dynamic query aggregate
|
||||
message DynamicQueryAggregate {
|
||||
string path = 1;
|
||||
int32 type = 2; // PoweredSoft.DynamicQuery.Core.AggregateType
|
||||
}
|
||||
|
||||
// Dynamic query request for User
|
||||
message DynamicQueryUsersRequest {
|
||||
int32 page = 1;
|
||||
int32 page_size = 2;
|
||||
repeated DynamicQueryFilter filters = 3;
|
||||
repeated DynamicQuerySort sorts = 4;
|
||||
repeated DynamicQueryGroup groups = 5;
|
||||
repeated DynamicQueryAggregate aggregates = 6;
|
||||
}
|
||||
|
||||
// Dynamic query response for User
|
||||
message DynamicQueryUsersResponse {
|
||||
repeated User data = 1;
|
||||
int64 total_records = 2;
|
||||
int32 number_of_pages = 3;
|
||||
}
|
||||
|
||||
// Placeholder proto file - will be regenerated on next build
|
||||
|
||||
Loading…
Reference in New Issue
Block a user