Compare commits

..

3 Commits

Author SHA1 Message Date
david.nguyen 6aece5a769 Handle generic types in proto message name generation
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>
2026-02-19 18:56:37 -05:00
david.nguyen b372805c4e Fix string filter values not converting to correct CLR types for DynamicQuery
Publish NuGets / build (release) Successful in 41s
Convert string filter values (e.g. from gRPC transport) to their actual
property types (DateTime, DateTimeOffset) so PoweredSoft.DynamicLinq can
build valid LINQ expressions. Also removes filters with null values and
recurses into composite filters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 11:28:09 -05:00
david.nguyen 89ccbe990f add AND / OR support when filtering
Publish NuGets / build (release) Successful in 34s
2026-02-02 17:53:43 -05:00
5 changed files with 173 additions and 14 deletions
@@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Svrnty.CQRS.DynamicQuery.Abstractions;
@@ -16,10 +19,13 @@ public abstract class DynamicQueryHandlerBase<TSource, TDestination>
private readonly IQueryHandlerAsync _queryHandlerAsync;
private readonly IEnumerable<IQueryableProvider<TSource>> _queryableProviders;
private readonly IEnumerable<IAlterQueryableService<TSource, TDestination>> _alterQueryableServices;
private readonly IEnumerable<IDynamicQueryInterceptorProvider<TSource, TDestination>> _dynamicQueryInterceptorProviders;
private readonly IEnumerable<IDynamicQueryInterceptorProvider<TSource, TDestination>>
_dynamicQueryInterceptorProviders;
private readonly IServiceProvider _serviceProvider;
public DynamicQueryHandlerBase(IQueryHandlerAsync queryHandlerAsync,
public DynamicQueryHandlerBase(IQueryHandlerAsync queryHandlerAsync,
IEnumerable<IQueryableProvider<TSource>> queryableProviders,
IEnumerable<IAlterQueryableService<TSource, TDestination>> alterQueryableServices,
IEnumerable<IDynamicQueryInterceptorProvider<TSource, TDestination>> dynamicQueryInterceptorProviders,
@@ -32,7 +38,8 @@ public abstract class DynamicQueryHandlerBase<TSource, TDestination>
_serviceProvider = serviceProvider;
}
protected virtual Task<IQueryable<TSource>> GetQueryableAsync(IDynamicQuery query, CancellationToken cancellationToken = default)
protected virtual Task<IQueryable<TSource>> GetQueryableAsync(IDynamicQuery query,
CancellationToken cancellationToken = default)
{
if (_queryableProviders.Any())
{
@@ -56,7 +63,8 @@ public abstract class DynamicQueryHandlerBase<TSource, TDestination>
yield return _serviceProvider.GetService(type) as IQueryInterceptor;
}
protected async Task<IQueryExecutionResult<TDestination>> ProcessQueryAsync(IDynamicQuery query, CancellationToken cancellationToken = default)
protected async Task<IQueryExecutionResult<TDestination>> ProcessQueryAsync(IDynamicQuery query,
CancellationToken cancellationToken = default)
{
var source = await GetQueryableAsync(query, cancellationToken);
source = await AlterSourceAsync(source, query, cancellationToken);
@@ -67,11 +75,13 @@ public abstract class DynamicQueryHandlerBase<TSource, TDestination>
_queryHandlerAsync.AddInterceptor(interceptor);
var criteria = CreateCriteriaFromQuery(query);
var result = await _queryHandlerAsync.ExecuteAsync<TSource, TDestination>(source, criteria, options, cancellationToken);
var result =
await _queryHandlerAsync.ExecuteAsync<TSource, TDestination>(source, criteria, options, cancellationToken);
return result;
}
protected virtual async Task<IQueryable<TSource>> AlterSourceAsync(IQueryable<TSource> source, IDynamicQuery query, CancellationToken cancellationToken)
protected virtual async Task<IQueryable<TSource>> AlterSourceAsync(IQueryable<TSource> source, IDynamicQuery query,
CancellationToken cancellationToken)
{
foreach (var t in _alterQueryableServices)
source = await t.AlterQueryableAsync(source, query, cancellationToken);
@@ -81,16 +91,94 @@ public abstract class DynamicQueryHandlerBase<TSource, TDestination>
protected virtual IQueryCriteria CreateCriteriaFromQuery(IDynamicQuery query)
{
var filters = query?.GetFilters() ?? new List<IFilter>();
ConvertFilterValuesToPropertyTypes(filters);
var criteria = new QueryCriteria
{
Page = query?.GetPage(),
PageSize = query?.GetPageSize(),
Filters = query?.GetFilters() ?? new List<IFilter>(),
Filters = filters,
Sorts = query?.GetSorts() ?? new List<ISort>(),
Groups = query?.GetGroups() ?? new List<IGroup>(),
Aggregates = query?.GetAggregates() ?? new List<IAggregate>()
};
return criteria;
}
/// <summary>
/// Converts string filter values to the correct CLR type based on TSource property types.
/// This handles the case where transport layers (e.g. gRPC) pass all values as strings,
/// but PoweredSoft.DynamicLinq needs the actual type to build LINQ expressions.
/// </summary>
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087",
Justification = "TSource properties are preserved by EF Core and DynamicLinq usage")]
private static void ConvertFilterValuesToPropertyTypes(List<IFilter> filters)
{
for (var i = filters.Count - 1; i >= 0; i--)
{
var filter = filters[i];
if (filter is SimpleFilter simpleFilter)
{
if (simpleFilter.Value == null)
{
filters.RemoveAt(i);
continue;
}
if (simpleFilter.Value is string strValue && !string.IsNullOrEmpty(strValue))
{
var propertyType = ResolvePropertyType(typeof(TSource), simpleFilter.Path);
if (propertyType == null)
continue;
var targetType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
if (targetType == typeof(DateTime))
{
if (DateTime.TryParse(strValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal,
out var dt))
{
simpleFilter.Value = DateTime.SpecifyKind(dt, DateTimeKind.Utc);
}
}
else if (targetType == typeof(DateTimeOffset))
{
if (DateTimeOffset.TryParse(strValue, CultureInfo.InvariantCulture, DateTimeStyles.None,
out var dto))
{
simpleFilter.Value = dto;
}
}
}
}
else if (filter is CompositeFilter compositeFilter && compositeFilter.Filters != null)
{
ConvertFilterValuesToPropertyTypes(compositeFilter.Filters);
}
}
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2070",
Justification = "Property types are preserved by EF Core and DynamicLinq usage")]
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2075",
Justification = "Nested property type resolution is inherently dynamic")]
static Type? ResolvePropertyType(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type type, string? path)
{
if (string.IsNullOrEmpty(path))
return null;
var currentType = type;
foreach (var part in path.Split('.'))
{
var property = currentType.GetProperty(part,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (property == null)
return null;
currentType = property.PropertyType;
}
return currentType;
}
}
}
+4 -2
View File
@@ -2864,7 +2864,8 @@ namespace Svrnty.CQRS.Grpc.Generators
sb.AppendLine(" {");
sb.AppendLine(" Path = protoFilter.Path,");
sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.FilterType)protoFilter.Type).ToString(),");
sb.AppendLine(" Value = protoFilter.Value");
sb.AppendLine(" Value = protoFilter.Value,");
sb.AppendLine(" And = true");
sb.AppendLine(" };");
sb.AppendLine();
sb.AppendLine(" // Handle nested AND filters");
@@ -2894,7 +2895,8 @@ namespace Svrnty.CQRS.Grpc.Generators
sb.AppendLine(" {");
sb.AppendLine(" Path = pf.Path,");
sb.AppendLine(" Type = ((PoweredSoft.DynamicQuery.Core.FilterType)pf.Type).ToString(),");
sb.AppendLine(" Value = pf.Value");
sb.AppendLine(" Value = pf.Value,");
sb.AppendLine(" And = true");
sb.AppendLine(" };");
sb.AppendLine(" if (pf.And != null && pf.And.Count > 0)");
sb.AppendLine(" {");
@@ -413,17 +413,21 @@ internal class ProtoFileGenerator
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;
// Don't generate messages for system types or primitives
if (type.ContainingNamespace?.ToString().StartsWith("System") == true)
return;
_generatedMessages.Add(type.Name);
_generatedMessages.Add(messageName);
_messagesBuilder.AppendLine($"// {type.Name} entity");
_messagesBuilder.AppendLine($"message {type.Name} {{");
_messagesBuilder.AppendLine($"// {messageName} entity");
_messagesBuilder.AppendLine($"message {messageName} {{");
// Collect nested complex types to generate after closing this message
var nestedComplexTypes = new List<INamedTypeSymbol>();
+16 -1
View File
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using Microsoft.CodeAnalysis;
namespace Svrnty.CQRS.Grpc.Generators;
@@ -151,13 +152,27 @@ internal static class ProtoFileTypeMapper
// Complex types (classes/records) become message types
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
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>
/// Converts C# PascalCase property name to proto snake_case field name.
/// Uses simple conversion: add underscore before each uppercase letter (except first).
+50
View File
@@ -21,6 +21,13 @@ service QueryService {
}
// 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;
@@ -59,3 +66,46 @@ message User {
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;
}