@@ -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,192 @@ 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 < dynamic > ( 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 , dynamic 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 , dynamic 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 element . GetDecimal ( ) ;
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 // Return type must be object because we can return any type from the request
611+ private static object ? ReplacePlaceholderInString ( string value , dynamic requestBody , ILogger logger )
612+ #pragma warning restore CA1859
613+ {
614+ logger . LogTrace ( "{Method} called" , nameof ( ReplacePlaceholderInString ) ) ;
615+
616+ logger . LogDebug ( "Processing value: {Value}" , value ) ;
617+
618+ // Check if the value starts with @request.body.
619+ if ( ! value . StartsWith ( "@request.body." , StringComparison . OrdinalIgnoreCase ) )
620+ {
621+ logger . LogDebug ( "Value {Value} does not start with @request.body. Skipping" , value ) ;
622+ return value ;
623+ }
624+
625+ // Extract the property path after @request.body.
626+ var propertyPath = value [ "@request.body." . Length ..] ;
627+
628+ logger . LogDebug ( "Extracted property path: {PropertyPath}" , propertyPath ) ;
629+
630+ return GetValueFromRequestBody ( requestBody , propertyPath , logger ) ;
631+ }
632+
633+ private static object ? GetValueFromRequestBody ( dynamic requestBody , string propertyPath , ILogger logger )
634+ {
635+ logger . LogTrace ( "{Method} called" , nameof ( GetValueFromRequestBody ) ) ;
636+
637+ logger . LogDebug ( "Getting value for {PropertyPath}" , propertyPath ) ;
638+
639+ try
640+ {
641+ // Split the property path by dots to handle nested properties
642+ var propertyNames = propertyPath . Split ( '.' ) ;
643+
644+ // Handle JsonElement
645+ if ( requestBody is JsonElement element )
646+ {
647+ return GetNestedValueFromJsonElement ( element , propertyNames , logger ) ;
648+ }
649+ else
650+ {
651+ // Handle other dynamic types by converting to JsonElement
652+ var json = JsonSerializer . Serialize ( requestBody ) ;
653+ var jsonElement = JsonSerializer . Deserialize < JsonElement > ( json ) ;
654+ return GetNestedValueFromJsonElement ( jsonElement , propertyNames , logger ) ;
655+ }
656+ }
657+ catch
658+ {
659+ // If we can't get the property, return null
660+ }
661+
662+ return null ;
663+ }
664+
665+ private static object ? GetNestedValueFromJsonElement ( JsonElement element , string [ ] propertyNames , ILogger logger )
666+ {
667+ logger . LogTrace ( "{Method} called" , nameof ( GetNestedValueFromJsonElement ) ) ;
668+
669+ var current = element ;
670+
671+ // Navigate through the nested properties
672+ foreach ( var propertyName in propertyNames )
673+ {
674+ if ( current . ValueKind != JsonValueKind . Object )
675+ {
676+ logger . LogDebug ( "Current JSON element is not an object. Cannot navigate to property {PropertyName}" , propertyName ) ;
677+ return null ; // Can't navigate further if current element is not an object
678+ }
679+
680+ if ( ! current . TryGetProperty ( propertyName , out current ) )
681+ {
682+ logger . LogDebug ( "Property {PropertyName} not found in JSON. Returning null" , propertyName ) ;
683+ return null ; // Property not found
684+ }
685+ }
686+
687+ return ConvertJsonElementToObject ( current , logger ) ;
688+ }
689+
690+ private static object ? ConvertJsonElementToObject ( JsonElement element , ILogger logger )
691+ {
692+ logger . LogTrace ( "{Method} called" , nameof ( ConvertJsonElementToObject ) ) ;
693+
694+ return element . ValueKind switch
695+ {
696+ JsonValueKind . String => element . GetString ( ) ,
697+ JsonValueKind . Number => element . GetDecimal ( ) ,
698+ JsonValueKind . True => true ,
699+ JsonValueKind . False => false ,
700+ JsonValueKind . Null or JsonValueKind . Undefined => null ,
701+ // For complex objects/arrays, return the JsonElement itself
702+ // which can be serialized later
703+ JsonValueKind . Object or JsonValueKind . Array => element ,
704+ _ => element . ToString ( ) ,
705+ } ;
706+ }
517707}
0 commit comments