From f8e891aa3e6c73065409f0d73a5067cd89e0e312 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Wed, 19 Nov 2025 15:53:06 +0100 Subject: [PATCH 1/4] Allow treating unsuccessful status code as non-error, avoid setting exception --- .../Extensions/HttpResponseExtensions.cs | 4 ++-- .../Options/ReadOnlyRestClientOptions.cs | 1 - src/RestSharp/Options/RestClientOptions.cs | 6 ++++++ src/RestSharp/Response/RestResponse.cs | 20 +++++++++---------- src/RestSharp/RestClient.Async.cs | 5 ++--- .../RequestFailureTests.cs | 15 ++++++++++++++ 6 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/RestSharp/Extensions/HttpResponseExtensions.cs b/src/RestSharp/Extensions/HttpResponseExtensions.cs index 44497c91b..8cfd52a60 100644 --- a/src/RestSharp/Extensions/HttpResponseExtensions.cs +++ b/src/RestSharp/Extensions/HttpResponseExtensions.cs @@ -18,8 +18,8 @@ namespace RestSharp.Extensions; static class HttpResponseExtensions { - public static Exception? MaybeException(this HttpResponseMessage httpResponse) - => httpResponse.IsSuccessStatusCode + public static Exception? MaybeException(this HttpResponseMessage httpResponse, bool throwOnUnsuccessfulStatusCode) + => httpResponse.IsSuccessStatusCode || !throwOnUnsuccessfulStatusCode ? null #if NET : new HttpRequestException($"Request failed with status code {httpResponse.StatusCode}", null, httpResponse.StatusCode); diff --git a/src/RestSharp/Options/ReadOnlyRestClientOptions.cs b/src/RestSharp/Options/ReadOnlyRestClientOptions.cs index 93951fea6..9ba2331d2 100644 --- a/src/RestSharp/Options/ReadOnlyRestClientOptions.cs +++ b/src/RestSharp/Options/ReadOnlyRestClientOptions.cs @@ -21,7 +21,6 @@ namespace RestSharp; public partial class ReadOnlyRestClientOptions { public IReadOnlyCollection? Interceptors { get; private set; } - // partial void CopyAdditionalProperties(RestClientOptions inner); partial void CopyAdditionalProperties(RestClientOptions inner) => Interceptors = GetInterceptors(inner); static ReadOnlyCollection? GetInterceptors(RestClientOptions? options) { diff --git a/src/RestSharp/Options/RestClientOptions.cs b/src/RestSharp/Options/RestClientOptions.cs index 96ebf9df1..7d3c5d026 100644 --- a/src/RestSharp/Options/RestClientOptions.cs +++ b/src/RestSharp/Options/RestClientOptions.cs @@ -208,6 +208,12 @@ public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(ba /// public bool ThrowOnAnyError { get; set; } + /// + /// When set to false, the client doesn't throw an exception when the response status code is not successful. + /// Default is true. + /// + public bool ErrorWhenUnsuccessfulStatusCode { get; set; } = true; + /// /// Set to true to allow multiple default parameters with the same name. Default is false. /// This setting doesn't apply to headers as multiple header values for the same key is allowed. diff --git a/src/RestSharp/Response/RestResponse.cs b/src/RestSharp/Response/RestResponse.cs index 62b5d569a..65f8c58bc 100644 --- a/src/RestSharp/Response/RestResponse.cs +++ b/src/RestSharp/Response/RestResponse.cs @@ -13,7 +13,6 @@ // limitations under the License. using System.Diagnostics; -using System.Text; using RestSharp.Extensions; // ReSharper disable SuggestBaseTypeForParameter @@ -39,12 +38,11 @@ public partial class RestResponse(RestRequest request) : RestResponse(request [DebuggerDisplay($"{{{nameof(DebuggerDisplay)}()}}")] public class RestResponse(RestRequest request) : RestResponseBase(request) { internal static async Task FromHttpResponse( - HttpResponseMessage httpResponse, - RestRequest request, - Encoding encoding, - CookieCollection? cookieCollection, - CalculateResponseStatus calculateResponseStatus, - CancellationToken cancellationToken + HttpResponseMessage httpResponse, + RestRequest request, + ReadOnlyRestClientOptions options, + CookieCollection? cookieCollection, + CancellationToken cancellationToken ) { return request.AdvancedResponseWriter?.Invoke(httpResponse, request) ?? await GetDefaultResponse().ConfigureAwait(false); @@ -56,7 +54,7 @@ async Task GetDefaultResponse() { #endif var bytes = stream == null ? null : await stream.ReadAsBytes(cancellationToken).ConfigureAwait(false); - var content = bytes == null ? null : await httpResponse.GetResponseString(bytes, encoding); + var content = bytes == null ? null : await httpResponse.GetResponseString(bytes, options.Encoding); return new(request) { Content = content, @@ -65,11 +63,11 @@ async Task GetDefaultResponse() { ContentLength = httpResponse.Content?.Headers.ContentLength, ContentType = httpResponse.Content?.Headers.ContentType?.MediaType, Cookies = cookieCollection, - ErrorException = httpResponse.MaybeException(), + ErrorException = httpResponse.MaybeException(options.ErrorWhenUnsuccessfulStatusCode), Headers = httpResponse.Headers.GetHeaderParameters(), IsSuccessStatusCode = httpResponse.IsSuccessStatusCode, RawBytes = bytes, - ResponseStatus = calculateResponseStatus(httpResponse), + ResponseStatus = options.CalculateResponseStatus(httpResponse), ResponseUri = httpResponse.RequestMessage?.RequestUri, RootElement = request.RootElement, Server = httpResponse.Headers.Server.ToString(), @@ -83,4 +81,4 @@ async Task GetDefaultResponse() { public RestResponse() : this(new()) { } } -public delegate ResponseStatus CalculateResponseStatus(HttpResponseMessage httpResponse); +public delegate ResponseStatus CalculateResponseStatus(HttpResponseMessage httpResponse); \ No newline at end of file diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index d299a9ead..660367435 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -30,9 +30,8 @@ public async Task ExecuteAsync(RestRequest request, CancellationTo ? await RestResponse.FromHttpResponse( internalResponse.ResponseMessage!, request, - Options.Encoding, + Options, internalResponse.CookieContainer?.GetCookies(internalResponse.Url), - Options.CalculateResponseStatus, cancellationToken ) .ConfigureAwait(false) @@ -49,7 +48,7 @@ public async Task ExecuteAsync(RestRequest request, CancellationTo request.CompletionOption = HttpCompletionOption.ResponseHeadersRead; var response = await ExecuteRequestAsync(request, cancellationToken).ConfigureAwait(false); - var exception = response.Exception ?? response.ResponseMessage?.MaybeException(); + var exception = response.Exception ?? response.ResponseMessage?.MaybeException(Options.ErrorWhenUnsuccessfulStatusCode); if (exception != null) { return Options.ThrowOnAnyError ? throw exception : null; diff --git a/test/RestSharp.Tests.Integrated/RequestFailureTests.cs b/test/RestSharp.Tests.Integrated/RequestFailureTests.cs index 7037670c0..f482d5464 100644 --- a/test/RestSharp.Tests.Integrated/RequestFailureTests.cs +++ b/test/RestSharp.Tests.Integrated/RequestFailureTests.cs @@ -21,6 +21,20 @@ public async Task Handles_GET_Request_Errors_With_Response_Type() { var response = await _client.ExecuteAsync(request); response.StatusCode.Should().Be(HttpStatusCode.NotFound); + response.IsSuccessStatusCode.Should().BeFalse(); + response.ErrorException.Should().NotBeNull(); + response.Data.Should().Be(null); + } + + [Fact] + public async Task Does_not_throw_on_unsuccessful_status_code_with_option() { + using var client = new RestClient(new RestClientOptions(server.Url!) { ErrorWhenUnsuccessfulStatusCode = false }); + var request = new RestRequest("status?code=404"); + var response = await client.ExecuteAsync(request); + + response.StatusCode.Should().Be(HttpStatusCode.NotFound); + response.IsSuccessStatusCode.Should().BeFalse(); + response.ErrorException.Should().BeNull(); response.Data.Should().Be(null); } @@ -29,6 +43,7 @@ public async Task Throws_on_unsuccessful_call() { using var client = new RestClient(new RestClientOptions(server.Url!) { ThrowOnAnyError = true }); var request = new RestRequest("status?code=500"); + // ReSharper disable once AccessToDisposedClosure var task = () => client.ExecuteAsync(request); await task.Should().ThrowExactlyAsync(); } From 7519f89d12c871a838e5e415b913054e8a09e6a2 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Wed, 19 Nov 2025 16:12:47 +0100 Subject: [PATCH 2/4] Update src/RestSharp/Options/RestClientOptions.cs Co-authored-by: qodo-merge-for-open-source[bot] <189517486+qodo-merge-for-open-source[bot]@users.noreply.github.com> --- src/RestSharp/Options/RestClientOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/RestSharp/Options/RestClientOptions.cs b/src/RestSharp/Options/RestClientOptions.cs index 7d3c5d026..a2350cedc 100644 --- a/src/RestSharp/Options/RestClientOptions.cs +++ b/src/RestSharp/Options/RestClientOptions.cs @@ -209,10 +209,10 @@ public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(ba public bool ThrowOnAnyError { get; set; } /// - /// When set to false, the client doesn't throw an exception when the response status code is not successful. + /// When set to false, the client doesn't set the `ErrorException` property for responses with unsuccessful status codes. /// Default is true. /// - public bool ErrorWhenUnsuccessfulStatusCode { get; set; } = true; + public bool SetErrorExceptionOnUnsuccessfulStatusCode { get; set; } = true; /// /// Set to true to allow multiple default parameters with the same name. Default is false. From bed88f553bc5f76a154ee3ff93e55a2fa5f51775 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Wed, 19 Nov 2025 16:16:26 +0100 Subject: [PATCH 3/4] Follow up fix --- src/RestSharp/Response/RestResponse.cs | 2 +- src/RestSharp/RestClient.Async.cs | 2 +- test/RestSharp.Tests.Integrated/RequestFailureTests.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/RestSharp/Response/RestResponse.cs b/src/RestSharp/Response/RestResponse.cs index 65f8c58bc..ef08e6bd3 100644 --- a/src/RestSharp/Response/RestResponse.cs +++ b/src/RestSharp/Response/RestResponse.cs @@ -63,7 +63,7 @@ async Task GetDefaultResponse() { ContentLength = httpResponse.Content?.Headers.ContentLength, ContentType = httpResponse.Content?.Headers.ContentType?.MediaType, Cookies = cookieCollection, - ErrorException = httpResponse.MaybeException(options.ErrorWhenUnsuccessfulStatusCode), + ErrorException = httpResponse.MaybeException(options.SetErrorExceptionOnUnsuccessfulStatusCode), Headers = httpResponse.Headers.GetHeaderParameters(), IsSuccessStatusCode = httpResponse.IsSuccessStatusCode, RawBytes = bytes, diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index 660367435..06a43d6f6 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -48,7 +48,7 @@ public async Task ExecuteAsync(RestRequest request, CancellationTo request.CompletionOption = HttpCompletionOption.ResponseHeadersRead; var response = await ExecuteRequestAsync(request, cancellationToken).ConfigureAwait(false); - var exception = response.Exception ?? response.ResponseMessage?.MaybeException(Options.ErrorWhenUnsuccessfulStatusCode); + var exception = response.Exception ?? response.ResponseMessage?.MaybeException(Options.SetErrorExceptionOnUnsuccessfulStatusCode); if (exception != null) { return Options.ThrowOnAnyError ? throw exception : null; diff --git a/test/RestSharp.Tests.Integrated/RequestFailureTests.cs b/test/RestSharp.Tests.Integrated/RequestFailureTests.cs index f482d5464..6702ece2d 100644 --- a/test/RestSharp.Tests.Integrated/RequestFailureTests.cs +++ b/test/RestSharp.Tests.Integrated/RequestFailureTests.cs @@ -28,7 +28,7 @@ public async Task Handles_GET_Request_Errors_With_Response_Type() { [Fact] public async Task Does_not_throw_on_unsuccessful_status_code_with_option() { - using var client = new RestClient(new RestClientOptions(server.Url!) { ErrorWhenUnsuccessfulStatusCode = false }); + using var client = new RestClient(new RestClientOptions(server.Url!) { SetErrorExceptionOnUnsuccessfulStatusCode = false }); var request = new RestRequest("status?code=404"); var response = await client.ExecuteAsync(request); From 8a8f153059d0d3bf364b483afac45f6f9924fcf8 Mon Sep 17 00:00:00 2001 From: Alexey Zimarev Date: Thu, 20 Nov 2025 12:05:55 +0100 Subject: [PATCH 4/4] Update the agents file --- agents.md | 722 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 630 insertions(+), 92 deletions(-) diff --git a/agents.md b/agents.md index b4cdc3a7b..8dffa5fd3 100644 --- a/agents.md +++ b/agents.md @@ -1,124 +1,638 @@ ### RestSharp – Developer Notes (agents.md) #### Scope -This document captures project-specific knowledge to speed up advanced development and maintenance of RestSharp. It focuses on build, configuration, and testing details unique to this repository, plus conventions and tips that help avoid common pitfalls. +This document captures project-specific knowledge to speed up advanced development and maintenance of RestSharp. It focuses on build, configuration, testing details, source generation, and conventions unique to this repository. --- -### Build and configuration +## Solution Structure -- Solution layout - - Root solution: `RestSharp.sln`. - - Library sources in `src/RestSharp` targeting multiple frameworks via shared props. - - Tests live under `test/` and are multi-targeted (see below). +### Projects and Organization -- Multi-targeting - - Tests target: `net48; net8.0; net9.0; net10.0` (defined in `test/Directory.Build.props`). - - .NET Framework 4.8 support is provided via `Microsoft.NETFramework.ReferenceAssemblies.net472` for reference assemblies during build when `TargetFramework == net48`. - - CI uses `actions/setup-dotnet@v4` with `dotnet-version: 9.0.x` for packaging; building tests locally may require multiple SDKs if you intend to run against all TFMs. Practically, you can run on a single installed TFM by overriding `-f`. - - CI for pull requests runs tests against the supported .NET versions (.NET 8, .NET 9, and .NET 10) on Linux and Windows. On Windows, it also runs tests against .NET Framework 4.8. +The solution (`RestSharp.sln`) is organized into the following structure: -- Central props - - `test/Directory.Build.props` imports `props/Common.props` from the repo root. This propagates common settings into all test projects. - - Notable properties: - - `true` and `false` in tests. - - `disable` in tests (be mindful when adding nullable-sensitive code in tests). - - `xUnit1033;CS8002` to quiet specific analyzer warnings. - - Test logs: `VSTestLogger` and `VSTestResultsDirectory` are preconfigured to write TRX per TFM into `test-results/`. +**Core Library:** +- `src/RestSharp/` - Main library targeting multiple frameworks -- Packaging (FYI) - - CI workflow `.github/workflows/build-dev.yml` demonstrates release packaging: `dotnet pack -c Release -o nuget -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg` and `dotnet nuget push` to nuget.org using OIDC via `NuGet/login@v1`. +**Serializer Extensions:** +- `src/RestSharp.Serializers.NewtonsoftJson/` - Newtonsoft.Json serializer +- `src/RestSharp.Serializers.Xml/` - XML serializer +- `src/RestSharp.Serializers.CsvHelper/` - CSV serializer + +**Source Generator:** +- `gen/SourceGenerator/` - Incremental source generator for code generation (see dedicated section below) + +**Test Projects:** +- `test/RestSharp.Tests/` - Core unit tests +- `test/RestSharp.Tests.Integrated/` - Integration tests using WireMock +- `test/RestSharp.Tests.Serializers.Json/` - JSON serializer tests +- `test/RestSharp.Tests.Serializers.Xml/` - XML serializer tests +- `test/RestSharp.Tests.Serializers.Csv/` - CSV serializer tests +- `test/RestSharp.Tests.Shared/` - Shared test utilities +- `test/RestSharp.InteractiveTests/` - Interactive/manual tests + +**Performance:** +- `benchmarks/RestSharp.Benchmarks/` - BenchmarkDotNet performance tests --- -### Testing +## Build and Configuration -- Test frameworks and helpers - - xUnit is the test framework. `test/Directory.Build.props` adds global `using` aliases for `Xunit`, `FluentAssertions`, and `AutoFixture` so you can use `[Fact]`, `Should()`, etc., without explicit `using` statements in each test file. - - Additional packages commonly used in unit tests (see `test/RestSharp.Tests/RestSharp.Tests.csproj`): `Moq`, `RichardSzalay.MockHttp`, `System.Net.Http.Json`. - - Integrated tests leverage `WireMockServer` (see `RestSharp.Tests.Integrated`) and use assets under `Assets/` where needed. +### Multi-Targeting -- Running tests - - Run all tests for the entire solution: - ``` - dotnet test RestSharp.sln -c Debug - ``` - - Run a specific test project (multi-targeted will run for all installed TFMs): - ``` - dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj - ``` - - Select a single target framework (useful if you don’t have all SDKs installed): - ``` - dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj -f net8.0 - ``` - - Run by fully-qualified name (FQN) — recommended for pinpointing a single test: - ``` - dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj \ - --filter "FullyQualifiedName=RestSharp.Tests.UrlBuilderTests_Get.Should_build_url_with_query" - ``` - Notes: - - Prefer `FullyQualifiedName` for precision. Class and method names are case-sensitive. - - You can combine with `-f net8.0` to avoid cross-TFM failures when only one SDK is present. - - Run by namespace or class: - ``` - dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj --filter "RestSharp.Tests.UrlBuilderTests" - ``` +**Library Targets** (`src/Directory.Build.props`): +- `netstandard2.0` - .NET Standard 2.0 for broad compatibility +- `net471` - .NET Framework 4.7.1 +- `net48` - .NET Framework 4.8 +- `net8.0` - .NET 8 +- `net9.0` - .NET 9 +- `net10.0` - .NET 10 -- Logs and results - - TRX logs are written per TFM into `test-results//` with file name `.trx` as configured by `VSTestLogger`/`VSTestResultsDirectory` in `Directory.Build.props`. - - To additionally emit console diagnostics: - ``` - dotnet test -v n - ``` +**Test Targets** (`test/Directory.Build.props`): +- `net48` - .NET Framework 4.8 (Windows only) +- `net8.0` - .NET 8 +- `net9.0` - .NET 9 +- `net10.0` - .NET 10 -- Code coverage - - The `coverlet.collector` package is referenced for data-collector based coverage. - - Example coverage run (generates cobertura xml): - ``` - dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj \ - -f net8.0 \ - --collect:"XPlat Code Coverage" \ - -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura - ``` - - Output will be placed under the test results directory for the given run. +**Source Generator Target:** +- `netstandard2.0` - Required for source generators to work across all compiler versions + +### Build Properties Hierarchy + +The build system uses a hierarchical props structure: + +1. **`props/Common.props`** - Root properties imported by all projects: + - Sets `RepoRoot` variable + - Configures assembly signing (`RestSharp.snk`) + - Sets `LangVersion=preview` and `ImplicitUsings=enable` + - Enables nullable reference types (`Nullable=enable`) + - Adds global `using System.Net.Http;` + +2. **`src/Directory.Build.props`** - Source project properties: + - Imports `Common.props` + - Defines multi-targeting for libraries + - Configures NuGet package metadata (icon, license, description) + - Enables SourceLink for debugging + - Uses MinVer for versioning + - Conditionally adds polyfills for older frameworks + - Generates XML documentation files + +3. **`test/Directory.Build.props`** - Test project properties: + - Imports `Common.props` + - Sets `IsTestProject=true` and `IsPackable=false` + - Configures test result output: `test-results//.trx` + - Disables nullable (`Nullable=disable` for tests) + - Adds global usings for xUnit, FluentAssertions, AutoFixture + - Suppresses warnings: `xUnit1033`, `CS8002` + +4. **`Directory.Packages.props`** - Central Package Management: + - All package versions defined centrally + - TFM-specific version overrides (e.g., `System.Text.Json` for .NET 10) + - Separate sections for runtime, compile, and testing dependencies + +### Framework-Specific Considerations + +**Legacy Framework Support (.NET Framework 4.7.1/4.8, netstandard2.0):** +- `System.Text.Json` is added as a package reference (newer frameworks have it built-in) +- Polyfills are enabled via `Nullable` package for nullable reference type attributes +- Reference assemblies provided by `Microsoft.NETFramework.ReferenceAssemblies.net472` + +**Modern .NET (8/9/10):** +- Native support for most features +- Conditional compilation using `#if NET8_0_OR_GREATER` +- Platform-specific attributes like `[UnsupportedOSPlatform("browser")]` + +### Assembly Signing + +All assemblies are strong-named using `RestSharp.snk` (configured in `Common.props`). + +--- + +## Source Generator + +RestSharp includes a custom incremental source generator located in `gen/SourceGenerator/` that automates boilerplate code generation. + +### Generator Architecture + +**Project Configuration:** +- Targets: `netstandard2.0` (required for source generators) +- Language: C# preview features enabled +- Output: Not included in build output (`IncludeBuildOutput=false`) +- Analyzer rules: Extended analyzer rules enforced +- Referenced as analyzer in main project: `OutputItemType="Analyzer"` + +**Dependencies:** +- `Microsoft.CodeAnalysis.Analyzers` - Analyzer SDK +- `Microsoft.CodeAnalysis.CSharp` - Roslyn C# APIs + +**Global Usings:** +```csharp +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +``` + +### Generator Components + +#### 1. ImmutableGenerator (`ImmutableGenerator.cs`) + +**Purpose:** Generates immutable (read-only) wrapper classes from mutable classes. + +**Trigger Attribute:** `[GenerateImmutable]` + +**How It Works:** +1. Scans compilation for classes annotated with `[GenerateImmutable]` +2. Identifies all properties with `set` accessors (excluding those marked with `[Exclude]`) +3. Generates a `ReadOnly{ClassName}` partial class with: + - Read-only properties (getters only) + - Constructor accepting the mutable class instance + - Partial method `CopyAdditionalProperties` for extensibility + - Preserves XML documentation comments + +**Example Usage:** +```csharp +[GenerateImmutable] +public class RestClientOptions { + public Uri? BaseUrl { get; set; } + public string? UserAgent { get; set; } + [Exclude] // This property won't be in the immutable version + public List Interceptors { get; set; } +} +``` + +**Generated Output:** `ReadOnlyRestClientOptions.cs` with immutable properties and a constructor that copies values from `RestClientOptions`. + +**Location:** Generated files appear in `obj///generated/SourceGenerator/SourceGenerator.ImmutableGenerator/` + +#### 2. InheritedCloneGenerator (`InheritedCloneGenerator.cs`) + +**Purpose:** Generates static factory methods to clone objects from base types to derived types. + +**Trigger Attribute:** `[GenerateClone(BaseType = typeof(BaseClass), Name = "MethodName")]` + +**How It Works:** +1. Finds classes with `[GenerateClone]` attribute +2. Extracts `BaseType` and `Name` from attribute parameters +3. Analyzes properties from the base type and its inheritance chain +4. Generates a static factory method that: + - Takes the base type as parameter + - Creates a new instance of the derived type + - Copies all properties from base to derived + - Uses constructor parameters where applicable + +**Example Usage:** +```csharp +[GenerateClone(BaseType = typeof(RestResponse), Name = "FromResponse")] +public partial class RestResponse : RestResponse { + public T? Data { get; set; } +} +``` + +**Generated Output:** `RestResponse.Clone.g.cs` with a static `FromResponse` method that creates `RestResponse` from `RestResponse`. + +**Location:** Generated files appear in `obj///generated/SourceGenerator/SourceGenerator.InheritedCloneGenerator/` + +#### 3. Extensions (`Extensions.cs`) + +**Purpose:** Helper extension methods for the generators using C# extension types. + +**Key Methods:** +- `FindClasses(predicate)` - Finds classes matching a predicate across all syntax trees +- `FindAnnotatedClasses(attributeName, strict)` - Finds classes with specific attributes +- `GetBaseTypesAndThis()` - Traverses type hierarchy to get all base types + +### Attribute Definitions + +Located in `src/RestSharp/Extensions/GenerateImmutableAttribute.cs`: + +```csharp +[AttributeUsage(AttributeTargets.Class)] +class GenerateImmutableAttribute : Attribute; + +[AttributeUsage(AttributeTargets.Class)] +class GenerateCloneAttribute : Attribute { + public Type? BaseType { get; set; } + public string? Name { get; set; } +} + +[AttributeUsage(AttributeTargets.Property)] +class Exclude : Attribute; // Excludes properties from immutable generation +``` + +### Integration with Main Project + +In `src/RestSharp/RestSharp.csproj`: +```xml + + true + + + + + +``` + +### Debugging Generated Code + +Generated files are emitted to the `obj` directory when `EmitCompilerGeneratedFiles=true`. To view: +```bash +# Example path for net8.0 Debug build +ls src/RestSharp/obj/Debug/net8.0/generated/SourceGenerator/ +``` + +--- + +## Testing + +### Test Framework and Helpers + +**Primary Framework:** xUnit + +**Assertion Library:** FluentAssertions + +**Test Data:** AutoFixture + +**Mocking:** +- `Moq` - General mocking +- `RichardSzalay.MockHttp` - HTTP message handler mocking +- `WireMock.Net` - HTTP server mocking for integration tests + +**Global Usings** (configured in `test/Directory.Build.props`): +```csharp +using Xunit; +using Xunit.Abstractions; +using FluentAssertions; +using FluentAssertions.Extensions; +using AutoFixture; +``` + +These are automatically available in all test files without explicit `using` statements. + +### Test Project Organization + +**Unit Tests (`RestSharp.Tests`):** +- Tests for core functionality +- Uses mocking for HTTP interactions +- Example: `UrlBuilderTests`, `ObjectParserTests` +- Organized with partial classes for large test suites (e.g., `UrlBuilderTests.Get.cs`, `UrlBuilderTests.Post.cs`) + +**Integration Tests (`RestSharp.Tests.Integrated`):** +- Uses `WireMockServer` for realistic HTTP scenarios +- Tests actual HTTP behavior without external dependencies +- Example: `DownloadFileTests` spins up WireMock server in constructor, disposes in `IDisposable.Dispose` +- Asset files stored in `Assets/` directory + +**Serializer Tests:** +- Separate projects for each serializer (JSON, XML, CSV) +- Test serialization/deserialization behavior + +### Running Tests + +**All tests for entire solution:** +```bash +dotnet test RestSharp.sln -c Debug +``` + +**Specific test project:** +```bash +dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj +``` + +**Single target framework:** +```bash +dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj -f net8.0 +``` + +**Single test by fully-qualified name (recommended for precision):** +```bash +dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj \ + --filter "FullyQualifiedName=RestSharp.Tests.UrlBuilderTests_Get.Should_build_url_with_query" \ + -f net8.0 +``` + +**Filter by namespace or class:** +```bash +dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj \ + --filter "RestSharp.Tests.UrlBuilderTests" +``` + +**With verbose output:** +```bash +dotnet test -v n +``` + +### Test Results and Logging + +**Output Location:** `test-results//.trx` + +**Configuration** (in `test/Directory.Build.props`): +```xml +trx%3bLogFileName=$(MSBuildProjectName).trx +$(RepoRoot)/test-results/$(TargetFramework) +``` + +Results are written per target framework, making it easy to identify TFM-specific failures. + +### Code Coverage + +**Tool:** coverlet.collector (data-collector based) + +**Generate coverage report:** +```bash +dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj \ + -f net8.0 \ + --collect:"XPlat Code Coverage" \ + -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura +``` + +Coverage output is placed in the test results directory. + +### Adding New Tests + +**Best Practices:** +1. Co-locate tests by feature area +2. Use partial classes for large test suites (link via `` in `.csproj`) +3. For HTTP tests, prefer `WireMockServer` over live endpoints +4. Use FluentAssertions for readable assertions: `result.Should().Be(expected)` +5. Avoid time-sensitive or locale-sensitive assertions; pin formats when needed +6. Use `#if NET8_0_OR_GREATER` for TFM-specific APIs + +**Example Test Structure:** +```csharp +public class MyFeatureTests { + [Fact] + public void Should_do_something() { + // Arrange + var fixture = new Fixture(); + var input = fixture.Create(); + + // Act + var result = MyFeature.Process(input); + + // Assert + result.Should().NotBeNull(); + } +} +``` + +--- + +## Continuous Integration + +### CI Workflows + +**Location:** `.github/workflows/` + +#### 1. Pull Request Workflow (`pull-request.yml`) + +**Triggers:** Pull requests (excluding `docs/**` changes) + +**Test Matrix:** +- **Windows:** Tests against `net48`, `net8.0`, `net9.0`, `net10.0` +- **Linux:** Tests against `net8.0`, `net9.0`, `net10.0` (no .NET Framework) + +**SDK Setup:** +```yaml +dotnet-version: | + 8.0.x + 9.0.x + 10.0.x +``` + +**Test Command:** +```bash +dotnet test -c Debug -f ${{ matrix.dotnet }} +``` + +**Artifacts:** Test results uploaded for each TFM and OS combination + +#### 2. Build and Deploy Workflow (`build-dev.yml`) + +**Triggers:** +- Push to `dev` branch +- Tags (for releases) + +**SDK:** .NET 10.0.x (for packaging) + +**Steps:** +1. Checkout with full history (`git fetch --prune --unshallow` for MinVer) +2. NuGet login using OIDC (`NuGet/login@v1`) +3. Pack: `dotnet pack -c Release -o nuget -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg` +4. Push to NuGet.org with `--skip-duplicate` + +**Permissions:** Requires `id-token: write` for OIDC authentication + +#### 3. Test Results Workflow (`test-results.yml`) + +Publishes test results as GitHub checks. + +### Local CI Simulation + +To replicate CI behavior locally: + +**Windows (all TFMs):** +```bash +dotnet test -c Debug -f net48 +dotnet test -c Debug -f net8.0 +dotnet test -c Debug -f net9.0 +dotnet test -c Debug -f net10.0 +``` + +**Linux/macOS (no .NET Framework):** +```bash +dotnet test -c Debug -f net8.0 +dotnet test -c Debug -f net9.0 +dotnet test -c Debug -f net10.0 +``` + +--- + +## Versioning and Packaging + +### Versioning Strategy + +**Tool:** MinVer (Git-based semantic versioning) + +**Configuration** (in `src/Directory.Build.props`): +```xml + +``` + +**Custom Version Target:** +```xml + + + $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) + $(MinVerMajor).$(MinVerMinor).$(MinVerPatch) + + +``` + +Version is determined from Git tags and commit history. Requires unshallow clone for accurate versioning. + +### Package Configuration + +**NuGet Metadata:** +- Icon: `restsharp.png` +- License: Apache-2.0 +- Project URL: https://restsharp.dev +- Repository: https://github.com/restsharp/RestSharp.git +- README: Included in package -- Adding new tests - - New xUnit test files can be added anywhere under the corresponding test project directory; no extra `using` directives are required for `Xunit`/`FluentAssertions`/`AutoFixture` thanks to `Directory.Build.props` implicit usings. - - Prefer co-locating tests by feature area and splitting large suites using partial classes (see `UrlBuilderTests.cs` with `UrlBuilderTests.Get.cs`/`Post.cs` linked via `DependentUpon` in the project) to keep navigation manageable. - - For HTTP behavior, use `WireMockServer` in integrated tests rather than live endpoints. See `test/RestSharp.Tests.Integrated/DownloadFileTests.cs` for a pattern: spin up a server, register expectations in the constructor, and dispose in `IDisposable.Dispose`. - - Follow existing assertions style with FluentAssertions. +**Symbol Packages:** `.snupkg` format for debugging -- Verified example run - - The test infrastructure was validated by executing a trivial `[Fact]` via fully qualified name using the built-in test runner. Use the FQN filtering example above to replicate. +**SourceLink:** Enabled for source debugging + +### Local Packaging + +```bash +dotnet pack src/RestSharp/RestSharp.csproj -c Release -o nuget \ + -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg +``` + +Output: `nuget/RestSharp..nupkg` and `RestSharp..snupkg` --- -### Additional development information +## Code Organization and Conventions + +### File Organization + +**Partial Classes:** Large classes are split using partial classes with `` in `.csproj`: +```xml + + RestClient.cs + +``` + +Examples: +- `RestClient.cs` with `RestClient.Async.cs`, `RestClient.Extensions.*.cs` +- `PropertyCache.cs` with `PropertyCache.Populator.cs`, `PropertyCache.Populator.RequestProperty.cs` -- Code style and analyzers - - Adhere to the style used in `src/RestSharp` and existing tests. Test projects disable nullable by default; the main library might have different settings (check the respective `*.csproj` and imported props). - - The repo uses central package management via `Directory.Packages.props`. Prefer bumping versions there unless a project has a specific override. +### Code Style -- HTTP/integration test guidance - - Use `WireMockServer` for predictable, offline tests. Avoid time-sensitive or locale-sensitive assertions in integrated tests; when needed, pin formats (e.g., `"d"`) as seen in `ObjectParserTests`. - - Be explicit about stream usage across TFMs. Some tests use `#if NET8_0_OR_GREATER` to select APIs like `Stream.ReadExactly`. +- `.editorconfig` is used for code formatting and style rules +- All source files in `/src` must have a license header: + ```text + // Copyright (c) .NET Foundation and Contributors + // + // Licensed under the Apache License, Version 2.0 (the "License"); + // you may not use this file except in compliance with the License. + // You may obtain a copy of the License at + // + // http://www.apache.org/licenses/LICENSE-2.0 + // + // Unless required by applicable law or agreed to in writing, software + // distributed under the License is distributed on an "AS IS" BASIS, + // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + // See the License for the specific language governing permissions and + // limitations under the License. + // + // Adapted from Rebus + ``` +- Test files (all projects located in `/test`) don't need the license header -- Multi-TFM nuances - - When debugging TFM-specific behavior, run `dotnet test -f ` to reproduce. Conditional compilation symbols (e.g., `NET8_0_OR_GREATER`) are used in tests; ensure your changes compile under all declared target frameworks or scope them with `#if`. +**Nullable Reference Types:** +- Enabled in source projects (`Nullable=enable`) +- Disabled in test projects (`Nullable=disable`) -- Artifacts and outputs - - NuGet packages are output to `nuget/` during local `dotnet pack` unless overridden. - - Test artifacts are collected under `test-results//` per the configuration. +**Language Version:** `preview` - allows use of latest C# features -- Common pitfalls - - Running tests targeting `net48` on non-Windows environments requires the reference assemblies (already pulled by package reference) but still may need Mono/compat setup on some systems; if unavailable, skip with `-f`. - - Some integrated tests rely on asset files under `Assets/`. Ensure `AppDomain.CurrentDomain.BaseDirectory` resolves correctly when running from IDE vs CLI. +**Implicit Usings:** Enabled globally + +**Warnings:** +- XML documentation warnings suppressed in source (`NoWarn=1591`) +- Test-specific warnings suppressed (`xUnit1033`, `CS8002`) + +### Platform-Specific Code + +Use conditional compilation and attributes: + +```csharp +#if NET +[UnsupportedOSPlatform("browser")] +#endif +public ICredentials? Credentials { get; set; } +``` + +```csharp +#if NET8_0_OR_GREATER +await using var stream = ... +#else +using var stream = ... +#endif +``` --- -### Quick commands reference +## Common Development Tasks + +### Building the Solution + +**Debug build:** +```bash +dotnet build RestSharp.sln -c Debug +``` +**Release build:** +```bash +dotnet build RestSharp.sln -c Release ``` + +### Working with Source Generator + +**View generated files:** +```bash +# After building +find src/RestSharp/obj -name "*.g.cs" -o -name "ReadOnly*.cs" +``` + +**Debug generator:** +1. Set `true` in project +2. Build project +3. Check `obj///generated/SourceGenerator/` + +**Add new generator:** +1. Create new class implementing `IIncrementalGenerator` +2. Add `[Generator(LanguageNames.CSharp)]` attribute +3. Implement `Initialize` method +4. Register source output + +### Multi-TFM Development + +**Build for specific TFM:** +```bash +dotnet build src/RestSharp/RestSharp.csproj -f net8.0 +``` + +**Check TFM-specific behavior:** +- Use `#if` directives for conditional compilation +- Test against all supported TFMs before committing +- Be aware of API differences (e.g., `Stream.ReadExactly` in .NET 8+) + +### Troubleshooting + +**Issue:** Tests fail on specific TFM +- **Solution:** Run with `-f ` to isolate, check for TFM-specific APIs + +**Issue:** Source generator not running +- **Solution:** Clean and rebuild, check `EmitCompilerGeneratedFiles` setting + +**Issue:** .NET Framework tests fail on non-Windows +- **Solution:** Expected behavior; run with `-f net8.0` or higher on Linux/macOS + +**Issue:** MinVer version incorrect +- **Solution:** Ensure full Git history with `git fetch --prune --unshallow` + +--- + +## Quick Reference Commands + +```bash # Build solution dotnet build RestSharp.sln -c Release @@ -126,8 +640,32 @@ dotnet build RestSharp.sln -c Release dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj -f net8.0 # Run a single test by FQN -dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj --filter "FullyQualifiedName=RestSharp.Tests.ObjectParserTests.ShouldUseRequestProperty" +dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj \ + --filter "FullyQualifiedName=RestSharp.Tests.ObjectParserTests.ShouldUseRequestProperty" \ + -f net8.0 + +# Pack locally +dotnet pack src/RestSharp/RestSharp.csproj -c Release -o nuget \ + -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -# Pack (local) -dotnet pack src/RestSharp/RestSharp.csproj -c Release -o nuget -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg +# Generate code coverage +dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj -f net8.0 \ + --collect:"XPlat Code Coverage" \ + -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura + +# View generated source files +find src/RestSharp/obj/Debug -name "*.g.cs" -o -name "ReadOnly*.cs" + +# Clean all build artifacts +dotnet clean RestSharp.sln +rm -rf src/*/bin src/*/obj test/*/bin test/*/obj gen/*/bin gen/*/obj ``` + +--- + +## Additional Resources + +- **Main Documentation:** https://restsharp.dev +- **Repository:** https://github.com/restsharp/RestSharp +- **NuGet Package:** https://www.nuget.org/packages/RestSharp +- **License:** Apache-2.0