@@ -151,14 +151,20 @@ private static bool MatchesParameterQuery(
151151 }
152152 }
153153
154- if ( string . Equals ( p . ParameterType , "Literal" , StringComparison . OrdinalIgnoreCase )
155- && string . Equals ( p . Name , "category" , StringComparison . OrdinalIgnoreCase ) )
154+ if ( string . Equals ( p . ParameterType , "Literal" , StringComparison . OrdinalIgnoreCase ) )
156155 {
157- if ( ! MatchesLiteralCategory ( resource . Resource , p . Literal ) )
156+ // Skip FHIR pagination/control parameters — they are not filters.
157+ if ( string . Equals ( p . Name , "_count" , StringComparison . OrdinalIgnoreCase )
158+ || string . Equals ( p . Name , "_sort" , StringComparison . OrdinalIgnoreCase )
159+ || string . Equals ( p . Name , "_include" , StringComparison . OrdinalIgnoreCase )
160+ || string . Equals ( p . Name , "_revinclude" , StringComparison . OrdinalIgnoreCase ) )
161+ continue ;
162+
163+ if ( ! MatchesLiteralFilter ( resource . Resource , p . Name , p . Literal ) )
158164 return false ;
159165 }
160166
161- if ( string . Equals ( p . Name , "date" , StringComparison . OrdinalIgnoreCase )
167+ if ( IsTemporalSearchParam ( p . Name )
162168 && string . Equals ( p . ParameterType , "Variable" , StringComparison . OrdinalIgnoreCase ) )
163169 {
164170 var isGe = p . Format ? . StartsWith ( "ge" , StringComparison . OrdinalIgnoreCase ) == true ;
@@ -176,7 +182,7 @@ private static bool MatchesParameterQuery(
176182 // ge{S}: resource.End >= S (resource extends into [S, +inf))
177183 // le{E}: resource.Start <= E (resource extends into (-inf, E])
178184 // For instant types (e.g. authoredOn) start == end.
179- if ( ! TryGetResourceDateRange ( resource . ResourceType , resource . Resource ,
185+ if ( ! TryGetResourceDateRangeForParam ( p . Name , resource . ResourceType , resource . Resource ,
180186 out var resourceStart , out var resourceEnd ) )
181187 {
182188 // Fail closed: the query has a date filter and we don't recognize the
@@ -186,8 +192,8 @@ private static bool MatchesParameterQuery(
186192 {
187193 output . WriteLine (
188194 $ " [simulator] WARNING: { resource . Key } has no recognized date field for the " +
189- $ "'{ resource . ResourceType } ' Parameter query 'date ' filter; excluding from " +
190- "predicted-acquired set (fail-closed). Extend TryGetResourceDateRange to model this shape." ) ;
195+ $ "'{ resource . ResourceType } ' Parameter query '{ p . Name } ' filter; excluding from " +
196+ "predicted-acquired set (fail-closed). Extend TryGetResourceDateRangeForParam to model this shape." ) ;
191197 }
192198 return false ;
193199 }
@@ -283,36 +289,126 @@ private static bool TryGetReferencedResourceId(JsonElement resource, string reso
283289 return false ;
284290 }
285291
286- private static bool MatchesLiteralCategory ( JsonElement resource , string ? literal )
292+ /// <summary>
293+ /// Generic literal filter. Handles:
294+ /// - CodeableConcept arrays (e.g. <c>category</c>) — matches if any coding.code is in the accepted set.
295+ /// - Simple code/string fields (e.g. <c>intent</c>, <c>status</c>) — matches if the field value is in the accepted set.
296+ /// </summary>
297+ private static bool MatchesLiteralFilter ( JsonElement resource , string paramName , string ? literal )
287298 {
288299 if ( string . IsNullOrWhiteSpace ( literal ) )
289300 return true ;
290301
291302 var accepted = literal . Split ( ',' , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries )
292303 . ToHashSet ( StringComparer . OrdinalIgnoreCase ) ;
293304
294- if ( ! resource . TryGetProperty ( "category" , out var categories ) || categories . ValueKind != JsonValueKind . Array )
305+ if ( ! resource . TryGetProperty ( paramName , out var fieldValue ) )
295306 return false ;
296307
297- foreach ( var category in categories . EnumerateArray ( ) )
308+ // Case 1: Simple string/code field (e.g. intent, status)
309+ if ( fieldValue . ValueKind == JsonValueKind . String )
298310 {
299- if ( ! category . TryGetProperty ( "coding" , out var codingArray ) || codingArray . ValueKind != JsonValueKind . Array )
300- continue ;
311+ var value = fieldValue . GetString ( ) ;
312+ return ! string . IsNullOrWhiteSpace ( value ) && accepted . Contains ( value ) ;
313+ }
301314
302- foreach ( var coding in codingArray . EnumerateArray ( ) )
315+ // Case 2: CodeableConcept array (e.g. category)
316+ if ( fieldValue . ValueKind == JsonValueKind . Array )
317+ {
318+ foreach ( var item in fieldValue . EnumerateArray ( ) )
303319 {
304- if ( ! coding . TryGetProperty ( "code" , out var codeProp ) || codeProp . ValueKind != JsonValueKind . String )
305- continue ;
320+ if ( item . TryGetProperty ( "coding" , out var codingArray ) && codingArray . ValueKind == JsonValueKind . Array )
321+ {
322+ foreach ( var coding in codingArray . EnumerateArray ( ) )
323+ {
324+ if ( coding . TryGetProperty ( "code" , out var codeProp )
325+ && codeProp . ValueKind == JsonValueKind . String )
326+ {
327+ var code = codeProp . GetString ( ) ;
328+ if ( ! string . IsNullOrWhiteSpace ( code ) && accepted . Contains ( code ) )
329+ return true ;
330+ }
331+ }
332+ }
333+ }
334+ return false ;
335+ }
306336
307- var code = codeProp . GetString ( ) ;
308- if ( ! string . IsNullOrWhiteSpace ( code ) && accepted . Contains ( code ) )
309- return true ;
337+ // Case 3: Single CodeableConcept object (e.g. code)
338+ if ( fieldValue . ValueKind == JsonValueKind . Object
339+ && fieldValue . TryGetProperty ( "coding" , out var singleCodingArray )
340+ && singleCodingArray . ValueKind == JsonValueKind . Array )
341+ {
342+ foreach ( var coding in singleCodingArray . EnumerateArray ( ) )
343+ {
344+ if ( coding . TryGetProperty ( "code" , out var codeProp )
345+ && codeProp . ValueKind == JsonValueKind . String )
346+ {
347+ var code = codeProp . GetString ( ) ;
348+ if ( ! string . IsNullOrWhiteSpace ( code ) && accepted . Contains ( code ) )
349+ return true ;
350+ }
310351 }
352+ return false ;
311353 }
312354
313355 return false ;
314356 }
315357
358+ /// <summary>
359+ /// Determines whether a FHIR search parameter name is a temporal (date-like) filter.
360+ /// Recognized names: <c>date</c>, <c>authoredon</c>, <c>authored</c>, <c>issued</c>,
361+ /// <c>effective</c>, <c>onset-date</c>, <c>recorded-date</c>.
362+ /// </summary>
363+ private static bool IsTemporalSearchParam ( string paramName )
364+ => string . Equals ( paramName , "date" , StringComparison . OrdinalIgnoreCase )
365+ || string . Equals ( paramName , "authoredon" , StringComparison . OrdinalIgnoreCase )
366+ || string . Equals ( paramName , "authored" , StringComparison . OrdinalIgnoreCase )
367+ || string . Equals ( paramName , "issued" , StringComparison . OrdinalIgnoreCase )
368+ || string . Equals ( paramName , "effective" , StringComparison . OrdinalIgnoreCase )
369+ || string . Equals ( paramName , "onset-date" , StringComparison . OrdinalIgnoreCase )
370+ || string . Equals ( paramName , "recorded-date" , StringComparison . OrdinalIgnoreCase ) ;
371+
372+ /// <summary>
373+ /// Resolves the resource's date range for a given FHIR search parameter name.
374+ /// Named search parameters like <c>authoredon</c> map to specific fields regardless of
375+ /// what <c>TryGetResourceDateRange</c> would pick for the generic <c>date</c> param.
376+ /// </summary>
377+ private static bool TryGetResourceDateRangeForParam ( string paramName , string resourceType ,
378+ JsonElement resource , out DateTimeOffset start , out DateTimeOffset end )
379+ {
380+ // Named search parameters that map to specific fields.
381+ if ( string . Equals ( paramName , "authoredon" , StringComparison . OrdinalIgnoreCase )
382+ || string . Equals ( paramName , "authored" , StringComparison . OrdinalIgnoreCase ) )
383+ {
384+ return TryGetInstant ( resource , "authoredOn" , out start , out end ) ;
385+ }
386+
387+ if ( string . Equals ( paramName , "issued" , StringComparison . OrdinalIgnoreCase ) )
388+ {
389+ return TryGetInstant ( resource , "issued" , out start , out end ) ;
390+ }
391+
392+ if ( string . Equals ( paramName , "effective" , StringComparison . OrdinalIgnoreCase ) )
393+ {
394+ return TryGetEffective ( resource , out start , out end ) ;
395+ }
396+
397+ if ( string . Equals ( paramName , "onset-date" , StringComparison . OrdinalIgnoreCase ) )
398+ {
399+ return TryGetInstant ( resource , "onsetDateTime" , out start , out end )
400+ || TryGetPeriod ( resource , "onsetPeriod" , out start , out end ) ;
401+ }
402+
403+ if ( string . Equals ( paramName , "recorded-date" , StringComparison . OrdinalIgnoreCase ) )
404+ {
405+ return TryGetInstant ( resource , "recordedDate" , out start , out end ) ;
406+ }
407+
408+ // Generic "date" → resource-type-aware resolution.
409+ return TryGetResourceDateRange ( resourceType , resource , out start , out end ) ;
410+ }
411+
316412 /// <summary>
317413 /// Returns the resource's date range (start, end) for FHIR <c>date</c> search-param
318414 /// matching with overlap semantics. For instant-style fields the start and end are
0 commit comments