diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 92807d7e3..12eb9b98d 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -37,7 +37,13 @@ jobs: 9.0.x 10.0.x - name: Run tests - run: dotnet test -c Debug -f ${{ matrix.dotnet }} + run: | + dotnet test test/RestSharp.Tests -c Debug -f ${{ matrix.dotnet }} + dotnet test test/RestSharp.Tests.Integrated -c Debug -f ${{ matrix.dotnet }} + dotnet test test/RestSharp.Tests.Serializers.Json -c Debug -f ${{ matrix.dotnet }} + dotnet test test/RestSharp.Tests.Serializers.Xml -c Debug -f ${{ matrix.dotnet }} + dotnet test test/RestSharp.Tests.Serializers.Csv -c Debug -f ${{ matrix.dotnet }} + dotnet test test/RestSharp.Tests.DependencyInjection -c Debug -f ${{ matrix.dotnet }} - name: Upload Test Results if: always() uses: actions/upload-artifact@v6 @@ -65,7 +71,13 @@ jobs: 9.0.x 10.0.x - name: Run tests - run: dotnet test -f ${{ matrix.dotnet }} + run: | + dotnet test test/RestSharp.Tests -f ${{ matrix.dotnet }} + dotnet test test/RestSharp.Tests.Integrated -f ${{ matrix.dotnet }} + dotnet test test/RestSharp.Tests.Serializers.Json -f ${{ matrix.dotnet }} + dotnet test test/RestSharp.Tests.Serializers.Xml -f ${{ matrix.dotnet }} + dotnet test test/RestSharp.Tests.Serializers.Csv -f ${{ matrix.dotnet }} + dotnet test test/RestSharp.Tests.DependencyInjection -f ${{ matrix.dotnet }} - name: Upload Test Results if: always() uses: actions/upload-artifact@v6 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..615e58179 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,2 @@ +## MCP Servers Available +- mem0: Use this AI memory for storing and retrieving long-term context as well as short-term context \ No newline at end of file diff --git a/RestSharp.slnx b/RestSharp.slnx new file mode 100644 index 000000000..d06f32574 --- /dev/null +++ b/RestSharp.slnx @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/docs/usage/response.md b/docs/docs/usage/response.md index dbaf302b4..43745ec39 100644 --- a/docs/docs/usage/response.md +++ b/docs/docs/usage/response.md @@ -28,6 +28,19 @@ Response object contains the following properties: | `ErrorException` | `Exception?` | Exception thrown when executing the request, if any. | | `Version` | `Version?` | HTTP protocol version of the request. | | `RootElement` | `string?` | Root element of the serialized response content, only works if deserializer supports it. | +| `MergedParameters` | `ParametersCollection` | Combined view of request parameters and client default parameters at execution time. | + +### Merged parameters + +The `MergedParameters` property provides a combined view of the request's own parameters and the client's [default parameters](request.md#request-headers) as they were at execution time. This is useful for logging or debugging the full set of parameters that were applied to a request, since `Request.Parameters` only contains the parameters added directly to the request. + +```csharp +var response = await client.ExecuteAsync(request); + +foreach (var param in response.MergedParameters) { + Console.WriteLine($"{param.Name} = {param.Value} ({param.Type})"); +} +``` In addition, `RestResponse` has one additional property: diff --git a/src/RestSharp/Response/RestResponseBase.cs b/src/RestSharp/Response/RestResponseBase.cs index 383895fb4..b54ed4e17 100644 --- a/src/RestSharp/Response/RestResponseBase.cs +++ b/src/RestSharp/Response/RestResponseBase.cs @@ -132,6 +132,12 @@ protected RestResponseBase(RestRequest request) { /// public Version? Version { get; set; } + /// + /// Combined view of request parameters and client default parameters as they were at execution time. + /// Use this to inspect the full set of parameters that were applied to the request. + /// + public ParametersCollection MergedParameters { get; internal set; } = new RequestParameters(); + /// /// Root element of the serialized response content, only works if deserializer supports it /// diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index df4558795..a6142d07a 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -36,6 +36,7 @@ public async Task ExecuteAsync(RestRequest request, CancellationTo ) .ConfigureAwait(false) : GetErrorResponse(request, internalResponse.Exception, internalResponse.TimeoutToken); + response.MergedParameters = new RequestParameters(request.Parameters.Union(DefaultParameters)); await OnAfterRequest(response, cancellationToken).ConfigureAwait(false); return Options.ThrowOnAnyError ? response.ThrowIfError() : response; diff --git a/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs b/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs index c2038f6a3..da058b238 100644 --- a/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs +++ b/test/RestSharp.Tests.Integrated/DefaultParameterTests.cs @@ -4,7 +4,8 @@ namespace RestSharp.Tests.Integrated; public sealed class DefaultParameterTests(WireMockTestServer server) : IClassFixture { - readonly RequestBodyCapturer _capturer = server.ConfigureBodyCapturer(Method.Get, false); + readonly RequestBodyCapturer _capturer = server.ConfigureBodyCapturer(Method.Get, false); + readonly RequestBodyCapturer _capturerOnPath = server.ConfigureBodyCapturer(Method.Get); [Fact] public async Task Should_add_default_and_request_query_get_parameters() { @@ -48,4 +49,74 @@ public async Task Should_not_encode_pipe_character_when_encode_is_false() { var query = _capturer.RawUrl.Split('?')[1]; query.Should().Contain("ids=in:001|116"); } + + [Fact] + public async Task Should_include_multiple_default_query_params_with_same_name() { + using var client = new RestClient( + new RestClientOptions(server.Url!) { AllowMultipleDefaultParametersWithSameName = true } + ); + client.AddDefaultParameter("filter", "active", ParameterType.QueryString); + client.AddDefaultParameter("filter", "verified", ParameterType.QueryString); + + var request = new RestRequest("capture"); + await client.GetAsync(request); + + var query = _capturerOnPath.Url!.Query; + query.Should().Contain("filter=active"); + query.Should().Contain("filter=verified"); + } + + [Fact] + public async Task Should_include_default_query_params_in_BuildUriString_without_executing() { + using var client = new RestClient(server.Url!); + client.AddDefaultParameter("foo", "bar", ParameterType.QueryString); + + var request = new RestRequest("resource"); + var uri = client.BuildUriString(request); + + uri.Should().Contain("foo=bar"); + } + + [Fact] + public async Task Should_not_permanently_mutate_request_parameters_after_execute() { + using var client = new RestClient(server.Url!); + client.AddDefaultParameter("default_key", "default_val", ParameterType.QueryString); + + var request = new RestRequest("capture"); + var paramsBefore = request.Parameters.Count; + + await client.GetAsync(request); + + // Request parameters should not have been mutated by the execution. + request.Parameters.Count.Should().Be(paramsBefore); + + // Now replace the default parameter with a different value. + client.DefaultParameters.ReplaceParameter(new QueryParameter("default_key", "updated_val")); + + await client.GetAsync(request); + + // The second execution should use the updated default value, not the stale one. + var query = _capturerOnPath.Url!.Query; + query.Should().Contain("default_key=updated_val"); + query.Should().NotContain("default_key=default_val"); + } + + [Fact] + public async Task Should_include_default_params_in_merged_parameters_on_response() { + using var client = new RestClient(server.Url!); + client.AddDefaultParameter("default_key", "default_val", ParameterType.QueryString); + + var request = new RestRequest("capture").AddQueryParameter("req_key", "req_val"); + var response = await client.ExecuteAsync(request); + + var defaultParam = response.MergedParameters + .FirstOrDefault(p => p.Name == "default_key" && p.Type == ParameterType.QueryString); + defaultParam.Should().NotBeNull(); + defaultParam!.Value.Should().Be("default_val"); + + var requestParam = response.MergedParameters + .FirstOrDefault(p => p.Name == "req_key" && p.Type == ParameterType.QueryString); + requestParam.Should().NotBeNull(); + requestParam!.Value.Should().Be("req_val"); + } } \ No newline at end of file diff --git a/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs b/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs index 559c6435a..2a0a60084 100644 --- a/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs +++ b/test/RestSharp.Tests.Integrated/HttpHeadersTests.cs @@ -68,6 +68,25 @@ public async Task Should_sent_custom_UserAgent() { response.GetHeaderValue("Server").Should().Be("Kestrel"); } + [Fact] + public async Task Default_headers_should_appear_in_response_merged_parameters() { + const string headerName = "X-Custom-Default"; + const string headerValue = "DefaultValue123"; + + _client.AddDefaultHeader(headerName, headerValue); + + var request = new RestRequest("/headers"); + var response = await _client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + + var param = response.MergedParameters + .FirstOrDefault(p => p.Name == headerName && p.Type == ParameterType.HttpHeader); + + param.Should().NotBeNull(); + param!.Value.Should().Be(headerValue); + } + static void CheckHeader(RestResponse response, Header header) { var h = FindHeader(response, header.Name); h.Should().NotBeNull();