33using JetBrains . Annotations ;
44using JsonApiDotNetCore . Configuration ;
55using JsonApiDotNetCore . Queries . Expressions ;
6+ using JsonApiDotNetCore . QueryStrings ;
67using JsonApiDotNetCore . Resources ;
78using JsonApiDotNetCore . Resources . Annotations ;
89using JsonApiDotNetCore . Resources . Internal ;
@@ -13,14 +14,18 @@ namespace JsonApiDotNetCore.Queries.Internal.Parsing;
1314public class FilterParser : QueryExpressionParser
1415{
1516 private readonly IResourceFactory _resourceFactory ;
17+ private readonly IEnumerable < IFilterValueConverter > _filterValueConverters ;
1618 private readonly Action < ResourceFieldAttribute , ResourceType , string > ? _validateSingleFieldCallback ;
1719 private ResourceType ? _resourceTypeInScope ;
1820
19- public FilterParser ( IResourceFactory resourceFactory , Action < ResourceFieldAttribute , ResourceType , string > ? validateSingleFieldCallback = null )
21+ public FilterParser ( IResourceFactory resourceFactory , IEnumerable < IFilterValueConverter > filterValueConverters ,
22+ Action < ResourceFieldAttribute , ResourceType , string > ? validateSingleFieldCallback = null )
2023 {
2124 ArgumentGuard . NotNull ( resourceFactory ) ;
25+ ArgumentGuard . NotNull ( filterValueConverters ) ;
2226
2327 _resourceFactory = resourceFactory ;
28+ _filterValueConverters = filterValueConverters ;
2429 _validateSingleFieldCallback = validateSingleFieldCallback ;
2530 }
2631
@@ -153,7 +158,7 @@ protected ComparisonExpression ParseComparison(string operatorName)
153158 }
154159 else if ( leftTerm is ResourceFieldChainExpression fieldChain && fieldChain . Fields [ ^ 1 ] is AttrAttribute attribute )
155160 {
156- Converter < string , object > rightConstantValueConverter = GetConstantValueConverterForAttribute ( attribute ) ;
161+ Converter < string , object > rightConstantValueConverter = GetConstantValueConverterForAttribute ( attribute , typeof ( ComparisonExpression ) ) ;
157162 rightTerm = ParseCountOrConstantOrNullOrField ( FieldChainRequirements . EndsInAttribute , rightConstantValueConverter ) ;
158163 }
159164 else
@@ -172,16 +177,11 @@ protected MatchTextExpression ParseTextMatch(string matchFunctionName)
172177 EatSingleCharacterToken ( TokenKind . OpenParen ) ;
173178
174179 ResourceFieldChainExpression targetAttributeChain = ParseFieldChain ( FieldChainRequirements . EndsInAttribute , null ) ;
175- Type targetAttributeType = ( ( AttrAttribute ) targetAttributeChain . Fields [ ^ 1 ] ) . Property . PropertyType ;
176-
177- if ( targetAttributeType != typeof ( string ) )
178- {
179- throw new QueryParseException ( "Attribute of type 'String' expected." ) ;
180- }
180+ var targetAttribute = ( AttrAttribute ) targetAttributeChain . Fields [ ^ 1 ] ;
181181
182182 EatSingleCharacterToken ( TokenKind . Comma ) ;
183183
184- Converter < string , object > constantValueConverter = stringValue => stringValue ;
184+ Converter < string , object > constantValueConverter = GetConstantValueConverterForAttribute ( targetAttribute , typeof ( MatchTextExpression ) ) ;
185185 LiteralConstantExpression constant = ParseConstant ( constantValueConverter ) ;
186186
187187 EatSingleCharacterToken ( TokenKind . CloseParen ) ;
@@ -197,12 +197,12 @@ protected AnyExpression ParseAny()
197197
198198 ResourceFieldChainExpression targetAttributeChain = ParseFieldChain ( FieldChainRequirements . EndsInAttribute , null ) ;
199199 var targetAttribute = ( AttrAttribute ) targetAttributeChain . Fields [ ^ 1 ] ;
200- Converter < string , object > constantValueConverter = GetConstantValueConverterForAttribute ( targetAttribute ) ;
201200
202201 EatSingleCharacterToken ( TokenKind . Comma ) ;
203202
204203 ImmutableHashSet < LiteralConstantExpression > . Builder constantsBuilder = ImmutableHashSet . CreateBuilder < LiteralConstantExpression > ( ) ;
205204
205+ Converter < string , object > constantValueConverter = GetConstantValueConverterForAttribute ( targetAttribute , typeof ( AnyExpression ) ) ;
206206 LiteralConstantExpression constant = ParseConstant ( constantValueConverter ) ;
207207 constantsBuilder . Add ( constant ) ;
208208
@@ -444,23 +444,76 @@ private Converter<string, object> GetConstantValueConverterForCount()
444444 return stringValue => ConvertStringToType ( stringValue , typeof ( int ) ) ;
445445 }
446446
447- private object ConvertStringToType ( string value , Type type )
447+ private static object ConvertStringToType ( string value , Type type )
448448 {
449449 try
450450 {
451451 return RuntimeTypeConverter . ConvertType ( value , type ) ! ;
452452 }
453- catch ( FormatException )
453+ catch ( FormatException exception )
454454 {
455- throw new QueryParseException ( $ "Failed to convert '{ value } ' of type 'String' to type '{ type . Name } '.") ;
455+ throw new QueryParseException ( $ "Failed to convert '{ value } ' of type 'String' to type '{ type . Name } '.", exception ) ;
456456 }
457457 }
458458
459- private Converter < string , object > GetConstantValueConverterForAttribute ( AttrAttribute attribute )
459+ private Converter < string , object > GetConstantValueConverterForAttribute ( AttrAttribute attribute , Type outerExpressionType )
460460 {
461- return stringValue => attribute . Property . Name == nameof ( Identifiable < object > . Id )
462- ? DeObfuscateStringId ( attribute . Type . ClrType , stringValue )
463- : ConvertStringToType ( stringValue , attribute . Property . PropertyType ) ;
461+ return stringValue =>
462+ {
463+ object ? value = TryConvertFromStringUsingFilterValueConverters ( attribute , stringValue , outerExpressionType ) ;
464+
465+ if ( value != null )
466+ {
467+ return value ;
468+ }
469+
470+ if ( outerExpressionType == typeof ( MatchTextExpression ) )
471+ {
472+ if ( attribute . Property . PropertyType != typeof ( string ) )
473+ {
474+ throw new QueryParseException ( "Attribute of type 'String' expected." ) ;
475+ }
476+ }
477+ else
478+ {
479+ // Partial text matching on an obfuscated ID usually fails.
480+ if ( attribute . Property . Name == nameof ( Identifiable < object > . Id ) )
481+ {
482+ return DeObfuscateStringId ( attribute . Type . ClrType , stringValue ) ;
483+ }
484+ }
485+
486+ return ConvertStringToType ( stringValue , attribute . Property . PropertyType ) ;
487+ } ;
488+ }
489+
490+ private object ? TryConvertFromStringUsingFilterValueConverters ( AttrAttribute attribute , string stringValue , Type outerExpressionType )
491+ {
492+ bool isPartialTextMatch = outerExpressionType == typeof ( MatchTextExpression ) ;
493+
494+ foreach ( IFilterValueConverter converter in _filterValueConverters )
495+ {
496+ if ( converter . CanConvert ( attribute ) )
497+ {
498+ object result = converter . Convert ( attribute , stringValue , outerExpressionType ) ;
499+
500+ if ( result == null )
501+ {
502+ throw new InvalidOperationException (
503+ $ "Converter '{ converter . GetType ( ) . Name } ' returned null for '{ stringValue } ' on attribute '{ attribute . PublicName } '. Return a sentinel value instead.") ;
504+ }
505+
506+ if ( isPartialTextMatch && result is not string )
507+ {
508+ throw new InvalidOperationException (
509+ $ "Converter '{ converter . GetType ( ) . Name } ' returned a non-string value for '{ stringValue } ' on attribute '{ attribute . PublicName } '.") ;
510+ }
511+
512+ return result ;
513+ }
514+ }
515+
516+ return null ;
464517 }
465518
466519 private object DeObfuscateStringId ( Type resourceClrType , string stringId )
0 commit comments