All checks were successful
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>
185 lines
7.5 KiB
C#
185 lines
7.5 KiB
C#
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;
|
|
using PoweredSoft.DynamicQuery;
|
|
using PoweredSoft.DynamicQuery.Core;
|
|
|
|
namespace Svrnty.CQRS.DynamicQuery;
|
|
|
|
public abstract class DynamicQueryHandlerBase<TSource, TDestination>
|
|
where TSource : class
|
|
where TDestination : class
|
|
{
|
|
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 IServiceProvider _serviceProvider;
|
|
|
|
public DynamicQueryHandlerBase(IQueryHandlerAsync queryHandlerAsync,
|
|
IEnumerable<IQueryableProvider<TSource>> queryableProviders,
|
|
IEnumerable<IAlterQueryableService<TSource, TDestination>> alterQueryableServices,
|
|
IEnumerable<IDynamicQueryInterceptorProvider<TSource, TDestination>> dynamicQueryInterceptorProviders,
|
|
IServiceProvider serviceProvider)
|
|
{
|
|
_queryHandlerAsync = queryHandlerAsync;
|
|
_queryableProviders = queryableProviders;
|
|
_alterQueryableServices = alterQueryableServices;
|
|
_dynamicQueryInterceptorProviders = dynamicQueryInterceptorProviders;
|
|
_serviceProvider = serviceProvider;
|
|
}
|
|
|
|
protected virtual Task<IQueryable<TSource>> 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<TSource> for {typeof(TSource).Name}");
|
|
}
|
|
|
|
public virtual IQueryExecutionOptions GetQueryExecutionOptions(IQueryable<TSource> source, IDynamicQuery query)
|
|
{
|
|
return new QueryExecutionOptions();
|
|
}
|
|
|
|
public virtual IEnumerable<IQueryInterceptor> GetInterceptors()
|
|
{
|
|
var types = _dynamicQueryInterceptorProviders.SelectMany(t => t.GetInterceptorsTypes()).Distinct();
|
|
foreach (var type in types)
|
|
yield return _serviceProvider.GetService(type) as IQueryInterceptor;
|
|
}
|
|
|
|
protected async Task<IQueryExecutionResult<TDestination>> 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<TSource, TDestination>(source, criteria, options, cancellationToken);
|
|
return result;
|
|
}
|
|
|
|
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);
|
|
|
|
return source;
|
|
}
|
|
|
|
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 = 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;
|
|
}
|
|
}
|
|
}
|
|
|