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 PoweredSoft.DynamicQuery; using PoweredSoft.DynamicQuery.Core; using Svrnty.CQRS.DynamicQuery.Abstractions; namespace Svrnty.CQRS.DynamicQuery; public abstract class DynamicQueryHandlerBase where TSource : class where TDestination : class { private readonly IQueryHandlerAsync _queryHandlerAsync; private readonly IEnumerable> _queryableProviders; private readonly IEnumerable> _alterQueryableServices; private readonly IEnumerable> _dynamicQueryInterceptorProviders; private readonly IServiceProvider _serviceProvider; public DynamicQueryHandlerBase(IQueryHandlerAsync queryHandlerAsync, IEnumerable> queryableProviders, IEnumerable> alterQueryableServices, IEnumerable> dynamicQueryInterceptorProviders, IServiceProvider serviceProvider) { _queryHandlerAsync = queryHandlerAsync; _queryableProviders = queryableProviders; _alterQueryableServices = alterQueryableServices; _dynamicQueryInterceptorProviders = dynamicQueryInterceptorProviders; _serviceProvider = serviceProvider; } protected virtual Task> GetQueryableAsync(IDynamicQuery query, CancellationToken cancellationToken = default) { if (_queryableProviders.Any()) { // 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 for {typeof(TSource).Name}"); } public virtual IQueryExecutionOptions GetQueryExecutionOptions(IQueryable source, IDynamicQuery query) { return new QueryExecutionOptions(); } public virtual IEnumerable GetInterceptors() { var types = _dynamicQueryInterceptorProviders.SelectMany(t => t.GetInterceptorsTypes()).Distinct(); foreach (var type in types) yield return _serviceProvider.GetService(type) as IQueryInterceptor; } protected async Task> ProcessQueryAsync(IDynamicQuery query, CancellationToken cancellationToken = default) { var source = await GetQueryableAsync(query, cancellationToken); source = await AlterSourceAsync(source, query, cancellationToken); var options = GetQueryExecutionOptions(source, query); var interceptors = GetInterceptors(); foreach (var interceptor in interceptors) _queryHandlerAsync.AddInterceptor(interceptor); var criteria = CreateCriteriaFromQuery(query); var result = await _queryHandlerAsync.ExecuteAsync(source, criteria, options, cancellationToken); return result; } protected virtual async Task> AlterSourceAsync(IQueryable source, IDynamicQuery query, CancellationToken cancellationToken) { foreach (var t in _alterQueryableServices) source = await t.AlterQueryableAsync(source, query, cancellationToken); return source; } 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 = 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; } } }