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; + } + } }