agents.md
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.
The solution (RestSharp.slnx) is organized into the following structure:
Core Library:
src/RestSharp/ - Main library targeting multiple frameworksSerializer Extensions:
src/RestSharp.Serializers.NewtonsoftJson/ - Newtonsoft.Json serializersrc/RestSharp.Serializers.Xml/ - XML serializersrc/RestSharp.Serializers.CsvHelper/ - CSV serializerSource Generator:
gen/SourceGenerator/ - Incremental source generator for code generation (see dedicated section below)Test Projects:
test/RestSharp.Tests/ - Core unit teststest/RestSharp.Tests.Integrated/ - Integration tests using WireMocktest/RestSharp.Tests.Serializers.Json/ - JSON serializer teststest/RestSharp.Tests.Serializers.Xml/ - XML serializer teststest/RestSharp.Tests.Serializers.Csv/ - CSV serializer teststest/RestSharp.Tests.Shared/ - Shared test utilitiestest/RestSharp.InteractiveTests/ - Interactive/manual testsPerformance:
benchmarks/RestSharp.Benchmarks/ - BenchmarkDotNet performance testsLibrary Targets (src/Directory.Build.props):
netstandard2.0 - .NET Standard 2.0 for broad compatibilitynet471 - .NET Framework 4.7.1net48 - .NET Framework 4.8net8.0 - .NET 8net9.0 - .NET 9net10.0 - .NET 10Test Targets (test/Directory.Build.props):
net48 - .NET Framework 4.8 (Windows only)net8.0 - .NET 8net9.0 - .NET 9net10.0 - .NET 10Source Generator Target:
netstandard2.0 - Required for source generators to work across all compiler versionsThe build system uses a hierarchical props structure:
props/Common.props - Root properties imported by all projects:
RepoRoot variableRestSharp.snk)LangVersion=preview and ImplicitUsings=enableNullable=enable)using System.Net.Http;src/Directory.Build.props - Source project properties:
Common.propstest/Directory.Build.props - Test project properties:
Common.propsIsTestProject=true and IsPackable=falsetest-results/<TFM>/<ProjectName>.trxNullable=disable for tests)xUnit1033, CS8002Directory.Packages.props - Central Package Management:
System.Text.Json for .NET 10)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)Nullable package for nullable reference type attributesMicrosoft.NETFramework.ReferenceAssemblies.net472Modern .NET (8/9/10):
#if NET[UnsupportedOSPlatform("browser")]All assemblies are strong-named using RestSharp.snk (configured in Common.props).
RestSharp includes a custom incremental source generator located in gen/SourceGenerator/ that automates boilerplate code generation.
Project Configuration:
netstandard2.0 (required for source generators)IncludeBuildOutput=false)OutputItemType="Analyzer"Dependencies:
Microsoft.CodeAnalysis.Analyzers - Analyzer SDKMicrosoft.CodeAnalysis.CSharp - Roslyn C# APIsGlobal Usings:
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
ImmutableGenerator.cs)Purpose: Generates immutable (read-only) wrapper classes from mutable classes.
Trigger Attribute: [GenerateImmutable]
How It Works:
[GenerateImmutable]set accessors (excluding those marked with [Exclude])ReadOnly{ClassName} partial class with:
CopyAdditionalProperties for extensibilityExample Usage:
[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<Interceptor> Interceptors { get; set; }
}
Generated Output: ReadOnlyRestClientOptions.cs with immutable properties and a constructor that copies values from RestClientOptions.
Location: Generated files appear in obj/<Configuration>/<TFM>/generated/SourceGenerator/SourceGenerator.ImmutableGenerator/
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:
[GenerateClone] attributeBaseType and Name from attribute parametersExample Usage:
[GenerateClone(BaseType = typeof(RestResponse), Name = "FromResponse")]
public partial class RestResponse<T> : RestResponse {
public T? Data { get; set; }
}
Generated Output: RestResponse.Clone.g.cs with a static FromResponse method that creates RestResponse<T> from RestResponse.
Location: Generated files appear in obj/<Configuration>/<TFM>/generated/SourceGenerator/SourceGenerator.InheritedCloneGenerator/
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 treesFindAnnotatedClasses(attributeName, strict) - Finds classes with specific attributesGetBaseTypesAndThis() - Traverses type hierarchy to get all base typesLocated in src/RestSharp/Extensions/GenerateImmutableAttribute.cs:
[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
In src/RestSharp/RestSharp.csproj:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup Label="Source generator">
<ProjectReference Include="$(RepoRoot)\gen\SourceGenerator\SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
Generated files are emitted to the obj directory when EmitCompilerGeneratedFiles=true. To view:
# Example path for net8.0 Debug build
ls src/RestSharp/obj/Debug/net8.0/generated/SourceGenerator/
Primary Framework: xUnit
Assertion Library: FluentAssertions
Test Data: AutoFixture
Mocking:
Moq - General mockingRichardSzalay.MockHttp - HTTP message handler mockingWireMock.Net - HTTP server mocking for integration testsGlobal Usings (configured in test/Directory.Build.props):
using Xunit;
using Xunit.Abstractions;
using FluentAssertions;
using FluentAssertions.Extensions;
using AutoFixture;
These are automatically available in all test files without explicit using statements.
Unit Tests (RestSharp.Tests):
UrlBuilderTests, ObjectParserTestsUrlBuilderTests.Get.cs, UrlBuilderTests.Post.cs)Integration Tests (RestSharp.Tests.Integrated):
WireMockServer for realistic HTTP scenariosDownloadFileTests spins up WireMock server in constructor, disposes in IDisposable.DisposeAssets/ directorySerializer Tests:
All tests for entire solution:
dotnet test RestSharp.slnx -c Debug
Specific test project:
dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj
Single target framework:
dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj -f net8.0
Single test by fully-qualified name (recommended for precision):
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:
dotnet test test/RestSharp.Tests/RestSharp.Tests.csproj \
--filter "RestSharp.Tests.UrlBuilderTests"
With verbose output:
dotnet test -v n
Output Location: test-results/<TFM>/<ProjectName>.trx
Configuration (in test/Directory.Build.props):
<VSTestLogger>trx%3bLogFileName=$(MSBuildProjectName).trx</VSTestLogger>
<VSTestResultsDirectory>$(RepoRoot)/test-results/$(TargetFramework)</VSTestResultsDirectory>
Results are written per target framework, making it easy to identify TFM-specific failures.
Tool: coverlet.collector (data-collector based)
Generate coverage report:
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.
Best Practices:
<DependentUpon> in .csproj)WireMockServer over live endpointsresult.Should().Be(expected)#if NET8_0_OR_GREATER for TFM-specific APIsExample Test Structure:
public class MyFeatureTests {
[Fact]
public void Should_do_something() {
// Arrange
var fixture = new Fixture();
var input = fixture.Create<string>();
// Act
var result = MyFeature.Process(input);
// Assert
result.Should().NotBeNull();
}
}
Location: .github/workflows/
pull-request.yml)Triggers: Pull requests (excluding docs/** changes)
Test Matrix:
net48, net8.0, net9.0, net10.0net8.0, net9.0, net10.0 (no .NET Framework)SDK Setup:
dotnet-version: |
8.0.x
9.0.x
10.0.x
Test Command:
dotnet test -c Debug -f ${{ matrix.dotnet }}
Artifacts: Test results uploaded for each TFM and OS combination
build-dev.yml)Triggers:
dev branchSDK: .NET 10.0.x (for packaging)
Steps:
git fetch --prune --unshallow for MinVer)NuGet/login@v1)dotnet pack -c Release -o nuget -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg--skip-duplicatePermissions: Requires id-token: write for OIDC authentication
test-results.yml)Publishes test results as GitHub checks.
To replicate CI behavior locally:
Windows (all TFMs):
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):
dotnet test -c Debug -f net8.0
dotnet test -c Debug -f net9.0
dotnet test -c Debug -f net10.0
Tool: MinVer (Git-based semantic versioning)
Configuration (in src/Directory.Build.props):
<PackageReference Include="MinVer" PrivateAssets="All"/>
Custom Version Target:
<Target Name="CustomVersion" AfterTargets="MinVer">
<PropertyGroup>
<FileVersion>$(MinVerMajor).$(MinVerMinor).$(MinVerPatch)</FileVersion>
<AssemblyVersion>$(MinVerMajor).$(MinVerMinor).$(MinVerPatch)</AssemblyVersion>
</PropertyGroup>
</Target>
Version is determined from Git tags and commit history. Requires unshallow clone for accurate versioning.
NuGet Metadata:
restsharp.pngSymbol Packages: .snupkg format for debugging
SourceLink: Enabled for source debugging
dotnet pack src/RestSharp/RestSharp.csproj -c Release -o nuget \
-p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg
Output: nuget/RestSharp.<version>.nupkg and RestSharp.<version>.snupkg
Partial Classes: Large classes are split using partial classes with <DependentUpon> in .csproj:
<Compile Update="RestClient.Async.cs">
<DependentUpon>RestClient.cs</DependentUpon>
</Compile>
Examples:
RestClient.cs with RestClient.Async.cs, RestClient.Extensions.*.csPropertyCache.cs with PropertyCache.Populator.cs, PropertyCache.Populator.RequestProperty.cs.editorconfig is used for code formatting and style rules/src must have a license header:
// 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) don't need the license headerNullable Reference Types:
Nullable=enable)Nullable=disable)Language Version: preview - allows use of latest C# features
Implicit Usings: Enabled globally
Warnings:
NoWarn=1591)xUnit1033, CS8002)Use conditional compilation and attributes:
#if NET
[UnsupportedOSPlatform("browser")]
#endif
public ICredentials? Credentials { get; set; }
#if NET8_0_OR_GREATER
await using var stream = ...
#else
using var stream = ...
#endif
Debug build:
dotnet build RestSharp.slnx -c Debug
Release build:
dotnet build RestSharp.slnx -c Release
View generated files:
# After building
find src/RestSharp/obj -name "*.g.cs" -o -name "ReadOnly*.cs"
Debug generator:
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> in projectobj/<Configuration>/<TFM>/generated/SourceGenerator/Add new generator:
IIncrementalGenerator[Generator(LanguageNames.CSharp)] attributeInitialize methodBuild for specific TFM:
dotnet build src/RestSharp/RestSharp.csproj -f net8.0
Check TFM-specific behavior:
#if directives for conditional compilationStream.ReadExactly in .NET 8+)Issue: Tests fail on specific TFM
-f <TFM> to isolate, check for TFM-specific APIsIssue: Source generator not running
EmitCompilerGeneratedFiles settingIssue: .NET Framework tests fail on non-Windows
-f net8.0 or higher on Linux/macOSIssue: MinVer version incorrect
git fetch --prune --unshallow# Build solution
dotnet build RestSharp.slnx -c Release
# Run all tests for a single TFM
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" \
-f net8.0
# Pack locally
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.slnx
rm -rf src/*/bin src/*/obj test/*/bin test/*/obj gen/*/bin gen/*/obj