33using GridifyExtensions . Enums ;
44using GridifyExtensions . Models ;
55using Microsoft . EntityFrameworkCore ;
6+ using Microsoft . EntityFrameworkCore . Infrastructure ;
7+ using Microsoft . Extensions . DependencyInjection ;
68
79namespace GridifyExtensions . Extensions ;
810
@@ -236,56 +238,40 @@ public static async Task<PagedResponse<object>> ColumnDistinctValuesAsync<TEntit
236238
237239 if ( ! mapper . IsEncrypted ( model . PropertyName ) )
238240 {
239- var baseQuery = query
240- . ApplyFiltering ( gridifyModel , mapper )
241- . ApplySelect ( model . PropertyName , mapper ) ;
241+ var selectedNonEncrypted = query
242+ . ApplyFiltering ( gridifyModel , mapper )
243+ . ApplySelect ( model . PropertyName , mapper )
244+ . Distinct ( ) ;
242245
243- var filterEmpty = string . IsNullOrWhiteSpace ( gridifyModel . Filter ) ;
244- var hasNull = false ;
245- var take = model . PageSize ;
246-
247- if ( filterEmpty )
246+ var term = ExtractStarContainsTerm ( model . Filter , model . PropertyName ) ;
247+ if ( ! string . IsNullOrEmpty ( term ) && IsStringColumn ( query , mapper , model . PropertyName ) )
248248 {
249- // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
250- hasNull = await baseQuery . AnyAsync ( x => x == null , cancellationToken ) ;
251- if ( hasNull && take > 0 )
252- {
253- take -= 1 ;
254- }
249+ var termLower = term . ToLower ( ) ;
250+
251+ var projected = query
252+ . ApplyFiltering ( gridifyModel , mapper )
253+ . Select ( StringSelector ( query , mapper , model . PropertyName ) )
254+ . Distinct ( ) ;
255+
256+ var data = await projected
257+ . OrderBy ( x => x == null ? 0 : 1 )
258+ . ThenBy ( x => x != null && x . ToLower ( ) == termLower ? 0 : 1 )
259+ . ThenBy ( x => x == null ? int . MaxValue : x . Length )
260+ . ThenBy ( x => x )
261+ . Take ( model . PageSize )
262+ . ToListAsync ( cancellationToken ) ;
263+
264+ return new CursoredResponse < object ? > ( data . Cast < object ? > ( )
265+ . ToList ( ) ,
266+ model . PageSize ) ;
255267 }
256268
257- // smart ordering for string values ---
258- var orderedQuery = baseQuery . Distinct ( ) ;
259-
260- if ( typeof ( string ) . IsAssignableFrom ( orderedQuery . ElementType ) )
261- {
262- var stringQuery = ( IQueryable < string ? > ) orderedQuery ;
263-
264- orderedQuery = stringQuery
265- . OrderBy ( x => x == null ? int . MaxValue : x . Length ) // shorter first
266- . ThenBy ( x => x ) ! ; // then lexicographic
267- }
268- else
269- {
270- orderedQuery = orderedQuery . OrderBy ( x => x ) ;
271- }
272-
273- var result = await orderedQuery
274- . Take ( take )
275- . ToListAsync ( cancellationToken ) ;
276-
277- if ( ! filterEmpty || ! hasNull )
278- {
279- return new CursoredResponse < object ? > ( result ! , model . PageSize ) ;
280- }
269+ var data2 = await selectedNonEncrypted
270+ . OrderBy ( x => ( object ? ) x == null ? 0 : 1 )
271+ . Take ( model . PageSize )
272+ . ToListAsync ( cancellationToken ) ;
281273
282- if ( result . Count > 0 && ReferenceEquals ( result [ ^ 1 ] , null ) )
283- {
284- result . RemoveAt ( result . Count - 1 ) ;
285- }
286-
287- result . Insert ( 0 , null ! ) ;
288- return new CursoredResponse < object ? > ( result ! , model . PageSize ) ;
274+ return new CursoredResponse < object ? > ( data2 ! , model . PageSize ) ;
289275 }
290276
291277 // Encrypted path
@@ -397,4 +383,92 @@ public static IEnumerable<MappingModel> GetMappings<TEntity>()
397383 }
398384 } ) ;
399385 }
386+
387+
388+ private static Expression < Func < TEntity , string ? > > EfStringSelector < TEntity > ( string propertyName )
389+ where TEntity : class
390+ {
391+ var e = Expression . Parameter ( typeof ( TEntity ) , "e" ) ;
392+ var body = Expression . Call (
393+ typeof ( EF ) ,
394+ nameof ( EF . Property ) ,
395+ [
396+ typeof ( string )
397+ ] ,
398+ e ,
399+ Expression . Constant ( propertyName ) ) ;
400+
401+ return Expression . Lambda < Func < TEntity , string ? > > ( body , e ) ;
402+ }
403+
404+ private static string ? ExtractStarContainsTerm ( string ? filter , string propertyName )
405+ {
406+ if ( string . IsNullOrWhiteSpace ( filter ) ) return null ;
407+
408+ var m = System . Text . RegularExpressions . Regex . Match (
409+ filter ,
410+ $@ "(?i)\b{ System . Text . RegularExpressions . Regex . Escape ( propertyName ) } \s*=\s*\*(?<term>[^;,)]+)") ;
411+
412+ if ( ! m . Success ) return null ;
413+
414+ var term = m . Groups [ "term" ]
415+ . Value
416+ . Trim ( ) ;
417+ return term . Length == 0 ? null : term ;
418+ }
419+
420+ private static bool IsStringColumn < TEntity > ( IQueryable < TEntity > query , FilterMapper < TEntity > mapper , string name )
421+ where TEntity : class
422+ {
423+ var db = TryGetDbContext ( query ) ;
424+ var et = db ? . Model . FindEntityType ( typeof ( TEntity ) ) ;
425+ var p = et ? . FindProperty ( name ) ;
426+ if ( p != null )
427+ {
428+ return p . ClrType == typeof ( string ) ;
429+ }
430+
431+ var map = mapper . GetCurrentMaps ( )
432+ . FirstOrDefault ( m => m . From == name ) ;
433+ if ( map == null )
434+ {
435+ return false ;
436+ }
437+
438+ var body = map . To . Body is UnaryExpression { NodeType : ExpressionType . Convert } ue ? ue . Operand : map . To . Body ;
439+ return body . Type == typeof ( string ) ;
440+ }
441+
442+ private static Expression < Func < TEntity , string ? > > StringSelector < TEntity > ( IQueryable < TEntity > query ,
443+ FilterMapper < TEntity > mapper ,
444+ string name )
445+ where TEntity : class
446+ {
447+ var db = TryGetDbContext ( query ) ;
448+ var et = db ? . Model . FindEntityType ( typeof ( TEntity ) ) ;
449+ var p = et ? . FindProperty ( name ) ;
450+
451+ if ( p != null )
452+ {
453+ return EfStringSelector < TEntity > ( name ) ;
454+ }
455+
456+ var map = mapper . GetCurrentMaps ( )
457+ . FirstOrDefault ( m => m . From == name )
458+ ?? throw new KeyNotFoundException ( $ "No map found for '{ name } '.") ;
459+
460+ var param = map . To . Parameters [ 0 ] ;
461+ var body = map . To . Body is UnaryExpression { NodeType : ExpressionType . Convert } ue ? ue . Operand : map . To . Body ;
462+
463+ return body . Type != typeof ( string )
464+ ? throw new InvalidOperationException ( $ "Map '{ name } ' must return string. Actual: { body . Type } .")
465+ : Expression . Lambda < Func < TEntity , string ? > > ( body , param ) ;
466+ }
467+
468+ private static DbContext ? TryGetDbContext < TEntity > ( IQueryable < TEntity > query )
469+ {
470+ if ( query is not IInfrastructure < IServiceProvider > infra ) return null ;
471+ return infra . Instance . GetService < ICurrentDbContext > ( )
472+ ? . Context ;
473+ }
400474}
0 commit comments