@@ -374,6 +374,8 @@ private void ProcessMockResponseInternal(ProxyRequestArgs e, MockResponse matchi
374374 ProxyUtils . MergeHeaders ( headers , rateLimitingHeaders ) ;
375375 }
376376
377+ ReplacePlaceholders ( matchingResponse . Response , e . Session . HttpClient . Request , Logger ) ;
378+
377379 if ( matchingResponse . Response ? . Body is not null )
378380 {
379381 var bodyString = JsonSerializer . Serialize ( matchingResponse . Response . Body , ProxyUtils . JsonSerializerOptions ) as string ;
@@ -514,4 +516,209 @@ private static bool HasMatchingBody(MockResponse mockResponse, Request request)
514516
515517 return request . BodyString . Contains ( mockResponse . Request . BodyFragment , StringComparison . OrdinalIgnoreCase ) ;
516518 }
519+
520+ private static void ReplacePlaceholders ( MockResponseResponse ? response , Request request , ILogger logger )
521+ {
522+ logger . LogTrace ( "{Method} called" , nameof ( ReplacePlaceholders ) ) ;
523+
524+ if ( response is null ||
525+ response . Body is null || request . BodyString is null )
526+ {
527+ logger . LogTrace ( "Body is empty. Skipping replacing placeholders" ) ;
528+ return ;
529+ }
530+
531+ try
532+ {
533+ var requestBody = JsonSerializer . Deserialize < JsonElement > ( request . BodyString , ProxyUtils . JsonSerializerOptions ) ;
534+
535+ response . Body = ReplacePlaceholdersInObject ( response . Body , requestBody , logger ) ;
536+ }
537+ catch ( Exception ex )
538+ {
539+ logger . LogDebug ( ex , "Failed to parse request body as JSON" ) ;
540+ logger . LogWarning ( "Failed to parse request body as JSON. Placeholders in the mock response won't be replaced." ) ;
541+ }
542+
543+ logger . LogTrace ( "Left {Method}" , nameof ( ReplacePlaceholders ) ) ;
544+ }
545+
546+ private static object ? ReplacePlaceholdersInObject ( object ? obj , JsonElement requestBody , ILogger logger )
547+ {
548+ logger . LogTrace ( "{Method} called" , nameof ( ReplacePlaceholdersInObject ) ) ;
549+
550+ if ( obj is null )
551+ {
552+ return null ;
553+ }
554+
555+ // Handle JsonElement (which is what we get from System.Text.Json)
556+ if ( obj is JsonElement element )
557+ {
558+ return ReplacePlaceholdersInJsonElement ( element , requestBody , logger ) ;
559+ }
560+
561+ // Handle string values - check for placeholders
562+ if ( obj is string strValue )
563+ {
564+ return ReplacePlaceholderInString ( strValue , requestBody , logger ) ;
565+ }
566+
567+ // For other types, convert to JsonElement and process
568+ var json = JsonSerializer . Serialize ( obj ) ;
569+ var jsonElement = JsonSerializer . Deserialize < JsonElement > ( json ) ;
570+ return ReplacePlaceholdersInJsonElement ( jsonElement , requestBody , logger ) ;
571+ }
572+
573+ private static object ? ReplacePlaceholdersInJsonElement ( JsonElement element , JsonElement requestBody , ILogger logger )
574+ {
575+ logger . LogTrace ( "{Method} called" , nameof ( ReplacePlaceholdersInJsonElement ) ) ;
576+
577+ switch ( element . ValueKind )
578+ {
579+ case JsonValueKind . Object :
580+ var resultObj = new Dictionary < string , object ? > ( ) ;
581+ foreach ( var property in element . EnumerateObject ( ) )
582+ {
583+ resultObj [ property . Name ] = ReplacePlaceholdersInJsonElement ( property . Value , requestBody , logger ) ;
584+ }
585+ return resultObj ;
586+
587+ case JsonValueKind . Array :
588+ var resultArray = new List < object ? > ( ) ;
589+ foreach ( var item in element . EnumerateArray ( ) )
590+ {
591+ resultArray . Add ( ReplacePlaceholdersInJsonElement ( item , requestBody , logger ) ) ;
592+ }
593+ return resultArray ;
594+ case JsonValueKind . String :
595+ return ReplacePlaceholderInString ( element . GetString ( ) ?? "" , requestBody , logger ) ;
596+ case JsonValueKind . Number :
597+ return GetSafeNumber ( element , logger ) ;
598+ case JsonValueKind . True :
599+ return true ;
600+ case JsonValueKind . False :
601+ return false ;
602+ case JsonValueKind . Null :
603+ case JsonValueKind . Undefined :
604+ return null ;
605+ default :
606+ return element . ToString ( ) ;
607+ }
608+ }
609+
610+ #pragma warning disable CA1859
611+ // CA1859: This method must return object? because it may return different concrete types (string, int, bool, etc.) based on the JSON content.
612+ private static object ? ReplacePlaceholderInString ( string value , JsonElement requestBody , ILogger logger )
613+ #pragma warning restore CA1859
614+ {
615+ logger . LogTrace ( "{Method} called" , nameof ( ReplacePlaceholderInString ) ) ;
616+
617+ logger . LogDebug ( "Processing value: {Value}" , value ) ;
618+
619+ // Check if the value starts with @request.body.
620+ if ( ! value . StartsWith ( "@request.body." , StringComparison . OrdinalIgnoreCase ) )
621+ {
622+ logger . LogDebug ( "Value {Value} does not start with @request.body. Skipping" , value ) ;
623+ return value ;
624+ }
625+
626+ // Extract the property path after @request.body.
627+ var propertyPath = value [ "@request.body." . Length ..] ;
628+
629+ logger . LogDebug ( "Extracted property path: {PropertyPath}" , propertyPath ) ;
630+
631+ return GetValueFromRequestBody ( requestBody , propertyPath , logger ) ;
632+ }
633+
634+ private static object ? GetValueFromRequestBody ( JsonElement requestBody , string propertyPath , ILogger logger )
635+ {
636+ logger . LogTrace ( "{Method} called" , nameof ( GetValueFromRequestBody ) ) ;
637+
638+ logger . LogDebug ( "Getting value for {PropertyPath}" , propertyPath ) ;
639+
640+ try
641+ {
642+ // Split the property path by dots to handle nested properties
643+ var propertyNames = propertyPath . Split ( '.' ) ;
644+ return GetNestedValueFromJsonElement ( requestBody , propertyNames , logger ) ;
645+ }
646+ catch ( Exception ex )
647+ {
648+ // If we can't get the property, return null
649+ logger . LogDebug ( ex , "Failed to get value for {PropertyPath}. Returning null" , propertyPath ) ;
650+ }
651+
652+ return null ;
653+ }
654+
655+ private static object ? GetNestedValueFromJsonElement ( JsonElement element , string [ ] propertyNames , ILogger logger )
656+ {
657+ logger . LogTrace ( "{Method} called" , nameof ( GetNestedValueFromJsonElement ) ) ;
658+
659+ var current = element ;
660+
661+ // Navigate through the nested properties
662+ foreach ( var propertyName in propertyNames )
663+ {
664+ if ( current . ValueKind != JsonValueKind . Object )
665+ {
666+ logger . LogDebug ( "Current JSON element is not an object. Cannot navigate to property {PropertyName}" , propertyName ) ;
667+ return null ; // Can't navigate further if current element is not an object
668+ }
669+
670+ if ( ! current . TryGetProperty ( propertyName , out current ) )
671+ {
672+ logger . LogDebug ( "Property {PropertyName} not found in JSON. Returning null" , propertyName ) ;
673+ return null ; // Property not found
674+ }
675+ }
676+
677+ return ConvertJsonElementToObject ( current , logger ) ;
678+ }
679+
680+ private static object ? ConvertJsonElementToObject ( JsonElement element , ILogger logger )
681+ {
682+ logger . LogTrace ( "{Method} called" , nameof ( ConvertJsonElementToObject ) ) ;
683+
684+ return element . ValueKind switch
685+ {
686+ JsonValueKind . String => element . GetString ( ) ,
687+ JsonValueKind . Number => GetSafeNumber ( element , logger ) ,
688+ JsonValueKind . True => true ,
689+ JsonValueKind . False => false ,
690+ JsonValueKind . Null or JsonValueKind . Undefined => null ,
691+ // For complex objects/arrays, return the JsonElement itself
692+ // which can be serialized later
693+ JsonValueKind . Object or JsonValueKind . Array => element ,
694+ _ => element . ToString ( ) ,
695+ } ;
696+ }
697+
698+ // Attempts to safely extract a number from a JsonElement, falling back to double or string if necessary
699+ private static object ? GetSafeNumber ( JsonElement element , ILogger logger )
700+ {
701+ logger . LogTrace ( "{Method} called" , nameof ( GetSafeNumber ) ) ;
702+
703+ // Try to get as int
704+ if ( element . TryGetInt32 ( out var intValue ) )
705+ {
706+ return intValue ;
707+ }
708+ if ( element . TryGetInt64 ( out var longValue ) )
709+ {
710+ return longValue ;
711+ }
712+ if ( element . TryGetDecimal ( out var decimalValue ) )
713+ {
714+ return decimalValue ;
715+ }
716+ if ( element . TryGetDouble ( out var doubleValue ) )
717+ {
718+ return doubleValue ;
719+ }
720+
721+ // Fallback: return as string to avoid exceptions
722+ return element . GetRawText ( ) ;
723+ }
517724}
0 commit comments