Fix MinimalApi to resolve authorization services per-request
- Resolve ICommandAuthorizationService and IQueryAuthorizationService from request-scoped serviceProvider - Allows Scoped authorization services that depend on DbContext - Updated both MinimalApi and DynamicQuery.MinimalApi Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bd43bc9bde
commit
227be70f95
@ -23,7 +23,6 @@ public static class EndpointRouteBuilderExtensions
|
||||
public static IEndpointRouteBuilder MapSvrntyDynamicQueries(this IEndpointRouteBuilder endpoints, string routePrefix = "api/query")
|
||||
{
|
||||
var queryDiscovery = endpoints.ServiceProvider.GetRequiredService<IQueryDiscovery>();
|
||||
var authorizationService = endpoints.ServiceProvider.GetService<IQueryAuthorizationService>();
|
||||
|
||||
foreach (var queryMeta in queryDiscovery.GetQueries())
|
||||
{
|
||||
@ -43,14 +42,14 @@ public static class EndpointRouteBuilderExtensions
|
||||
if (dynamicQueryMeta.ParamsType == null)
|
||||
{
|
||||
// DynamicQuery<TSource, TDestination>
|
||||
MapDynamicQueryPost(endpoints, route, dynamicQueryMeta, authorizationService);
|
||||
MapDynamicQueryGet(endpoints, route, dynamicQueryMeta, authorizationService);
|
||||
MapDynamicQueryPost(endpoints, route, dynamicQueryMeta);
|
||||
MapDynamicQueryGet(endpoints, route, dynamicQueryMeta);
|
||||
}
|
||||
else
|
||||
{
|
||||
// DynamicQuery<TSource, TDestination, TParams>
|
||||
MapDynamicQueryWithParamsPost(endpoints, route, dynamicQueryMeta, authorizationService);
|
||||
MapDynamicQueryWithParamsGet(endpoints, route, dynamicQueryMeta, authorizationService);
|
||||
MapDynamicQueryWithParamsPost(endpoints, route, dynamicQueryMeta);
|
||||
MapDynamicQueryWithParamsGet(endpoints, route, dynamicQueryMeta);
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,8 +59,7 @@ public static class EndpointRouteBuilderExtensions
|
||||
private static void MapDynamicQueryPost(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
DynamicQueryMeta dynamicQueryMeta,
|
||||
IQueryAuthorizationService? authorizationService)
|
||||
DynamicQueryMeta dynamicQueryMeta)
|
||||
{
|
||||
var sourceType = dynamicQueryMeta.SourceType;
|
||||
var destinationType = dynamicQueryMeta.DestinationType;
|
||||
@ -75,7 +73,7 @@ public static class EndpointRouteBuilderExtensions
|
||||
.GetMethod(nameof(MapDynamicQueryPostTyped), BindingFlags.NonPublic | BindingFlags.Static)!
|
||||
.MakeGenericMethod(sourceType, destinationType);
|
||||
|
||||
var endpoint = (RouteHandlerBuilder)mapPostMethod.Invoke(null, [endpoints, route, queryType, handlerType, authorizationService])!;
|
||||
var endpoint = (RouteHandlerBuilder)mapPostMethod.Invoke(null, [endpoints, route, queryType, handlerType])!;
|
||||
|
||||
endpoint
|
||||
.WithName($"DynamicQuery_{dynamicQueryMeta.LowerCamelCaseName}_Post")
|
||||
@ -91,8 +89,7 @@ public static class EndpointRouteBuilderExtensions
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
Type queryType,
|
||||
Type handlerType,
|
||||
IQueryAuthorizationService? authorizationService)
|
||||
Type handlerType)
|
||||
where TSource : class
|
||||
where TDestination : class
|
||||
{
|
||||
@ -102,6 +99,7 @@ public static class EndpointRouteBuilderExtensions
|
||||
IServiceProvider serviceProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var authorizationService = serviceProvider.GetService<IQueryAuthorizationService>();
|
||||
if (authorizationService != null)
|
||||
{
|
||||
var authorizationResult = await authorizationService.IsAllowedAsync(queryType, cancellationToken);
|
||||
@ -129,8 +127,7 @@ public static class EndpointRouteBuilderExtensions
|
||||
private static void MapDynamicQueryGet(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
DynamicQueryMeta dynamicQueryMeta,
|
||||
IQueryAuthorizationService? authorizationService)
|
||||
DynamicQueryMeta dynamicQueryMeta)
|
||||
{
|
||||
var sourceType = dynamicQueryMeta.SourceType;
|
||||
var destinationType = dynamicQueryMeta.DestinationType;
|
||||
@ -141,6 +138,7 @@ public static class EndpointRouteBuilderExtensions
|
||||
|
||||
endpoints.MapGet(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var authorizationService = serviceProvider.GetService<IQueryAuthorizationService>();
|
||||
if (authorizationService != null)
|
||||
{
|
||||
var authorizationResult = await authorizationService.IsAllowedAsync(queryType, cancellationToken);
|
||||
@ -199,8 +197,7 @@ public static class EndpointRouteBuilderExtensions
|
||||
private static void MapDynamicQueryWithParamsPost(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
DynamicQueryMeta dynamicQueryMeta,
|
||||
IQueryAuthorizationService? authorizationService)
|
||||
DynamicQueryMeta dynamicQueryMeta)
|
||||
{
|
||||
var sourceType = dynamicQueryMeta.SourceType;
|
||||
var destinationType = dynamicQueryMeta.DestinationType;
|
||||
@ -214,7 +211,7 @@ public static class EndpointRouteBuilderExtensions
|
||||
.GetMethod(nameof(MapDynamicQueryWithParamsPostTyped), BindingFlags.NonPublic | BindingFlags.Static)!
|
||||
.MakeGenericMethod(sourceType, destinationType, paramsType);
|
||||
|
||||
var endpoint = (RouteHandlerBuilder)mapPostMethod.Invoke(null, [endpoints, route, queryType, handlerType, authorizationService])!;
|
||||
var endpoint = (RouteHandlerBuilder)mapPostMethod.Invoke(null, [endpoints, route, queryType, handlerType])!;
|
||||
|
||||
endpoint
|
||||
.WithName($"DynamicQuery_{dynamicQueryMeta.LowerCamelCaseName}_WithParams_Post")
|
||||
@ -230,8 +227,7 @@ public static class EndpointRouteBuilderExtensions
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
Type queryType,
|
||||
Type handlerType,
|
||||
IQueryAuthorizationService? authorizationService)
|
||||
Type handlerType)
|
||||
where TSource : class
|
||||
where TDestination : class
|
||||
where TParams : class
|
||||
@ -242,6 +238,7 @@ public static class EndpointRouteBuilderExtensions
|
||||
IServiceProvider serviceProvider,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var authorizationService = serviceProvider.GetService<IQueryAuthorizationService>();
|
||||
if (authorizationService != null)
|
||||
{
|
||||
var authorizationResult = await authorizationService.IsAllowedAsync(queryType, cancellationToken);
|
||||
@ -269,8 +266,7 @@ public static class EndpointRouteBuilderExtensions
|
||||
private static void MapDynamicQueryWithParamsGet(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
DynamicQueryMeta dynamicQueryMeta,
|
||||
IQueryAuthorizationService? authorizationService)
|
||||
DynamicQueryMeta dynamicQueryMeta)
|
||||
{
|
||||
var sourceType = dynamicQueryMeta.SourceType;
|
||||
var destinationType = dynamicQueryMeta.DestinationType;
|
||||
@ -282,6 +278,7 @@ public static class EndpointRouteBuilderExtensions
|
||||
|
||||
endpoints.MapGet(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var authorizationService = serviceProvider.GetService<IQueryAuthorizationService>();
|
||||
if (authorizationService != null)
|
||||
{
|
||||
var authorizationResult = await authorizationService.IsAllowedAsync(queryType, cancellationToken);
|
||||
|
||||
@ -35,7 +35,11 @@ public abstract class DynamicQueryHandlerBase<TSource, TDestination>
|
||||
protected virtual Task<IQueryable<TSource>> GetQueryableAsync(IDynamicQuery query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_queryableProviders.Any())
|
||||
return _queryableProviders.ElementAt(0).GetQueryableAsync(query, cancellationToken);
|
||||
{
|
||||
// Use Last() to prefer closed generic registrations (overrides) over open generic (default)
|
||||
// Registration order: open generic first, closed generic (override) last
|
||||
return _queryableProviders.Last().GetQueryableAsync(query, cancellationToken);
|
||||
}
|
||||
|
||||
throw new Exception($"You must provide a QueryableProvider<TSource> for {typeof(TSource).Name}");
|
||||
}
|
||||
|
||||
@ -329,6 +329,60 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
var commandTypeFullyQualified = commandType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
var resultTypeFullyQualified = resultType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
commandInfo.HandlerInterfaceName = $"ICommandHandler<{commandTypeFullyQualified}, {resultTypeFullyQualified}>";
|
||||
|
||||
// Check if result type is primitive
|
||||
var resultTypeString = resultType.ToDisplayString();
|
||||
commandInfo.IsResultPrimitiveType = IsPrimitiveType(resultTypeString);
|
||||
|
||||
// Extract result type properties if it's a complex type
|
||||
if (!commandInfo.IsResultPrimitiveType)
|
||||
{
|
||||
var resultProperties = resultType.GetMembers().OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic)
|
||||
.ToList();
|
||||
|
||||
foreach (var property in resultProperties)
|
||||
{
|
||||
var propertyType = property.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
var isList = IsListOrCollection(property.Type);
|
||||
var isComplexType = IsUserDefinedComplexType(property.Type);
|
||||
|
||||
var propInfo = new PropertyInfo
|
||||
{
|
||||
Name = property.Name,
|
||||
Type = property.Type.ToDisplayString(),
|
||||
FullyQualifiedType = propertyType,
|
||||
ProtoType = string.Empty, // Not needed for result mapping
|
||||
FieldNumber = 0, // Not needed for result mapping
|
||||
IsList = isList,
|
||||
IsComplexType = isComplexType,
|
||||
IsNullable = IsNullableType(property.Type),
|
||||
IsEnum = IsEnumType(property.Type),
|
||||
IsDecimal = IsDecimalType(property.Type),
|
||||
IsDateTime = IsDateTimeType(property.Type),
|
||||
IsDateTimeOffset = IsDateTimeOffsetType(property.Type),
|
||||
IsGuid = IsGuidType(property.Type),
|
||||
IsJsonElement = IsJsonElementType(property.Type),
|
||||
IsValueTypeCollection = IsValueTypeCollection(property.Type),
|
||||
IsBinaryType = IsBinaryType(property.Type),
|
||||
IsStream = IsStreamType(property.Type),
|
||||
};
|
||||
|
||||
// If it's a list, extract element type info
|
||||
if (isList)
|
||||
{
|
||||
var elementType = GetListElementType(property.Type);
|
||||
if (elementType != null)
|
||||
{
|
||||
propInfo.ElementType = elementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
|
||||
propInfo.IsElementComplexType = IsUserDefinedComplexType(elementType);
|
||||
propInfo.IsElementGuid = IsGuidType(elementType);
|
||||
}
|
||||
}
|
||||
|
||||
commandInfo.ResultProperties.Add(propInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -360,9 +414,11 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
IsEnum = IsEnumType(property.Type),
|
||||
IsDecimal = IsDecimalType(property.Type),
|
||||
IsDateTime = IsDateTimeType(property.Type),
|
||||
IsDateTimeOffset = IsDateTimeOffsetType(property.Type),
|
||||
IsGuid = IsGuidType(property.Type),
|
||||
IsJsonElement = IsJsonElementType(property.Type),
|
||||
IsList = IsListOrCollection(property.Type),
|
||||
IsValueTypeCollection = IsValueTypeCollection(property.Type),
|
||||
IsBinaryType = IsBinaryType(property.Type),
|
||||
IsStream = IsStreamType(property.Type),
|
||||
};
|
||||
@ -466,6 +522,12 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
return unwrapped.SpecialType == SpecialType.System_DateTime;
|
||||
}
|
||||
|
||||
private static bool IsDateTimeOffsetType(ITypeSymbol type)
|
||||
{
|
||||
var unwrapped = UnwrapNullableType(type);
|
||||
return unwrapped.ToDisplayString() == "System.DateTimeOffset";
|
||||
}
|
||||
|
||||
private static bool IsEnumType(ITypeSymbol type)
|
||||
{
|
||||
var unwrapped = UnwrapNullableType(type);
|
||||
@ -504,28 +566,119 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
|
||||
private static bool IsListOrCollection(ITypeSymbol type)
|
||||
{
|
||||
if (type is IArrayTypeSymbol)
|
||||
return true;
|
||||
// Unwrap nullable first to handle NpgsqlPolygon?
|
||||
var unwrapped = UnwrapNullableType(type);
|
||||
|
||||
if (type is INamedTypeSymbol namedType && namedType.IsGenericType)
|
||||
if (unwrapped is IArrayTypeSymbol arrayType)
|
||||
{
|
||||
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<");
|
||||
// byte[] is binary data, not a collection
|
||||
if (arrayType.ElementType.SpecialType == SpecialType.System_Byte)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (unwrapped is INamedTypeSymbol namedType)
|
||||
{
|
||||
// Check if it's a generic collection type
|
||||
if (namedType.IsGenericType)
|
||||
{
|
||||
var typeName = namedType.OriginalDefinition.ToDisplayString();
|
||||
if (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 true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it implements IList<T>, ICollection<T>, or IEnumerable<T> (handles types like NpgsqlPolygon)
|
||||
// Skip string which implements IEnumerable<char>
|
||||
if (namedType.SpecialType == SpecialType.System_String)
|
||||
return false;
|
||||
|
||||
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.IReadOnlyList<T>" ||
|
||||
ifaceName == "System.Collections.Generic.IReadOnlyCollection<T>")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a type is a value type that implements a collection interface (like NpgsqlPolygon)
|
||||
/// These require special construction syntax
|
||||
/// </summary>
|
||||
private static bool IsValueTypeCollection(ITypeSymbol type)
|
||||
{
|
||||
// Unwrap nullable first to handle NpgsqlPolygon?
|
||||
var unwrapped = UnwrapNullableType(type);
|
||||
|
||||
if (unwrapped is not INamedTypeSymbol namedType)
|
||||
return false;
|
||||
|
||||
// Must be a value type (struct)
|
||||
if (!namedType.IsValueType)
|
||||
return false;
|
||||
|
||||
// Must implement a collection interface (pass unwrapped since IsListOrCollection also unwraps)
|
||||
return IsListOrCollection(unwrapped);
|
||||
}
|
||||
|
||||
private static ITypeSymbol? GetListElementType(ITypeSymbol type)
|
||||
{
|
||||
if (type is IArrayTypeSymbol arrayType)
|
||||
// Unwrap nullable first to handle NpgsqlPolygon?
|
||||
var unwrapped = UnwrapNullableType(type);
|
||||
|
||||
if (unwrapped is IArrayTypeSymbol arrayType)
|
||||
return arrayType.ElementType;
|
||||
|
||||
if (type is INamedTypeSymbol namedType && namedType.IsGenericType && namedType.TypeArguments.Length > 0)
|
||||
if (unwrapped is INamedTypeSymbol namedType)
|
||||
{
|
||||
return namedType.TypeArguments[0];
|
||||
// First try generic type arguments
|
||||
if (namedType.IsGenericType && namedType.TypeArguments.Length > 0)
|
||||
{
|
||||
return namedType.TypeArguments[0];
|
||||
}
|
||||
|
||||
// Fall back to checking interfaces (for types like NpgsqlPolygon that implement IList<NpgsqlPoint>)
|
||||
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.IReadOnlyList<T>")
|
||||
{
|
||||
return iface.TypeArguments[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try ICollection<T> or IEnumerable<T> as fallback
|
||||
foreach (var iface in namedType.AllInterfaces)
|
||||
{
|
||||
if (iface.IsGenericType && iface.TypeArguments.Length == 1)
|
||||
{
|
||||
var ifaceName = iface.OriginalDefinition.ToDisplayString();
|
||||
if (ifaceName == "System.Collections.Generic.ICollection<T>" ||
|
||||
ifaceName == "System.Collections.Generic.IReadOnlyCollection<T>" ||
|
||||
ifaceName == "System.Collections.Generic.IEnumerable<T>")
|
||||
{
|
||||
return iface.TypeArguments[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -582,6 +735,8 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
ProtoType = string.Empty,
|
||||
FieldNumber = 0,
|
||||
IsComplexType = IsUserDefinedComplexType(property.Type),
|
||||
IsList = IsListOrCollection(property.Type),
|
||||
IsValueTypeCollection = IsValueTypeCollection(property.Type),
|
||||
};
|
||||
|
||||
// Recursively extract nested properties for complex types
|
||||
@ -620,9 +775,11 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
IsEnum = IsEnumType(property.Type),
|
||||
IsDecimal = IsDecimalType(property.Type),
|
||||
IsDateTime = IsDateTimeType(property.Type),
|
||||
IsDateTimeOffset = IsDateTimeOffsetType(property.Type),
|
||||
IsGuid = IsGuidType(property.Type),
|
||||
IsJsonElement = IsJsonElementType(property.Type),
|
||||
IsList = IsListOrCollection(property.Type),
|
||||
IsValueTypeCollection = IsValueTypeCollection(property.Type),
|
||||
IsBinaryType = IsBinaryType(property.Type),
|
||||
IsStream = IsStreamType(property.Type),
|
||||
};
|
||||
@ -695,8 +852,20 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
else if (prop.IsElementGuid)
|
||||
{
|
||||
// List<string> from proto -> List<Guid> in C#
|
||||
if (prop.IsValueTypeCollection)
|
||||
{
|
||||
// Value type collection proto message has Items field
|
||||
var constructorType = prop.FullyQualifiedType.TrimEnd('?');
|
||||
return $"{indent}{prop.Name} = new {constructorType}({source}?.Items?.Select(x => System.Guid.Parse(x)).ToArray() ?? System.Array.Empty<System.Guid>()),";
|
||||
}
|
||||
return $"{indent}{prop.Name} = {source}?.Select(x => System.Guid.Parse(x)).ToList(),";
|
||||
}
|
||||
else if (prop.IsValueTypeCollection)
|
||||
{
|
||||
// Value type collection (like NpgsqlPolygon): proto message has Items field
|
||||
var constructorType = prop.FullyQualifiedType.TrimEnd('?');
|
||||
return $"{indent}{prop.Name} = new {constructorType}({source}?.Items?.Select(x => new {prop.ElementType} {{ X = x.X, Y = x.Y }}).ToArray() ?? System.Array.Empty<{prop.ElementType ?? "object"}>()),";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Primitive list: just ToList()
|
||||
@ -736,6 +905,19 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
}
|
||||
}
|
||||
|
||||
// Handle DateTimeOffset (proto Timestamp -> C# DateTimeOffset)
|
||||
if (prop.IsDateTimeOffset)
|
||||
{
|
||||
if (prop.IsNullable)
|
||||
{
|
||||
return $"{indent}{prop.Name} = {source} == null ? (System.DateTimeOffset?)null : {source}.ToDateTimeOffset(),";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $"{indent}{prop.Name} = {source}.ToDateTimeOffset(),";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Guid (proto string -> C# Guid)
|
||||
if (prop.IsGuid)
|
||||
{
|
||||
@ -785,7 +967,9 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
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}");
|
||||
// For value type collections, the proto message has an Items field containing the repeated elements
|
||||
var itemsSource = prop.IsValueTypeCollection ? $"{source}?.Items" : source;
|
||||
sb.AppendLine($"{indent}{prop.Name} = {itemsSource}?.Select(x => new {prop.ElementType}");
|
||||
sb.AppendLine($"{indent}{{");
|
||||
|
||||
foreach (var nestedProp in prop.ElementNestedProperties!)
|
||||
@ -798,7 +982,17 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
sb.AppendLine(nestedAssignment);
|
||||
}
|
||||
|
||||
sb.Append($"{indent}}}).ToList(),");
|
||||
if (prop.IsValueTypeCollection)
|
||||
{
|
||||
// Value type collection: wrap in constructor with array
|
||||
// Use non-nullable type for constructor (remove trailing ? if present)
|
||||
var constructorType = prop.FullyQualifiedType.TrimEnd('?');
|
||||
sb.Append($"{indent}}}).ToArray() is {{ }} arr ? new {constructorType}(arr) : default,");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append($"{indent}}}).ToList(),");
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
@ -878,6 +1072,12 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
{
|
||||
return GenerateComplexListMapping(prop, source, indent);
|
||||
}
|
||||
if (prop.IsValueTypeCollection)
|
||||
{
|
||||
// Value type collection (like NpgsqlPolygon): use constructor with array
|
||||
var constructorType = prop.FullyQualifiedType.TrimEnd('?');
|
||||
return $"{indent}{prop.Name} = new {constructorType}({source}?.ToArray() ?? System.Array.Empty<{prop.ElementType ?? "object"}>()),";
|
||||
}
|
||||
return $"{indent}{prop.Name} = {source}?.ToList(),";
|
||||
}
|
||||
|
||||
@ -901,7 +1101,22 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
// Handle lists
|
||||
if (prop.IsList)
|
||||
{
|
||||
if (prop.IsElementComplexType)
|
||||
// Value type collections (like NpgsqlPolygon) map to a wrapper message with Items field
|
||||
if (prop.IsValueTypeCollection)
|
||||
{
|
||||
var protoWrapperType = prop.Type.Split('.').Last().Replace("?", "");
|
||||
var protoElementType = prop.ElementType?.Split('.').Last() ?? "object";
|
||||
if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any())
|
||||
{
|
||||
return GenerateResultValueTypeCollectionWithComplexElements(prop, source, indent, protoWrapperType, protoElementType);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Simple element type
|
||||
return $"{indent}{prop.Name} = new {protoWrapperType} {{ Items = {{ {source}.Select(x => new {protoElementType} {{ X = x.X, Y = x.Y }}) }} }},";
|
||||
}
|
||||
}
|
||||
else if (prop.IsElementComplexType)
|
||||
{
|
||||
// Complex list: map each element to proto type
|
||||
return GenerateResultComplexListMapping(prop, source, indent);
|
||||
@ -1040,6 +1255,27 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateResultValueTypeCollectionWithComplexElements(PropertyInfo prop, string source, string indent, string protoWrapperType, string protoElementType)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{indent}{prop.Name} = new {protoWrapperType} {{ Items = {{");
|
||||
sb.AppendLine($"{indent} {source}.Select(x => new {protoElementType}");
|
||||
sb.AppendLine($"{indent} {{");
|
||||
|
||||
if (prop.ElementNestedProperties != null)
|
||||
{
|
||||
foreach (var nestedProp in prop.ElementNestedProperties)
|
||||
{
|
||||
var nestedAssignment = GenerateResultNestedPropertyMapping(nestedProp, "x", indent + " ");
|
||||
sb.AppendLine(nestedAssignment);
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine($"{indent} }})");
|
||||
sb.Append($"{indent}}} }},");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateResultComplexObjectMapping(PropertyInfo prop, string source, string indent)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
@ -1147,7 +1383,22 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
// Handle lists
|
||||
if (prop.IsList)
|
||||
{
|
||||
if (prop.IsElementGuid)
|
||||
// Value type collections (like NpgsqlPolygon) map to a wrapper message with Items field
|
||||
if (prop.IsValueTypeCollection)
|
||||
{
|
||||
var protoWrapperType = prop.Type.Split('.').Last().Replace("?", "");
|
||||
var protoElementType = prop.ElementType?.Split('.').Last() ?? "object";
|
||||
if (prop.IsElementComplexType && prop.ElementNestedProperties != null && prop.ElementNestedProperties.Any())
|
||||
{
|
||||
return GenerateResultValueTypeCollectionWithComplexElements(prop, source, indent, protoWrapperType, protoElementType);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Simple element type (like NpgsqlPoint)
|
||||
return $"{indent}{prop.Name} = new {protoWrapperType} {{ Items = {{ {source}.Select(x => new {protoElementType} {{ X = x.X, Y = x.Y }}) }} }},";
|
||||
}
|
||||
}
|
||||
else if (prop.IsElementGuid)
|
||||
{
|
||||
return $"{indent}{prop.Name} = {{ {source}?.Select(x => x.ToString()) ?? Enumerable.Empty<string>() }},";
|
||||
}
|
||||
@ -1247,9 +1498,11 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
IsEnum = IsEnumType(property.Type),
|
||||
IsDecimal = IsDecimalType(property.Type),
|
||||
IsDateTime = IsDateTimeType(property.Type),
|
||||
IsDateTimeOffset = IsDateTimeOffsetType(property.Type),
|
||||
IsGuid = IsGuidType(property.Type),
|
||||
IsJsonElement = IsJsonElementType(property.Type),
|
||||
IsList = IsListOrCollection(property.Type),
|
||||
IsValueTypeCollection = IsValueTypeCollection(property.Type),
|
||||
IsComplexType = IsUserDefinedComplexType(property.Type),
|
||||
IsBinaryType = IsBinaryType(property.Type),
|
||||
IsStream = IsStreamType(property.Type),
|
||||
@ -1310,9 +1563,11 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
IsEnum = IsEnumType(property.Type),
|
||||
IsDecimal = IsDecimalType(property.Type),
|
||||
IsDateTime = IsDateTimeType(property.Type),
|
||||
IsDateTimeOffset = IsDateTimeOffsetType(property.Type),
|
||||
IsGuid = IsGuidType(property.Type),
|
||||
IsJsonElement = IsJsonElementType(property.Type),
|
||||
IsList = IsListOrCollection(property.Type),
|
||||
IsValueTypeCollection = IsValueTypeCollection(property.Type),
|
||||
IsComplexType = IsUserDefinedComplexType(property.Type),
|
||||
IsBinaryType = IsBinaryType(property.Type),
|
||||
IsStream = IsStreamType(property.Type),
|
||||
@ -2166,6 +2421,7 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
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 {rootNamespace}.Grpc;");
|
||||
sb.AppendLine("using Svrnty.CQRS.Abstractions;");
|
||||
@ -2911,7 +3167,9 @@ namespace Svrnty.CQRS.Grpc.Generators
|
||||
FieldNumber = fieldNumber++,
|
||||
IsEnum = prop.Type.TypeKind == TypeKind.Enum,
|
||||
IsDecimal = propType.Contains("decimal") || propType.Contains("Decimal"),
|
||||
IsDateTime = propType.Contains("DateTime")
|
||||
IsDateTime = propType.Contains("DateTime"),
|
||||
IsList = IsListOrCollection(prop.Type),
|
||||
IsValueTypeCollection = IsValueTypeCollection(prop.Type),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -49,6 +49,12 @@ namespace Svrnty.CQRS.Grpc.Generators.Helpers
|
||||
isRepeated = false;
|
||||
isOptional = false;
|
||||
|
||||
// Handle byte[] as bytes proto type (NOT repeated uint32)
|
||||
if (csharpType == "System.Byte[]" || csharpType == "byte[]" || csharpType == "Byte[]")
|
||||
{
|
||||
return "bytes";
|
||||
}
|
||||
|
||||
// Handle arrays
|
||||
if (csharpType.EndsWith("[]"))
|
||||
{
|
||||
|
||||
@ -44,11 +44,13 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
|
||||
public bool IsNullable { get; set; }
|
||||
public bool IsDecimal { get; set; }
|
||||
public bool IsDateTime { get; set; }
|
||||
public bool IsDateTimeOffset { 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 bool IsValueTypeCollection { get; set; } // Value types that implement IList<T> (like NpgsqlPolygon)
|
||||
public string? ElementType { get; set; }
|
||||
public bool IsElementComplexType { get; set; }
|
||||
public bool IsElementGuid { get; set; }
|
||||
@ -67,11 +69,13 @@ namespace Svrnty.CQRS.Grpc.Generators.Models
|
||||
IsNullable = false;
|
||||
IsDecimal = false;
|
||||
IsDateTime = false;
|
||||
IsDateTimeOffset = false;
|
||||
IsGuid = false;
|
||||
IsJsonElement = false;
|
||||
IsBinaryType = false;
|
||||
IsStream = false;
|
||||
IsReadOnly = false;
|
||||
IsValueTypeCollection = false;
|
||||
IsElementComplexType = false;
|
||||
IsElementGuid = false;
|
||||
}
|
||||
|
||||
@ -159,7 +159,10 @@ internal static class ProtoFileTypeMapper
|
||||
}
|
||||
|
||||
/// <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).
|
||||
/// This matches protobuf's C# codegen expectations for PascalCase conversion.
|
||||
/// Example: TotalADeduire -> total_a_deduire -> TotalADeduire (in generated C#)
|
||||
/// </summary>
|
||||
public static string ToSnakeCase(string pascalCase)
|
||||
{
|
||||
@ -174,16 +177,8 @@ internal static class ProtoFileTypeMapper
|
||||
var c = pascalCase[i];
|
||||
if (char.IsUpper(c))
|
||||
{
|
||||
// Handle sequences of uppercase letters (e.g., "APIKey" -> "api_key")
|
||||
if (i + 1 < pascalCase.Length && char.IsUpper(pascalCase[i + 1]))
|
||||
{
|
||||
result.Append(char.ToLowerInvariant(c));
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Append('_');
|
||||
result.Append(char.ToLowerInvariant(c));
|
||||
}
|
||||
result.Append('_');
|
||||
result.Append(char.ToLowerInvariant(c));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@ -19,7 +19,6 @@ public static class EndpointRouteBuilderExtensions
|
||||
public static IEndpointRouteBuilder MapSvrntyQueries(this IEndpointRouteBuilder endpoints, string routePrefix = "api/query")
|
||||
{
|
||||
var queryDiscovery = endpoints.ServiceProvider.GetRequiredService<IQueryDiscovery>();
|
||||
var authorizationService = endpoints.ServiceProvider.GetService<IQueryAuthorizationService>();
|
||||
|
||||
foreach (var queryMeta in queryDiscovery.GetQueries())
|
||||
{
|
||||
@ -33,8 +32,8 @@ public static class EndpointRouteBuilderExtensions
|
||||
|
||||
var route = $"{routePrefix}/{queryMeta.LowerCamelCaseName}";
|
||||
|
||||
MapQueryPost(endpoints, route, queryMeta, authorizationService);
|
||||
MapQueryGet(endpoints, route, queryMeta, authorizationService);
|
||||
MapQueryPost(endpoints, route, queryMeta);
|
||||
MapQueryGet(endpoints, route, queryMeta);
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
@ -43,13 +42,13 @@ public static class EndpointRouteBuilderExtensions
|
||||
private static void MapQueryPost(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
IQueryMeta queryMeta,
|
||||
IQueryAuthorizationService? authorizationService)
|
||||
IQueryMeta queryMeta)
|
||||
{
|
||||
var handlerType = typeof(IQueryHandler<,>).MakeGenericType(queryMeta.QueryType, queryMeta.QueryResultType);
|
||||
|
||||
endpoints.MapPost(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var authorizationService = serviceProvider.GetService<IQueryAuthorizationService>();
|
||||
if (authorizationService != null)
|
||||
{
|
||||
var authorizationResult = await authorizationService.IsAllowedAsync(queryMeta.QueryType, cancellationToken);
|
||||
@ -90,13 +89,13 @@ public static class EndpointRouteBuilderExtensions
|
||||
private static void MapQueryGet(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
IQueryMeta queryMeta,
|
||||
IQueryAuthorizationService? authorizationService)
|
||||
IQueryMeta queryMeta)
|
||||
{
|
||||
var handlerType = typeof(IQueryHandler<,>).MakeGenericType(queryMeta.QueryType, queryMeta.QueryResultType);
|
||||
|
||||
endpoints.MapGet(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var authorizationService = serviceProvider.GetService<IQueryAuthorizationService>();
|
||||
if (authorizationService != null)
|
||||
{
|
||||
var authorizationResult = await authorizationService.IsAllowedAsync(queryMeta.QueryType, cancellationToken);
|
||||
@ -153,7 +152,6 @@ public static class EndpointRouteBuilderExtensions
|
||||
public static IEndpointRouteBuilder MapSvrntyCommands(this IEndpointRouteBuilder endpoints, string routePrefix = "api/command")
|
||||
{
|
||||
var commandDiscovery = endpoints.ServiceProvider.GetRequiredService<ICommandDiscovery>();
|
||||
var authorizationService = endpoints.ServiceProvider.GetService<ICommandAuthorizationService>();
|
||||
|
||||
foreach (var commandMeta in commandDiscovery.GetCommands())
|
||||
{
|
||||
@ -165,11 +163,11 @@ public static class EndpointRouteBuilderExtensions
|
||||
|
||||
if (commandMeta.CommandResultType == null)
|
||||
{
|
||||
MapCommandWithoutResult(endpoints, route, commandMeta, authorizationService);
|
||||
MapCommandWithoutResult(endpoints, route, commandMeta);
|
||||
}
|
||||
else
|
||||
{
|
||||
MapCommandWithResult(endpoints, route, commandMeta, authorizationService);
|
||||
MapCommandWithResult(endpoints, route, commandMeta);
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,13 +177,13 @@ public static class EndpointRouteBuilderExtensions
|
||||
private static void MapCommandWithoutResult(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
ICommandMeta commandMeta,
|
||||
ICommandAuthorizationService? authorizationService)
|
||||
ICommandMeta commandMeta)
|
||||
{
|
||||
var handlerType = typeof(ICommandHandler<>).MakeGenericType(commandMeta.CommandType);
|
||||
|
||||
endpoints.MapPost(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var authorizationService = serviceProvider.GetService<ICommandAuthorizationService>();
|
||||
if (authorizationService != null)
|
||||
{
|
||||
var authorizationResult = await authorizationService.IsAllowedAsync(commandMeta.CommandType, cancellationToken);
|
||||
@ -221,13 +219,13 @@ public static class EndpointRouteBuilderExtensions
|
||||
private static void MapCommandWithResult(
|
||||
IEndpointRouteBuilder endpoints,
|
||||
string route,
|
||||
ICommandMeta commandMeta,
|
||||
ICommandAuthorizationService? authorizationService)
|
||||
ICommandMeta commandMeta)
|
||||
{
|
||||
var handlerType = typeof(ICommandHandler<,>).MakeGenericType(commandMeta.CommandType, commandMeta.CommandResultType!);
|
||||
|
||||
endpoints.MapPost(route, async (HttpContext context, IServiceProvider serviceProvider, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var authorizationService = serviceProvider.GetService<ICommandAuthorizationService>();
|
||||
if (authorizationService != null)
|
||||
{
|
||||
var authorizationResult = await authorizationService.IsAllowedAsync(commandMeta.CommandType, cancellationToken);
|
||||
|
||||
@ -1,7 +1,111 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option csharp_namespace = "Generated.Grpc";
|
||||
option csharp_namespace = "Svrnty.Sample.Grpc";
|
||||
|
||||
package cqrs;
|
||||
|
||||
// Placeholder proto file - will be regenerated on next build
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user