From b372805c4ecefb239217aac7a251e997f67439ec Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Tue, 17 Feb 2026 11:28:09 -0500 Subject: [PATCH] Fix string filter values not converting to correct CLR types for DynamicQuery 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 --- .../DynamicQueryHandlerBase.cs | 102 ++++++++++++++++-- 1 file changed, 95 insertions(+), 7 deletions(-) diff --git a/Svrnty.CQRS.DynamicQuery/DynamicQueryHandlerBase.cs b/Svrnty.CQRS.DynamicQuery/DynamicQueryHandlerBase.cs index ac844ee..54dee1f 100644 --- a/Svrnty.CQRS.DynamicQuery/DynamicQueryHandlerBase.cs +++ b/Svrnty.CQRS.DynamicQuery/DynamicQueryHandlerBase.cs @@ -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 private readonly IQueryHandlerAsync _queryHandlerAsync; private readonly IEnumerable> _queryableProviders; private readonly IEnumerable> _alterQueryableServices; - private readonly IEnumerable> _dynamicQueryInterceptorProviders; + + private readonly IEnumerable> + _dynamicQueryInterceptorProviders; + private readonly IServiceProvider _serviceProvider; - public DynamicQueryHandlerBase(IQueryHandlerAsync queryHandlerAsync, + public DynamicQueryHandlerBase(IQueryHandlerAsync queryHandlerAsync, IEnumerable> queryableProviders, IEnumerable> alterQueryableServices, IEnumerable> dynamicQueryInterceptorProviders, @@ -32,7 +38,8 @@ public abstract class DynamicQueryHandlerBase _serviceProvider = serviceProvider; } - protected virtual Task> GetQueryableAsync(IDynamicQuery query, CancellationToken cancellationToken = default) + protected virtual Task> GetQueryableAsync(IDynamicQuery query, + CancellationToken cancellationToken = default) { if (_queryableProviders.Any()) { @@ -56,7 +63,8 @@ public abstract class DynamicQueryHandlerBase yield return _serviceProvider.GetService(type) as IQueryInterceptor; } - protected async Task> ProcessQueryAsync(IDynamicQuery query, CancellationToken cancellationToken = default) + protected async Task> 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 _queryHandlerAsync.AddInterceptor(interceptor); var criteria = CreateCriteriaFromQuery(query); - var result = await _queryHandlerAsync.ExecuteAsync(source, criteria, options, cancellationToken); + var result = + await _queryHandlerAsync.ExecuteAsync(source, criteria, options, cancellationToken); return result; } - protected virtual async Task> AlterSourceAsync(IQueryable source, IDynamicQuery query, CancellationToken cancellationToken) + protected virtual async Task> AlterSourceAsync(IQueryable 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 protected virtual IQueryCriteria CreateCriteriaFromQuery(IDynamicQuery query) { + var filters = query?.GetFilters() ?? new List(); + ConvertFilterValuesToPropertyTypes(filters); var criteria = new QueryCriteria { Page = query?.GetPage(), PageSize = query?.GetPageSize(), - Filters = query?.GetFilters() ?? new List(), + Filters = filters, Sorts = query?.GetSorts() ?? new List(), Groups = query?.GetGroups() ?? new List(), Aggregates = query?.GetAggregates() ?? new List() }; return criteria; } + + /// + /// 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. + /// + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2087", + Justification = "TSource properties are preserved by EF Core and DynamicLinq usage")] + private static void ConvertFilterValuesToPropertyTypes(List 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; + } + } }